diff --git a/README.md b/README.md
index fe00a90..59713f4 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 1245e83..ba1e019 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 5c90fbd..33a058a 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 5c23160..7f6ba98 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 4c3ed0b..89b38fb 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 912e80e..98e77d1 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 7f90a34..1707e19 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 e19fbcc..41d060f 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 0a8199d..4e19c3c 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 173e973..010ceea 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 e655acb..cc01164 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 582c80b..e7df081 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 80a236f..744fd17 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