Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions docs/learnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions ui/src/lib/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
7 changes: 4 additions & 3 deletions ui/src/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
13 changes: 10 additions & 3 deletions ui/src/lib/terminalAgentActivity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
5 changes: 4 additions & 1 deletion ui/src/lib/terminalAgentActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 6 additions & 3 deletions ui/src/styles/features.css
Original file line number Diff line number Diff line change
Expand Up @@ -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); }
}
Expand Down
6 changes: 6 additions & 0 deletions ui/src/styles/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 9 additions & 2 deletions ui/src/views/Work.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)');
});
});
8 changes: 4 additions & 4 deletions ui/src/views/Work.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,7 @@ function WorkTabs({
)}
<span>{tab.kind === 'file' ? leaf(tab.path) : tab.label}</span>
{tab.kind === 'terminal' && (
<span className={terminalIndicatorClass(tab.lifecycle, tab.agentActivity)} aria-label={terminalStatus(tab)} />
<span className={terminalIndicatorClass(tab.lifecycle, tab.agentActivity, tab.exitCode)} aria-label={terminalStatus(tab)} />
)}
</button>
<button
Expand Down Expand Up @@ -1031,7 +1031,7 @@ function WorkTabSelector({
</span>
<span className="label">{label}</span>
{tab.kind === 'terminal' && (
<span className={terminalIndicatorClass(tab.lifecycle, tab.agentActivity)} aria-label={terminalStatus(tab)} />
<span className={terminalIndicatorClass(tab.lifecycle, tab.agentActivity, tab.exitCode)} aria-label={terminalStatus(tab)} />
)}
{active && <span className="meta"><Icon name="check" size={12} stroke={2.2} /></span>}
</button>
Expand Down Expand Up @@ -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: {
Expand Down
20 changes: 11 additions & 9 deletions website/docs/work.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading