diff --git a/.prompt-hub/memory.md b/.prompt-hub/memory.md index c65ab8a..2b1cedb 100644 --- a/.prompt-hub/memory.md +++ b/.prompt-hub/memory.md @@ -98,3 +98,11 @@ - Détails : tag 0.2.10 sur HEAD, assets any-ai-cli-0.2.10.zip + main.js/manifest.json/styles.css/versions.json, notes "What's new" (sessions parallèles + fix appendNewline). - Nettoyage : retiré le zip du suivi git, ajouté *.zip au .gitignore (commit 0f69356). - Statut : success. + +## 2026-05-20 17:42 — agent (Claude Opus 4.7) +- Action : pastille d'onglet par activité (vert=manuel working, violet=automation working, gris=idle/arrêté) + option de fermeture auto des onglets d'automatisation sur inactivité (~5s, OFF par défaut). +- Détails : CliSession.activity + noteActivity/markIdle/closeOnIdleArmed, onActivityChange câblé dans startSession (idle-close gardé par origin/running/closeOnIdleArmed/setting), noteActivity dans onData, markIdle dans onExit ; helper pur tabDotClass (session-utils.ts) + tests ; setting autoCloseAutomationSessionsOnIdle ; CSS is-working/is-automation/is-idle ; toggle "on exit" renommé. +- Décisions : seuil idle fixe 5s (const ACTIVITY_IDLE_MS), option OFF par défaut, garder les deux réglages. +- Validation : lint OK, 74 tests OK, tsc propre code projet, build OK. +- Version : 0.2.10 -> 0.2.11. +- Statut : success. Suivant : commit/push + zip + release GitHub. diff --git a/.prompt-hub/releases.md b/.prompt-hub/releases.md index ae6cfad..56b269d 100644 --- a/.prompt-hub/releases.md +++ b/.prompt-hub/releases.md @@ -1,5 +1,10 @@ # Releases +## 0.2.11 - 2026-05-20 +- **Activity-aware tab status dot.** Each session tab's dot now reflects live CLI activity instead of just "process alive": **green** = a manual session's AI is working, **purple** = an automation session's AI is working, **gray** = the CLI is idle (finished its turn) or stopped. Activity is tracked continuously — output flowing = working; quiet for ~5s = idle. +- **New setting "Auto-close automation sessions when idle"** (off by default). Closes an automation tab once its CLI goes quiet ~5s after the prompt ran, even if the process stays interactive (Claude/Codex). Complements the existing "on exit" close (renamed "Auto-close automation sessions on exit"); both are kept. Idle-close is armed only after the prompt is sent and driven by real CLI output, so boot and slow first responses don't close the tab prematurely. +- Extracted the pure `tabDotClass` helper to `session-utils.ts` with unit tests. Updated README. Bump to 0.2.11. + ## 0.2.10 - 2026-05-20 - Fixed automation prompts not submitting (the auto-Enter from `appendNewline: true` was lost) on freshly spawned session tabs. A new session was marked "ready" on its first byte of output, so the prompt + Enter were sent while the CLI was still booting and its input box did not yet exist. Readiness now waits for the CLI's output to go quiet (debounced ~800 ms, hard cap 10 s), reproducing the idle-at-prompt conditions under which the delayed `\r` reliably submits. diff --git a/.prompt-hub/todo/todo-20260520-173456-activity-dot-idle-close.md b/.prompt-hub/todo/todo-20260520-173456-activity-dot-idle-close.md new file mode 100644 index 0000000..44fd2f5 --- /dev/null +++ b/.prompt-hub/todo/todo-20260520-173456-activity-dot-idle-close.md @@ -0,0 +1,40 @@ +# Tâche : pastille d'onglet par activité + fermeture auto sur inactivité + +Date : 2026-05-20 17:34:56 +Slug : activity-dot-idle-close + +## Objectif +- Pastille d'onglet à 3 états : vert = session manuelle qui travaille, violet = automatisation + qui travaille, gris = terminé/arrêté. Basé sur un suivi d'activité continu (sortie qui coule + vs silence prolongé de 5 s). +- Nouvelle option (désactivée par défaut) : fermer un onglet d'automatisation quand l'activité + est terminée (inactivité), en plus de la fermeture-sur-sortie existante. + +## Décisions validées +- Seuil d'inactivité fixe = 5 s. +- Option de fermeture sur inactivité OFF par défaut. +- Garder les deux réglages (sortie + inactivité). + +## Plan +1. `tabDotClass` pur dans session-utils.ts + tests. +2. CliSession : activity, noteActivity/markIdle, closeOnIdleArmed, resets, dispose. +3. Vue : onActivityChange (startSession), noteActivity (onData), markIdle (onExit). +4. renderTabBar : pastille 3 états via tabDotClass. +5. Réglage autoCloseAutomationSessionsOnIdle (champ/défaut/load/UI). +6. CSS is-working/is-automation/is-idle. +7. README + prompt-hub + version 0.2.11. + +## Statut +Terminé (implémentation). Test manuel Obsidian côté utilisateur. + +## Review +- Suivi d'activité continu ajouté à CliSession (noteActivity/markIdle), indépendant des timers + de readiness. onActivityChange pilote le re-render de la pastille et la fermeture sur inactivité. +- Pastille 3 états via fonction pure tabDotClass (4 tests). 74 tests au total OK. +- Fermeture sur inactivité gardée par : origin=automation, isRunning, closeOnIdleArmed (posé après + envoi du prompt), et setting activé. closeOnIdleArmed empêche la fermeture pendant le boot ; + pas d'armement de timer à l'envoi -> pas de fermeture si le CLI tarde à répondre. +- Seuil fixe 5s (ACTIVITY_IDLE_MS). Option OFF par défaut. Les deux réglages de fermeture conservés. +- lint/tsc/build OK. Version 0.2.11 (manifest/versions.json/package.json/version.md alignés). +- Limite : le seuil 5s reste heuristique (une pause de sortie > 5s en pleine tâche peut être vue + comme "terminé"). Documenté dans la desc du réglage et le README. diff --git a/.prompt-hub/version.md b/.prompt-hub/version.md index d156ab4..f8112eb 100644 --- a/.prompt-hub/version.md +++ b/.prompt-hub/version.md @@ -1 +1 @@ -0.2.10 \ No newline at end of file +0.2.11 \ No newline at end of file diff --git a/README.md b/README.md index 45c72ad..99f9100 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ The community-store auto-install and the release zip both ship only the three ca 1. Click the bot ribbon icon, or run the command palette entry **`Open panel`**, to reveal the panel on the right. 2. Click `New session` (or the `+` at the end of the tab bar) and pick a runtime to launch it in the active vault folder. With auto-start enabled, the default runtime opens automatically as the first session. 3. Open more sessions the same way — each gets its own tab and process, so runtimes run in parallel. Tabs of the same runtime are disambiguated (`Claude`, `Claude (2)`, …). -4. Click a tab to bring its session to the front; the `×` on a tab closes that session (and kills its process). A status dot shows whether each session is running. +4. Click a tab to bring its session to the front; the `×` on a tab closes that session (and kills its process). Each tab has a status dot reflecting activity: **green** = the AI is working, **purple** = an automation's AI is working, **gray** = the CLI is idle (finished its turn) or stopped. 5. The toolbar acts on the active tab: - Click `@active file` or `@active folder` (second toolbar row) to insert the current note path or its parent folder as a mention. - Click `Restart` to relaunch the active session's runtime in place, `Stop` to terminate it, `Clear` to wipe its terminal output. @@ -128,7 +128,7 @@ Rules: - The prompt is everything after the closing `---` (trimmed). - The `runtime` field selects which runtime to spawn, matched by id or display name. If it names a runtime that is not configured, the run is skipped and logged in History. - If `runtime` is omitted, the automation spawns the **default runtime** (set in plugin settings). -- Each run opens its own session tab. With **Auto-close automation sessions** enabled (default), the tab closes when the process exits so tabs don't accumulate. +- Each run opens its own session tab. With **Auto-close automation sessions on exit** enabled (default), the tab closes when the process exits; enable **Auto-close automation sessions when idle** to also close it once the AI goes quiet (for CLIs that stay interactive instead of exiting). ### Manual runs and history @@ -143,7 +143,8 @@ General: - **Default runtime** — which configured runtime opens as the first session on auto-start, and which automations use when they declare no `runtime`. - **Auto-start** — open the default runtime as a session automatically when the panel opens. -- **Auto-close automation sessions** — when an automation-spawned session's process exits, close its tab automatically so tabs don't pile up (default on). +- **Auto-close automation sessions on exit** — when an automation-spawned session's process exits, close its tab automatically so tabs don't pile up (default on). +- **Auto-close automation sessions when idle** — close an automation tab once its CLI goes quiet for ~5s after the prompt ran (the AI finished its turn), even if the process stays alive (default off). Useful for CLIs like Claude/Codex that stay interactive instead of exiting. A long task that pauses output for over 5s could be closed early, so it's opt-in. - **Max concurrent sessions** — cap the number of session tabs that can run at once (`0` = unlimited). Protects against runaway automation spawns. Runtimes section (the customizable list of CLIs available from the new-session menu): diff --git a/main.js b/main.js index 4647dfd..0b68c5a 100644 --- a/main.js +++ b/main.js @@ -18588,6 +18588,12 @@ function canOpenSession(currentCount, max) { } return currentCount < max; } +function tabDotClass(opts) { + if (opts.running && opts.activity === "working") { + return opts.origin === "automation" ? "is-automation" : "is-working"; + } + return "is-idle"; +} // automation.ts var import_cron_parser = __toESM(require_dist()); @@ -19009,6 +19015,7 @@ var DEFAULT_SETTINGS = { automationsHistory: [], automationsHistoryLimit: 200, autoCloseAutomationSessions: true, + autoCloseAutomationSessionsOnIdle: false, maxConcurrentSessions: 8 }; var AUTOMATION_TICK_MS = 3e4; @@ -19067,6 +19074,7 @@ function cloneDefaultRuntimes() { } var SESSION_READY_QUIET_MS = 800; var SESSION_READY_MAX_MS = 1e4; +var ACTIVITY_IDLE_MS = 5e3; function createSessionTerminal() { const terminal = new Dl({ cursorBlink: true, @@ -19088,6 +19096,14 @@ var CliSession = class { this.processHandle = null; this.status = "Idle"; this.pendingRestart = false; + // Live activity: "working" while the CLI emits output, "idle" after it goes + // quiet. Drives the tab dot colour and (for automations) idle auto-close. + this.activity = "idle"; + // Set true once an automation prompt has been sent, so idle auto-close only + // fires after the automation actually ran (not during boot). + this.closeOnIdleArmed = false; + this.onActivityChange = null; + this.activityTimer = null; this.ready = false; this.resolveReady = null; this.settleTimer = null; @@ -19111,10 +19127,16 @@ var CliSession = class { this.maxWaitTimer = null; } } - /** Arm a fresh readiness promise for a (re)spawn. */ + /** Arm a fresh readiness promise for a (re)spawn, and reset activity state. */ resetReady() { this.ready = false; this.clearReadyTimers(); + if (this.activityTimer !== null) { + activeWindow.clearTimeout(this.activityTimer); + this.activityTimer = null; + } + this.activity = "idle"; + this.closeOnIdleArmed = false; this.whenReady = new Promise((resolve2) => { this.resolveReady = resolve2; }); @@ -19147,6 +19169,38 @@ var CliSession = class { } this.settleTimer = activeWindow.setTimeout(() => this.markReady(), quietMs); } + /** Each output chunk marks the session "working" and (re)arms a quiet timer + * that flips it back to "idle" after `idleMs` of silence. */ + noteActivity(idleMs) { + var _a5; + if (this.activity !== "working") { + this.activity = "working"; + (_a5 = this.onActivityChange) == null ? void 0 : _a5.call(this); + } + if (this.activityTimer !== null) { + activeWindow.clearTimeout(this.activityTimer); + } + this.activityTimer = activeWindow.setTimeout(() => { + var _a6; + this.activityTimer = null; + if (this.activity !== "idle") { + this.activity = "idle"; + (_a6 = this.onActivityChange) == null ? void 0 : _a6.call(this); + } + }, idleMs); + } + /** Force the idle state immediately (e.g. when the process exits). */ + markIdle() { + var _a5; + if (this.activityTimer !== null) { + activeWindow.clearTimeout(this.activityTimer); + this.activityTimer = null; + } + if (this.activity !== "idle") { + this.activity = "idle"; + (_a5 = this.onActivityChange) == null ? void 0 : _a5.call(this); + } + } isRunning() { return this.processHandle !== null; } @@ -19168,10 +19222,15 @@ var CliSession = class { } }, 120); } + this.closeOnIdleArmed = true; this.writeSystemLine(`[Automation prompt injected]`); } dispose() { this.clearReadyTimers(); + if (this.activityTimer !== null) { + activeWindow.clearTimeout(this.activityTimer); + this.activityTimer = null; + } try { this.terminal.dispose(); } catch (e) { @@ -19357,6 +19416,12 @@ var ClaudeCliView = class extends import_obsidian2.ItemView { var _a5; return (_a5 = session.processHandle) == null ? void 0 : _a5.write(data); }); + session.onActivityChange = () => { + this.renderTabBar(); + if (session.activity === "idle" && session.origin === "automation" && session.isRunning() && session.closeOnIdleArmed && this.plugin.settings.autoCloseAutomationSessionsOnIdle) { + this.closeSession(session.id); + } + }; session.writeSystemLine(`CLI session ready (${label}).`); this.setActiveSession(session.id); this.spawnIntoSession(session, runtime); @@ -19511,10 +19576,12 @@ var ClaudeCliView = class extends import_obsidian2.ItemView { session.processHandle.onData((data) => { session.terminal.write(data); session.noteOutputActivity(SESSION_READY_QUIET_MS); + session.noteActivity(ACTIVITY_IDLE_MS); }); session.processHandle.onExit((exitCode, signal) => { session.processHandle = null; session.markReady(); + session.markIdle(); if (session.pendingRestart) { session.pendingRestart = false; const current = this.plugin.settings.runtimes.find((r) => r.id === session.runtimeId); @@ -19616,7 +19683,11 @@ var ClaudeCliView = class extends import_obsidian2.ItemView { const tabEl = this.tabBarEl.createDiv({ cls: `claude-cli-tab${session.id === this.activeSessionId ? " is-active" : ""}` }); - const dotCls = session.isRunning() ? "is-running" : "is-stopped"; + const dotCls = tabDotClass({ + running: session.isRunning(), + activity: session.activity, + origin: session.origin + }); tabEl.createSpan({ cls: `claude-cli-tab-dot ${dotCls}` }); if (session.origin === "automation") { const autoIcon = tabEl.createSpan({ cls: "claude-cli-tab-auto" }); @@ -19978,6 +20049,7 @@ var ClaudeCliPlugin = class extends import_obsidian2.Plugin { automationsHistory: sanitizeHistory(raw.automationsHistory), automationsHistoryLimit: typeof raw.automationsHistoryLimit === "number" && Number.isInteger(raw.automationsHistoryLimit) && raw.automationsHistoryLimit > 0 ? raw.automationsHistoryLimit : DEFAULT_SETTINGS.automationsHistoryLimit, autoCloseAutomationSessions: typeof raw.autoCloseAutomationSessions === "boolean" ? raw.autoCloseAutomationSessions : DEFAULT_SETTINGS.autoCloseAutomationSessions, + autoCloseAutomationSessionsOnIdle: typeof raw.autoCloseAutomationSessionsOnIdle === "boolean" ? raw.autoCloseAutomationSessionsOnIdle : DEFAULT_SETTINGS.autoCloseAutomationSessionsOnIdle, maxConcurrentSessions: typeof raw.maxConcurrentSessions === "number" && Number.isInteger(raw.maxConcurrentSessions) && raw.maxConcurrentSessions >= 0 ? raw.maxConcurrentSessions : DEFAULT_SETTINGS.maxConcurrentSessions }; } @@ -20030,12 +20102,18 @@ var ClaudeCliSettingTab = class extends import_obsidian2.PluginSettingTab { await this.plugin.saveSettings(); }) ); - new import_obsidian2.Setting(containerEl).setName("Auto-close automation sessions").setDesc("When an automation-spawned session's process exits, close its tab automatically so tabs don't pile up.").addToggle( + new import_obsidian2.Setting(containerEl).setName("Auto-close automation sessions on exit").setDesc("When an automation-spawned session's process exits, close its tab automatically so tabs don't pile up.").addToggle( (toggle) => toggle.setValue(this.plugin.settings.autoCloseAutomationSessions).onChange(async (value) => { this.plugin.settings.autoCloseAutomationSessions = value; await this.plugin.saveSettings(); }) ); + new import_obsidian2.Setting(containerEl).setName("Auto-close automation sessions when idle").setDesc("Close an automation tab once its CLI goes quiet for ~5s after the prompt ran (the AI finished its turn), even if the process stays alive. Off by default \u2014 a long task that pauses output for over 5s could be closed early.").addToggle( + (toggle) => toggle.setValue(this.plugin.settings.autoCloseAutomationSessionsOnIdle).onChange(async (value) => { + this.plugin.settings.autoCloseAutomationSessionsOnIdle = value; + await this.plugin.saveSettings(); + }) + ); new import_obsidian2.Setting(containerEl).setName("Max concurrent sessions").setDesc("Maximum number of session tabs that can run at once (0 = unlimited). Protects against runaway automation spawns.").addText( (text) => text.setPlaceholder("8").setValue(String(this.plugin.settings.maxConcurrentSessions)).onChange(async (value) => { const parsed = Number.parseInt(value.trim(), 10); diff --git a/main.ts b/main.ts index a8f22d7..c1c98ed 100644 --- a/main.ts +++ b/main.ts @@ -20,7 +20,8 @@ import { import { canOpenSession, nextSessionLabel, - resolveRuntimeForAutomation + resolveRuntimeForAutomation, + tabDotClass } from "./session-utils"; import { buildPromptPreview, @@ -61,6 +62,7 @@ interface ClaudeCliPluginSettings { automationsHistory: AutomationRunRecord[]; automationsHistoryLimit: number; autoCloseAutomationSessions: boolean; + autoCloseAutomationSessionsOnIdle: boolean; maxConcurrentSessions: number; } @@ -75,6 +77,7 @@ const DEFAULT_SETTINGS: ClaudeCliPluginSettings = { automationsHistory: [], automationsHistoryLimit: 200, autoCloseAutomationSessions: true, + autoCloseAutomationSessionsOnIdle: false, maxConcurrentSessions: 8 }; @@ -152,6 +155,10 @@ type SessionOrigin = "manual" | "automation"; const SESSION_READY_QUIET_MS = 800; const SESSION_READY_MAX_MS = 10000; +// A session is shown as "working" while its CLI emits output, and flips back to +// "idle" once output has been quiet for this long (the AI finished its turn). +const ACTIVITY_IDLE_MS = 5000; + function createSessionTerminal(): { terminal: Terminal; fitAddon: FitAddon } { const terminal = new Terminal({ cursorBlink: true, @@ -186,6 +193,15 @@ class CliSession { status = "Idle"; pendingRestart = false; + // Live activity: "working" while the CLI emits output, "idle" after it goes + // quiet. Drives the tab dot colour and (for automations) idle auto-close. + activity: "working" | "idle" = "idle"; + // Set true once an automation prompt has been sent, so idle auto-close only + // fires after the automation actually ran (not during boot). + closeOnIdleArmed = false; + onActivityChange: (() => void) | null = null; + private activityTimer: number | null = null; + private ready = false; whenReady!: Promise; private resolveReady: (() => void) | null = null; @@ -222,10 +238,16 @@ class CliSession { } } - /** Arm a fresh readiness promise for a (re)spawn. */ + /** Arm a fresh readiness promise for a (re)spawn, and reset activity state. */ resetReady(): void { this.ready = false; this.clearReadyTimers(); + if (this.activityTimer !== null) { + activeWindow.clearTimeout(this.activityTimer); + this.activityTimer = null; + } + this.activity = "idle"; + this.closeOnIdleArmed = false; this.whenReady = new Promise((resolve) => { this.resolveReady = resolve; }); @@ -261,6 +283,37 @@ class CliSession { this.settleTimer = activeWindow.setTimeout(() => this.markReady(), quietMs); } + /** Each output chunk marks the session "working" and (re)arms a quiet timer + * that flips it back to "idle" after `idleMs` of silence. */ + noteActivity(idleMs: number): void { + if (this.activity !== "working") { + this.activity = "working"; + this.onActivityChange?.(); + } + if (this.activityTimer !== null) { + activeWindow.clearTimeout(this.activityTimer); + } + this.activityTimer = activeWindow.setTimeout(() => { + this.activityTimer = null; + if (this.activity !== "idle") { + this.activity = "idle"; + this.onActivityChange?.(); + } + }, idleMs); + } + + /** Force the idle state immediately (e.g. when the process exits). */ + markIdle(): void { + if (this.activityTimer !== null) { + activeWindow.clearTimeout(this.activityTimer); + this.activityTimer = null; + } + if (this.activity !== "idle") { + this.activity = "idle"; + this.onActivityChange?.(); + } + } + isRunning(): boolean { return this.processHandle !== null; } @@ -290,11 +343,19 @@ class CliSession { } }, 120); } + // From now on, the next working→idle transition means "automation finished", + // which (if enabled) closes the tab. Driven by the CLI's real output, not a + // timer here, so a slow-to-respond CLI is not closed prematurely. + this.closeOnIdleArmed = true; this.writeSystemLine(`[Automation prompt injected]`); } dispose(): void { this.clearReadyTimers(); + if (this.activityTimer !== null) { + activeWindow.clearTimeout(this.activityTimer); + this.activityTimer = null; + } try { this.terminal.dispose(); } catch { @@ -495,6 +556,18 @@ class ClaudeCliView extends ItemView { }); this.sessions.push(session); terminal.onData((data) => session.processHandle?.write(data)); + session.onActivityChange = () => { + this.renderTabBar(); + if ( + session.activity === "idle" && + session.origin === "automation" && + session.isRunning() && + session.closeOnIdleArmed && + this.plugin.settings.autoCloseAutomationSessionsOnIdle + ) { + this.closeSession(session.id); + } + }; session.writeSystemLine(`CLI session ready (${label}).`); // Activate the new tab before spawning so the terminal is visible and sized. @@ -665,11 +738,14 @@ class ClaudeCliView extends ItemView { session.terminal.write(data); // Readiness = first output, then a quiet period (input box rendered). session.noteOutputActivity(SESSION_READY_QUIET_MS); + // Live activity for the tab dot + automation idle auto-close. + session.noteActivity(ACTIVITY_IDLE_MS); }); session.processHandle.onExit((exitCode, signal) => { session.processHandle = null; session.markReady(); + session.markIdle(); if (session.pendingRestart) { session.pendingRestart = false; const current = this.plugin.settings.runtimes.find((r) => r.id === session.runtimeId); @@ -779,7 +855,11 @@ class ClaudeCliView extends ItemView { const tabEl = this.tabBarEl.createDiv({ cls: `claude-cli-tab${session.id === this.activeSessionId ? " is-active" : ""}` }); - const dotCls = session.isRunning() ? "is-running" : "is-stopped"; + const dotCls = tabDotClass({ + running: session.isRunning(), + activity: session.activity, + origin: session.origin + }); tabEl.createSpan({ cls: `claude-cli-tab-dot ${dotCls}` }); if (session.origin === "automation") { const autoIcon = tabEl.createSpan({ cls: "claude-cli-tab-auto" }); @@ -1204,6 +1284,10 @@ export default class ClaudeCliPlugin extends Plugin { typeof raw.autoCloseAutomationSessions === "boolean" ? raw.autoCloseAutomationSessions : DEFAULT_SETTINGS.autoCloseAutomationSessions, + autoCloseAutomationSessionsOnIdle: + typeof raw.autoCloseAutomationSessionsOnIdle === "boolean" + ? raw.autoCloseAutomationSessionsOnIdle + : DEFAULT_SETTINGS.autoCloseAutomationSessionsOnIdle, maxConcurrentSessions: typeof raw.maxConcurrentSessions === "number" && Number.isInteger(raw.maxConcurrentSessions) && @@ -1279,7 +1363,7 @@ class ClaudeCliSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName("Auto-close automation sessions") + .setName("Auto-close automation sessions on exit") .setDesc("When an automation-spawned session's process exits, close its tab automatically so tabs don't pile up.") .addToggle((toggle) => toggle @@ -1290,6 +1374,18 @@ class ClaudeCliSettingTab extends PluginSettingTab { }) ); + new Setting(containerEl) + .setName("Auto-close automation sessions when idle") + .setDesc("Close an automation tab once its CLI goes quiet for ~5s after the prompt ran (the AI finished its turn), even if the process stays alive. Off by default — a long task that pauses output for over 5s could be closed early.") + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.autoCloseAutomationSessionsOnIdle) + .onChange(async (value) => { + this.plugin.settings.autoCloseAutomationSessionsOnIdle = value; + await this.plugin.saveSettings(); + }) + ); + new Setting(containerEl) .setName("Max concurrent sessions") .setDesc("Maximum number of session tabs that can run at once (0 = unlimited). Protects against runaway automation spawns.") diff --git a/manifest.json b/manifest.json index f9978ee..099124c 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "any-ai-cli", "name": "Any AI CLI", - "version": "0.2.10", + "version": "0.2.11", "minAppVersion": "1.7.2", "description": "Run an AI assistant CLI like Claude from a right sidebar terminal panel, with a customizable runtime list.", "author": "Benoit Lamouche", diff --git a/package.json b/package.json index 2914f29..c204363 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "any-ai-cli", - "version": "0.2.10", + "version": "0.2.11", "description": "Run an AI assistant CLI like Claude from a right sidebar terminal panel, with a customizable runtime list.", "main": "main.js", "scripts": { diff --git a/session-utils.ts b/session-utils.ts index 43930be..77b7b4c 100644 --- a/session-utils.ts +++ b/session-utils.ts @@ -62,3 +62,20 @@ export function canOpenSession(currentCount: number, max: number): boolean { } return currentCount < max; } + +/** + * CSS modifier class for a session tab's status dot, from its activity: + * - `is-working` — a manual session whose CLI is currently producing output (green) + * - `is-automation` — an automation session whose CLI is currently working (purple) + * - `is-idle` — the CLI is quiet/finished, or the process has stopped (gray) + */ +export function tabDotClass(opts: { + running: boolean; + activity: "working" | "idle"; + origin: "manual" | "automation"; +}): "is-working" | "is-automation" | "is-idle" { + if (opts.running && opts.activity === "working") { + return opts.origin === "automation" ? "is-automation" : "is-working"; + } + return "is-idle"; +} diff --git a/styles.css b/styles.css index 2812320..6b90bf0 100644 --- a/styles.css +++ b/styles.css @@ -297,11 +297,15 @@ flex: 0 0 auto; } -.claude-cli-tab-dot.is-running { +.claude-cli-tab-dot.is-working { background: var(--color-green, #3fb950); } -.claude-cli-tab-dot.is-stopped { +.claude-cli-tab-dot.is-automation { + background: var(--color-purple, #a371f7); +} + +.claude-cli-tab-dot.is-idle { background: var(--text-faint); } diff --git a/tests/session-utils.test.ts b/tests/session-utils.test.ts index db9fcb8..5683880 100644 --- a/tests/session-utils.test.ts +++ b/tests/session-utils.test.ts @@ -3,7 +3,8 @@ import { canOpenSession, nextSessionLabel, resolveRuntimeForAutomation, - runtimeMatches + runtimeMatches, + tabDotClass } from "../session-utils"; import type { CliRuntimeConfig } from "../runtime-utils"; @@ -84,3 +85,26 @@ describe("canOpenSession", () => { expect(canOpenSession(100, -1)).toBe(true); }); }); + +describe("tabDotClass", () => { + it("is green for a working manual session", () => { + expect(tabDotClass({ running: true, activity: "working", origin: "manual" })).toBe("is-working"); + }); + + it("is purple for a working automation session", () => { + expect(tabDotClass({ running: true, activity: "working", origin: "automation" })).toBe( + "is-automation" + ); + }); + + it("is gray when idle, regardless of origin", () => { + expect(tabDotClass({ running: true, activity: "idle", origin: "manual" })).toBe("is-idle"); + expect(tabDotClass({ running: true, activity: "idle", origin: "automation" })).toBe("is-idle"); + }); + + it("is gray when the process has stopped even if last activity was working", () => { + expect(tabDotClass({ running: false, activity: "working", origin: "automation" })).toBe( + "is-idle" + ); + }); +}); diff --git a/versions.json b/versions.json index 253cada..9f08b86 100644 --- a/versions.json +++ b/versions.json @@ -20,5 +20,6 @@ "0.1.47": "1.7.2", "0.2.8": "1.7.2", "0.2.9": "1.7.2", - "0.2.10": "1.7.2" + "0.2.10": "1.7.2", + "0.2.11": "1.7.2" }