From e2085a323bd64ef93b7b3916879aa71c98e69b03 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 21 Sep 2026 11:22:19 +0000 Subject: [PATCH] fix(work): apply Daniels palette to terminal status dots Remap Workbench tab and open-tabs indicators so agent waiting is blue, agent working pulses amber, clean exits are green, and failed exits stay red. Detection is unchanged. Co-authored-by: Daniels --- README.md | 4 ++-- ROADMAP.md | 9 +++++---- TASKS.md | 7 +++++-- docs/learnings.md | 10 +++++++--- ui/src/lib/i18n.test.ts | 4 ++++ ui/src/lib/i18n.ts | 7 ++++--- ui/src/lib/terminalAgentActivity.test.ts | 13 ++++++++++--- ui/src/lib/terminalAgentActivity.ts | 5 ++++- ui/src/styles/features.css | 9 ++++++--- ui/src/styles/tokens.css | 6 ++++++ ui/src/views/Work.test.ts | 11 +++++++++-- ui/src/views/Work.tsx | 8 ++++---- website/docs/work.md | 20 +++++++++++--------- 13 files changed, 77 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index fe00a90c..59713f44 100644 --- a/README.md +++ b/README.md @@ -136,8 +136,8 @@ the resolved app appearance automatically. grid. On Windows and Linux, Ctrl+C copies a terminal selection (else interrupt) and Ctrl+V pastes; macOS keeps ⌘C/⌘V. Claude Code starts with its complete dashboard and alternate-screen renderer in a configurable terminal font and size. Work tabs keep their width in a wheel-scrollable strip with an overflow - selector and tree-matched file icons, and middle-click closes a tab. Claude Code and Codex CLI tabs pulse the - running status dot while a turn is executing and return to steady green when the agent is idle at a prompt. + selector and tree-matched file icons, and middle-click closes a tab. Claude Code and Codex CLI tabs pulse amber + while a turn is executing and turn blue when the agent is waiting at a prompt; a clean exit is green and a failed exit is red. Only descriptors restore after relaunch; selecting one starts a fresh process. The New Terminal split button can launch a one-off native or WSL shell, while Settings → Terminal provides diff --git a/ROADMAP.md b/ROADMAP.md index 1245e83a..ba1e0191 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2349,10 +2349,11 @@ been consumed. **DAN-75 agent turn status on Work terminal tabs shipped (2026-09-21):** The Workbench tab-strip and overflow-menu dots still use process lifecycle for ordinary shells. When Claude Code or Codex CLI is hosting a live PTY, OSC -titles already on the output stream overlay **agent executing** (pulsing green) -versus **turn finished / idle** (steady green) without a new IPC stack. Unknown -CLIs stay lifecycle-only; a false busy signal is treated as worse than remaining -green. +titles already on the output stream overlay **agent working** (pulsing amber) +versus **waiting for input** (steady blue) without a new IPC stack. Clean +exits are green; failed exits and errors are red. Unknown CLIs stay +lifecycle-only; a false busy signal is treated as worse than remaining green +on a plain shell. **DAN-47 light terminal contrast shipped (2026-08-24):** Embedded terminals now provide xterm's complete normal and bright ANSI palettes from Strand's diff --git a/TASKS.md b/TASKS.md index 5c90fbd7..33a058ac 100644 --- a/TASKS.md +++ b/TASKS.md @@ -1352,8 +1352,11 @@ community plugins, performance and platform certification from Git feature gaps. - ☑ Workbench terminal tab dots distinguish Claude Code / Codex CLI turn execution from turn-idle while the PTY stays `running`, using OSC 0/2 titles (and OSC 9;4 only after the CLI is recognized) already on the PTY - stream (`terminalAgentActivity`, `work-terminal-state.agent-busy`; DAN-75). - Plain shells keep dormant/running/starting/exited/error meaning. + stream (`terminalAgentActivity`, `work-terminal-state.agent-busy` / + `agent-idle`; DAN-75). Daniels palette: green done / plain running + (`--add`), blue agent waiting (`--info`), amber agent working + starting + (`--warn`, pulse only on busy), red failed exit / error (`--del`). Plain + shells stay lifecycle-only; clean exit is green, non-zero is red. - ☑ `portable-pty` native runtime and Tauri channels: direct resolved argv, recovered PATH, bounded ordered output/input/resize, per-session reader, repository cwd/open validation, Unix process groups, Windows kill-on-close diff --git a/docs/learnings.md b/docs/learnings.md index 5c231603..7f6ba98e 100644 --- a/docs/learnings.md +++ b/docs/learnings.md @@ -2252,9 +2252,13 @@ project-only remainder when waiting at a prompt) onto the existing PTY stream. Parse those sequences in the UI output handler and store `agentActivity` on the tab. Do not treat OSC 9;4 progress as an agent until the session is already recognized — package managers emit it too. False "busy" is worse than staying -lifecycle-green. Reset the stream when the PTY leaves `running` so a relaunch -of the same tab id cannot inherit a spinner. Keep the overlay off unknown CLIs -and ordinary shells. +lifecycle-green on a plain shell. Reset the stream when the PTY leaves +`running` so a relaunch of the same tab id cannot inherit a spinner. Keep the +overlay off unknown CLIs and ordinary shells. Presentation is Daniels' four +colors: green (`--add`) done / plain running, blue (`--info`, fixed hue — not +`--accent`) agent waiting, amber (`--warn`) agent working with pulse / starting, +red (`--del`) failed. Do not pulse green. Clean `exited` with code 0 is green; +non-zero is red. **Terminal defaults and explicit shell choices have different lifetimes (2026-07-20).** The primary New Terminal action follows the repository/global diff --git a/ui/src/lib/i18n.test.ts b/ui/src/lib/i18n.test.ts index 4c3ed0ba..89b38fbf 100644 --- a/ui/src/lib/i18n.test.ts +++ b/ui/src/lib/i18n.test.ts @@ -6,6 +6,10 @@ describe('i18n', () => { it('resolves and interpolates catalog messages', () => { expect(t('updates.available', { version: '1.0.0' })).toBe('Version 1.0.0 is available.'); expect(() => t('updates.available')).toThrow('Missing localization value: version'); + expect(t('work.terminalAgentBusy')).toBe('Agent working'); + expect(t('work.terminalAgentIdle')).toBe('Agent waiting for input'); + expect(t('work.terminalDone')).toBe('Terminal done'); + expect(t('work.terminalFailed')).toBe('Terminal failed'); }); it('selects the English plural form', () => { diff --git a/ui/src/lib/i18n.ts b/ui/src/lib/i18n.ts index 912e80ec..98e77d1b 100644 --- a/ui/src/lib/i18n.ts +++ b/ui/src/lib/i18n.ts @@ -189,13 +189,14 @@ export const en = { 'work.fileMissingTitle': 'File no longer exists', 'work.fileMissingBody': '{path} was removed from the working tree. This pinned tab stays open for context.', 'work.terminalRunning': 'Terminal running', - 'work.terminalAgentBusy': 'Agent executing', - 'work.terminalAgentIdle': 'Agent idle', + 'work.terminalAgentBusy': 'Agent working', + 'work.terminalAgentIdle': 'Agent waiting for input', 'work.terminalStarting': 'Terminal starting', 'work.terminalDormant': 'Terminal not started', + 'work.terminalDone': 'Terminal done', + 'work.terminalFailed': 'Terminal failed', 'work.terminalExited': 'Process exited with code {code}', 'work.terminalError': 'Terminal error: {reason}', - 'work.terminalErrorShort': 'Terminal error', 'work.terminalUnknownError': 'Unknown error', 'work.terminalRelaunchDivider': 'Relaunching terminal', 'work.relaunch': 'Relaunch', diff --git a/ui/src/lib/terminalAgentActivity.test.ts b/ui/src/lib/terminalAgentActivity.test.ts index 7f90a345..1707e190 100644 --- a/ui/src/lib/terminalAgentActivity.test.ts +++ b/ui/src/lib/terminalAgentActivity.test.ts @@ -87,15 +87,22 @@ describe('terminal agent activity from PTY OSC', () => { }); describe('terminalIndicatorClass', () => { - it('keeps lifecycle classes for plain shells and idle agents', () => { + it('keeps lifecycle classes for plain shells', () => { expect(terminalIndicatorClass('running', null)).toBe('work-terminal-state running'); - expect(terminalIndicatorClass('running', 'idle')).toBe('work-terminal-state running'); expect(terminalIndicatorClass('dormant', null)).toBe('work-terminal-state dormant'); expect(terminalIndicatorClass('starting', 'busy')).toBe('work-terminal-state starting'); expect(terminalIndicatorClass('exited', null)).toBe('work-terminal-state exited'); + expect(terminalIndicatorClass('exited', null, 0)).toBe('work-terminal-state exited'); + expect(terminalIndicatorClass('error', null)).toBe('work-terminal-state error'); }); - it('adds agent-busy only while the PTY is running', () => { + it('overlays agent-busy and agent-idle only while the PTY is running', () => { expect(terminalIndicatorClass('running', 'busy')).toBe('work-terminal-state running agent-busy'); + expect(terminalIndicatorClass('running', 'idle')).toBe('work-terminal-state running agent-idle'); + }); + + it('marks non-zero exits as failed', () => { + expect(terminalIndicatorClass('exited', null, 1)).toBe('work-terminal-state exited failed'); + expect(terminalIndicatorClass('exited', 'idle', 130)).toBe('work-terminal-state exited failed'); }); }); diff --git a/ui/src/lib/terminalAgentActivity.ts b/ui/src/lib/terminalAgentActivity.ts index e19fbcc9..41d060fe 100644 --- a/ui/src/lib/terminalAgentActivity.ts +++ b/ui/src/lib/terminalAgentActivity.ts @@ -72,9 +72,12 @@ export function consumeTerminalAgentOutput( export function terminalIndicatorClass( lifecycle: TerminalLifecycle, activity: TerminalAgentActivity | null, + exitCode: number | null = null, ): string { const busy = lifecycle === 'running' && activity === 'busy'; - return `work-terminal-state ${lifecycle}${busy ? ' agent-busy' : ''}`; + const idle = lifecycle === 'running' && activity === 'idle'; + const failed = lifecycle === 'exited' && (exitCode ?? 0) !== 0; + return `work-terminal-state ${lifecycle}${busy ? ' agent-busy' : ''}${idle ? ' agent-idle' : ''}${failed ? ' failed' : ''}`; } function applyOsc(detector: Detector, command: OscCommand): void { diff --git a/ui/src/styles/features.css b/ui/src/styles/features.css index 0a8199d8..4e19c3c6 100644 --- a/ui/src/styles/features.css +++ b/ui/src/styles/features.css @@ -7876,13 +7876,16 @@ textarea.clone-input { background: var(--text-dim); } .work-terminal-state.running { background: var(--add); } +.work-terminal-state.running.agent-idle { background: var(--info); } .work-terminal-state.starting { background: var(--warn); } -.work-terminal-state.exited, -.work-terminal-state.error { background: var(--del); } .work-terminal-state.running.agent-busy { - box-shadow: 0 0 0 2px var(--add-bg-strong); + background: var(--warn); + box-shadow: 0 0 0 2px var(--warn-bg-strong); animation: work-terminal-agent-busy 0.9s ease-in-out infinite; } +.work-terminal-state.exited { background: var(--add); } +.work-terminal-state.exited.failed { background: var(--del); } +.work-terminal-state.error { background: var(--del); } @keyframes work-terminal-agent-busy { 50% { opacity: 0.35; transform: scale(1.35); } } diff --git a/ui/src/styles/tokens.css b/ui/src/styles/tokens.css index 173e9730..010ceea1 100644 --- a/ui/src/styles/tokens.css +++ b/ui/src/styles/tokens.css @@ -102,6 +102,10 @@ warning never shows up tinted green/blue/etc. (consumers fall back to --accent when this is undefined, which was the bug). */ --warn: oklch(0.80 0.135 70); + --warn-bg-strong: oklch(0.42 0.10 70 / 0.45); + /* Informational blue — fixed hue. `--accent` follows the theme preset and + is not a stable waiting/info color. */ + --info: oklch(0.74 0.14 250); --shadow-lg: 0 24px 64px rgba(0,0,0,0.55), 0 2px 8px rgba(0,0,0,0.4); --shadow-pop: 0 12px 32px rgba(0,0,0,0.45), 0 1px 0 rgba(255,255,255,0.04) inset; @@ -167,6 +171,8 @@ --mod-bg: oklch(0.93 0.08 85 / 0.55); /* Darker amber for contrast on the cream surface. */ --warn: oklch(0.56 0.15 65); + --warn-bg-strong: oklch(0.85 0.13 65 / 0.7); + --info: oklch(0.48 0.16 250); --shadow-lg: 0 24px 64px rgba(60,40,20,0.18), 0 2px 8px rgba(60,40,20,0.08); --shadow-pop: 0 12px 32px rgba(60,40,20,0.16), 0 1px 0 rgba(255,255,255,0.5) inset; diff --git a/ui/src/views/Work.test.ts b/ui/src/views/Work.test.ts index e655acb2..cc011643 100644 --- a/ui/src/views/Work.test.ts +++ b/ui/src/views/Work.test.ts @@ -27,10 +27,17 @@ describe('Work terminal renderer visibility', () => { }); describe('Work terminal agent status indicator', () => { - it('pulses the running dot only when the agent-busy overlay is present', () => { + it('maps Daniels palette: green done, blue waiting, amber working, red failed', () => { expect(rule('.work-terminal-state.running')).toContain('var(--add)'); expect(rule('.work-terminal-state.running')).not.toContain('animation:'); + expect(rule('.work-terminal-state.exited')).toContain('var(--add)'); + expect(rule('.work-terminal-state.running.agent-idle')).toContain('var(--info)'); + expect(rule('.work-terminal-state.running.agent-idle')).not.toContain('animation:'); + expect(rule('.work-terminal-state.starting')).toContain('var(--warn)'); + expect(rule('.work-terminal-state.running.agent-busy')).toContain('var(--warn)'); expect(rule('.work-terminal-state.running.agent-busy')).toContain('work-terminal-agent-busy'); - expect(rule('.work-terminal-state.running.agent-busy')).toContain('var(--add-bg-strong)'); + expect(rule('.work-terminal-state.running.agent-busy')).toContain('var(--warn-bg-strong)'); + expect(rule('.work-terminal-state.exited.failed')).toContain('var(--del)'); + expect(rule('.work-terminal-state.error')).toContain('var(--del)'); }); }); diff --git a/ui/src/views/Work.tsx b/ui/src/views/Work.tsx index 582c80b7..e7df0813 100644 --- a/ui/src/views/Work.tsx +++ b/ui/src/views/Work.tsx @@ -812,7 +812,7 @@ function WorkTabs({ )} {tab.kind === 'file' ? leaf(tab.path) : tab.label} {tab.kind === 'terminal' && ( - + )} @@ -1292,9 +1292,9 @@ function terminalStatus(tab: WorkTerminalTab): string { case 'starting': return t('work.terminalStarting'); case 'exited': - return t('work.terminalExited', { code: tab.exitCode ?? 0 }); + return (tab.exitCode ?? 0) === 0 ? t('work.terminalDone') : t('work.terminalFailed'); case 'error': - return t('work.terminalErrorShort'); + return t('work.terminalFailed'); case 'dormant': return t('work.terminalDormant'); default: { diff --git a/website/docs/work.md b/website/docs/work.md index 80a236fb..744fd171 100644 --- a/website/docs/work.md +++ b/website/docs/work.md @@ -117,15 +117,17 @@ complete dashboard even after setup tips and release notes have already been seen. This does not change Strand's repository-root working directory and the hints are ignored by other CLI agents. -While the shell is running, the tab-strip and open-tabs menu dots stay gray -(dormant), green (process alive), amber (starting), or red (exited/error). When -the terminal is hosting Claude Code or Codex CLI, the green running dot also -shows agent turn state: a pulsing ring while the agent is executing, and a -steady green once the turn finishes and the CLI is waiting for input. Screen -readers hear **Agent executing** or **Agent idle** instead of **Terminal -running**. Other shells and unknown CLIs keep the process-lifecycle meaning; -Strand only overlays agent state from OSC titles those CLIs already emit, and -prefers staying green over a false busy signal. +Tab-strip and open-tabs menu dots use a four-color status: gray (dormant / +inactive), **green** (plain shell running, or a clean exit), **blue** (recognized +agent waiting for input), **amber** (agent working, or the process is starting), +and **red** (error or a non-zero exit). While Claude Code or Codex CLI is +hosting a live PTY, the running dot pulses amber mid-turn and turns steady blue +once the CLI is waiting at a prompt. Screen readers hear **Agent working** or +**Agent waiting for input** instead of **Terminal running**; a finished process +is **Terminal done** or **Terminal failed**. Other shells and unknown CLIs keep +the process-lifecycle meaning; Strand only overlays agent state from OSC titles +those CLIs already emit, and prefers staying on a plain-shell color over a +false busy signal. Terminal descriptors restore after restarting Strand, but processes never do. Restored tabs are unselected and dormant; explicitly selecting one starts a