feat(cli): add tool execution progress messages#3155
Conversation
📋 Review SummaryThis PR introduces three related UX improvements for tool execution visibility: (1) elapsed time display next to the spinner after 3 seconds of execution, (2) a shell output statistics bar showing line count, byte size, and timeout, and (3) a terminal tab progress bar using OSC 9;4 sequences. The implementation is well-structured with good separation of concerns, but there are some concerns around memory tracking accuracy, terminal detection reliability, and test coverage that should be addressed before merging. 🔍 General Feedback
🎯 Specific Feedback🔴 Critical
🟡 High
🟢 Medium
🔵 Low
✅ Highlights
|
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
wenshao
left a comment
There was a problem hiding this comment.
[Critical] totalLines and totalBytes use = (replace) instead of += (accumulate) in packages/core/src/tools/shell.ts:284. Each data event overwrites the previous totals, so the stats bar will only reflect the most recent chunk, not cumulative output.
totalLines += event.chunk.split('\n').length;
totalBytes += Buffer.byteLength(event.chunk, 'utf-8');
— qwen3.6-plus via Qwen Code /review
wenshao
left a comment
There was a problem hiding this comment.
[Suggestion] packages/core/src/tools/shell.ts:292 — AnsiOutput branch uses token.text.length which counts UTF-16 code units, not bytes. For non-ASCII output (CJK, emojis), this will undercount.
line.reduce((ls, token) => ls + Buffer.byteLength(token.text, 'utf-8'), 0),
— qwen3.6-plus via Qwen Code /review
… time, shell stats, and terminal progress bar - Show per-tool elapsed time (Ns) next to spinner after 3 seconds of execution, covering all tools (not just shell), by piping existing core startTime through to the UI layer via IndividualToolCallDisplay.executionStartTime - Add shell output statistics bar below ANSI output showing +N lines overflow count, byte size, and explicit timeout when set by user - Add terminal tab progress bar via OSC 9;4 sequences for iTerm2, Ghostty, and ConEmu, with tmux/screen DCS passthrough support - Extend AnsiOutputDisplay with optional totalLines/totalBytes/timeoutMs fields - Add ShellStatsBar component for rendering shell output statistics
…ass displayHeight to ShellStatsBar - Use existing formatDuration() from formatters.ts instead of inline timeout formatting for correct precision (e.g., "2m 3s" not "2m") - Add displayHeight prop to ShellStatsBar so +N lines overflow calculation respects actual terminal height, not hardcoded DEFAULT_HEIGHT
Check process.stdout.isTTY in isProgressBarSupported() so escape sequences are not emitted when stdout is piped, redirected to log files, or running in CI environments where TERM_PROGRAM may be set but stdout is not a TTY. Also add defensive isProgressBarSupported() guard in the effect cleanup.
…g tools Previously showed raw seconds (e.g. "3600s") for long-running tools. Now formats as "3s" for under a minute, "1m 30s" for minutes, and "2h 15m" for hours, while keeping compact integer seconds for short durations.
…rogress # Conflicts: # packages/cli/src/ui/types.ts
492a8b3 to
372311c
Compare
Three issues found by post-merge audit:
- useTerminalProgress: WT_SESSION was wrongly used to exclude Windows
Terminal. WT 1.6+ actually supports OSC 9;4 progress sequences (per
Microsoft docs), so treat it as a positive indicator like iTerm2 and
Ghostty.
- useTerminalProgress: add process.on('exit'|'SIGINT'|'SIGTERM') handler
that writes PROGRESS_CLEAR. Without it, killing the CLI mid-tool (Ctrl+C,
SIGTERM) left the terminal tab stuck showing an indeterminate progress
indicator because React cleanup never ran. Mirrors the useBracketedPaste
cleanup pattern.
- shell.ts: ANSI totalBytes used token.text.length (character count),
inconsistent with the string path's Buffer.byteLength(..., 'utf-8').
Multi-byte chars (CJK, emoji) now count as their true UTF-8 byte length
in both paths.
…onent Move the executing-tool elapsed-seconds indicator out of ToolStatusIndicator (where it sat immediately after the spinner on the left edge) and into a new right-aligned ToolElapsedTime component. The left placement caused layout jitter: every second the elapsed text width would change (e.g. "9s" → "10s" → "1m" → "1m 15s"), shifting the tool name and description horizontally. Right-aligning the elapsed keeps the tool name anchored and only the far-right timer moves. - New packages/cli/src/ui/components/shared/ToolElapsedTime.tsx owns the setInterval + formatElapsed logic. - ToolStatusIndicator is now pure status again; the executionStartTime prop is gone from it. - ToolMessage and CompactToolGroupDisplay mount ToolElapsedTime as the last flex child of the status row, with marginLeft=1. - ToolInfo gains flexGrow=1 so the description fills the middle and the timer sits flush at the right edge of the row.
…rogress # Conflicts: # packages/cli/src/ui/components/AnsiOutput.tsx # packages/cli/src/ui/components/messages/ToolMessage.tsx
…ting-entry trackedCall.startTime is stamped when a tool is first registered with the scheduler (validating state), then preserved through awaiting_approval, scheduled, and executing transitions. Using it for the executing-row elapsed display meant any approval-wait time was counted as execution time — a tool that waited 30s for user approval would flash "30s" immediately when it actually began running. Add a separate executionStartTime on ExecutingToolCall, stamped at the moment of the transition into 'executing', and pipe that through useReactToolScheduler into IndividualToolCallDisplay.executionStartTime. startTime is kept as-is for durationMs bookkeeping. Also stops piping executionStartTime for validating/scheduled states, since those don't have a meaningful execution duration yet.
…SIGTERM Registering SIGINT/SIGTERM handlers that neither re-raise nor exit inhibits Node's default termination behavior. If this hook were ever the only signal handler in play, Ctrl+C would leave the process hanging. Drop the signal handlers and rely on 'exit' alone. Other parts of the CLI already own the signal-to-shutdown path (gemini.tsx, telemetry shutdown, sharedTokenManager, etc.) and ultimately call process.exit(), which fires 'exit' and runs this cleanup. SIGKILL cannot be cleaned up either way.
wenshao
left a comment
There was a problem hiding this comment.
[Suggestion] packages/cli/src/ui/components/agent-view/agentHistoryAdapter.ts:149-181 still drops executionStartTime when constructing IndividualToolCallDisplay items. The main scheduler/UI path now propagates this field, so agent-view tool groups can miss the new elapsed-time behavior and render inconsistently across views. Consider threading executionStartTime through agentMessagesToHistoryItems(...) for executing tools as well.
— gpt-5.4 via Qwen Code /review
The main TUI renders per-tool elapsed time via IndividualToolCallDisplay. executionStartTime, but the agent-view adapter (agentHistoryAdapter.ts) constructed its display items without this field, so sub-agent tool groups never showed the elapsed indicator. Thread it through the sub-agent event pipeline: - AgentToolOutputUpdateEvent gains an optional executionStartTime, emitted once per callId by agent-core.onToolCallsUpdate the first time a call is seen in the scheduler's 'executing' state (carrying ExecutingToolCall.executionStartTime). This also fires for tools that produce no live output, so their elapsed indicator appears too. - AgentInteractive tracks executionStartTimes in a callId→timestamp map, analogous to liveOutputs/shellPids. First TOOL_OUTPUT_UPDATE with a value wins; later events that re-carry it are ignored. Cleared on TOOL_RESULT. - AgentChatView passes the map as the new fifth argument to agentMessagesToHistoryItems. - The adapter reads the map for Executing tools and sets IndividualToolCallDisplay.executionStartTime, matching the main-view plumbing. Agent-view tool_groups now render the same elapsed-time indicator the main view does. Adds three test cases covering set-when-executing, skip-when-completed, and skip-when-map-absent.
wenshao
left a comment
There was a problem hiding this comment.
Good catch. Fixed in ebd9bdf — threaded executionStartTime through the sub-agent event pipeline so agentHistoryAdapter.ts can attach it when constructing IndividualToolCallDisplay items.
Concretely:
AgentToolOutputUpdateEventgained an optionalexecutionStartTime.agent-core.onToolCallsUpdatefires it once per callId the first time a call is seen inexecuting, carryingExecutingToolCall.executionStartTime. This also fires for tools that produce no live output (not just PID-producing ones), so their elapsed indicator works too.AgentInteractivetracks these in acallId → timestampmap analogous toliveOutputs/shellPids; first-value-wins so the timer is stable; cleared onTOOL_RESULT.AgentChatViewpasses the map as the fifth argument toagentMessagesToHistoryItems.- Adapter reads it for
Executingtools and setsIndividualToolCallDisplay.executionStartTime.
Main-view and agent-view tool groups now render the same elapsed-time indicator. Added three adapter tests (set-when-executing, skip-when-completed, skip-when-map-absent).
wenshao
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅ — gpt-5.4 via Qwen Code /review
totalLines/totalBytes are only emitted alongside AnsiOutputDisplay in
the ANSI-array branch of updateOutput. Computing split('\n') and
Buffer.byteLength for string chunks was wasted work — the values never
left the function.
Only compute stats when event.chunk is an AnsiLine[] now.
* feat(cli): add tool execution progress messages with per-tool elapsed time, shell stats, and terminal progress bar
- Show per-tool elapsed time (Ns) next to spinner after 3 seconds of execution,
covering all tools (not just shell), by piping existing core startTime through
to the UI layer via IndividualToolCallDisplay.executionStartTime
- Add shell output statistics bar below ANSI output showing +N lines overflow
count, byte size, and explicit timeout when set by user
- Add terminal tab progress bar via OSC 9;4 sequences for iTerm2, Ghostty, and
ConEmu, with tmux/screen DCS passthrough support
- Extend AnsiOutputDisplay with optional totalLines/totalBytes/timeoutMs fields
- Add ShellStatsBar component for rendering shell output statistics
* fix(cli): address review feedback — use formatDuration for timeout, pass displayHeight to ShellStatsBar
- Use existing formatDuration() from formatters.ts instead of inline
timeout formatting for correct precision (e.g., "2m 3s" not "2m")
- Add displayHeight prop to ShellStatsBar so +N lines overflow
calculation respects actual terminal height, not hardcoded DEFAULT_HEIGHT
* fix(cli): guard terminal progress bar against non-TTY stdout
Check process.stdout.isTTY in isProgressBarSupported() so escape sequences
are not emitted when stdout is piped, redirected to log files, or running
in CI environments where TERM_PROGRAM may be set but stdout is not a TTY.
Also add defensive isProgressBarSupported() guard in the effect cleanup.
* fix(cli): format tool elapsed time with minutes/hours for long-running tools
Previously showed raw seconds (e.g. "3600s") for long-running tools.
Now formats as "3s" for under a minute, "1m 30s" for minutes, and
"2h 15m" for hours, while keeping compact integer seconds for short
durations.
* fix(cli): audit fixes for terminal progress and shell output stats
Three issues found by post-merge audit:
- useTerminalProgress: WT_SESSION was wrongly used to exclude Windows
Terminal. WT 1.6+ actually supports OSC 9;4 progress sequences (per
Microsoft docs), so treat it as a positive indicator like iTerm2 and
Ghostty.
- useTerminalProgress: add process.on('exit'|'SIGINT'|'SIGTERM') handler
that writes PROGRESS_CLEAR. Without it, killing the CLI mid-tool (Ctrl+C,
SIGTERM) left the terminal tab stuck showing an indeterminate progress
indicator because React cleanup never ran. Mirrors the useBracketedPaste
cleanup pattern.
- shell.ts: ANSI totalBytes used token.text.length (character count),
inconsistent with the string path's Buffer.byteLength(..., 'utf-8').
Multi-byte chars (CJK, emoji) now count as their true UTF-8 byte length
in both paths.
* refactor(cli): right-align tool elapsed time, extract to its own component
Move the executing-tool elapsed-seconds indicator out of
ToolStatusIndicator (where it sat immediately after the spinner on the
left edge) and into a new right-aligned ToolElapsedTime component.
The left placement caused layout jitter: every second the elapsed text
width would change (e.g. "9s" → "10s" → "1m" → "1m 15s"), shifting the
tool name and description horizontally. Right-aligning the elapsed keeps
the tool name anchored and only the far-right timer moves.
- New packages/cli/src/ui/components/shared/ToolElapsedTime.tsx owns the
setInterval + formatElapsed logic.
- ToolStatusIndicator is now pure status again; the executionStartTime
prop is gone from it.
- ToolMessage and CompactToolGroupDisplay mount ToolElapsedTime as the
last flex child of the status row, with marginLeft=1.
- ToolInfo gains flexGrow=1 so the description fills the middle and the
timer sits flush at the right edge of the row.
* fix(core): measure tool elapsed from executing-transition, not validating-entry
trackedCall.startTime is stamped when a tool is first registered with the
scheduler (validating state), then preserved through awaiting_approval,
scheduled, and executing transitions. Using it for the executing-row
elapsed display meant any approval-wait time was counted as execution
time — a tool that waited 30s for user approval would flash "30s"
immediately when it actually began running.
Add a separate executionStartTime on ExecutingToolCall, stamped at the
moment of the transition into 'executing', and pipe that through
useReactToolScheduler into IndividualToolCallDisplay.executionStartTime.
startTime is kept as-is for durationMs bookkeeping.
Also stops piping executionStartTime for validating/scheduled states,
since those don't have a meaningful execution duration yet.
* fix(cli): only hook 'exit' for terminal progress cleanup, not SIGINT/SIGTERM
Registering SIGINT/SIGTERM handlers that neither re-raise nor exit
inhibits Node's default termination behavior. If this hook were ever the
only signal handler in play, Ctrl+C would leave the process hanging.
Drop the signal handlers and rely on 'exit' alone. Other parts of the
CLI already own the signal-to-shutdown path (gemini.tsx, telemetry
shutdown, sharedTokenManager, etc.) and ultimately call process.exit(),
which fires 'exit' and runs this cleanup. SIGKILL cannot be cleaned up
either way.
* fix(cli): thread executionStartTime through agent-view tool groups
The main TUI renders per-tool elapsed time via IndividualToolCallDisplay.
executionStartTime, but the agent-view adapter
(agentHistoryAdapter.ts) constructed its display items without this
field, so sub-agent tool groups never showed the elapsed indicator.
Thread it through the sub-agent event pipeline:
- AgentToolOutputUpdateEvent gains an optional executionStartTime,
emitted once per callId by agent-core.onToolCallsUpdate the first time
a call is seen in the scheduler's 'executing' state (carrying
ExecutingToolCall.executionStartTime). This also fires for tools that
produce no live output, so their elapsed indicator appears too.
- AgentInteractive tracks executionStartTimes in a callId→timestamp map,
analogous to liveOutputs/shellPids. First TOOL_OUTPUT_UPDATE with a
value wins; later events that re-carry it are ignored. Cleared on
TOOL_RESULT.
- AgentChatView passes the map as the new fifth argument to
agentMessagesToHistoryItems.
- The adapter reads the map for Executing tools and sets
IndividualToolCallDisplay.executionStartTime, matching the main-view
plumbing. Agent-view tool_groups now render the same elapsed-time
indicator the main view does.
Adds three test cases covering set-when-executing, skip-when-completed,
and skip-when-map-absent.
* fix(core): skip stats accounting for string shell chunks
totalLines/totalBytes are only emitted alongside AnsiOutputDisplay in
the ANSI-array branch of updateOutput. Computing split('\n') and
Buffer.byteLength for string chunks was wasted work — the values never
left the function.
Only compute stats when event.chunk is an AnsiLine[] now.
…LM#3508) (#216) Long-running shell tool calls (`npm install`, `find /`, build logs) were rendering the full visible PTY buffer inline (~24 lines on a typical terminal). The output dominated the viewport and pushed prior context off the top. This caps inline shell output to a small window (default 5 lines, matching Claude Code's `ShellProgressMessage`). The hidden line count is surfaced via the `+N lines` ShellStatsBar indicator (ANSI streaming) or the existing MaxSizedBox overflow indicator (completed string). The focused embedded shell bypasses the cap so the user sees the full live transcript while interacting with it. Adapted from QwenLM/qwen-code QwenLM#3508. Adaptations: - Pulled ShellStatsBar into our AnsiOutput.tsx ourselves (the upstream component lived in a QwenLM#3155 prerequisite that we're not porting in full — QwenLM#3155 also brings per-tool elapsed time and OSC 9;4 tab progress bar, which Alacritty doesn't support and we don't want enough to justify the surface). - Slimmed ShellStatsBar to just the `+N lines` count; upstream's `totalBytes` (UTF-8 byte size) display is part of the broader QwenLM#3155 scope. - Compute totalLines from `displayRenderer.data.length` directly rather than via upstream's `effectiveDisplayRenderer.stats` channel (also a QwenLM#3155 thing). - Skipped upstream's `forceShowResult` bypass (no consumer in our fork) — the focused-embedded-shell bypass is enough. New setting: `ui.shellOutputMaxLines` (number, default 5). Set to 0 to disable the cap entirely. Co-authored-by: Automaker <automaker@localhost> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cli): add tool execution progress messages with per-tool elapsed time, shell stats, and terminal progress bar
- Show per-tool elapsed time (Ns) next to spinner after 3 seconds of execution,
covering all tools (not just shell), by piping existing core startTime through
to the UI layer via IndividualToolCallDisplay.executionStartTime
- Add shell output statistics bar below ANSI output showing +N lines overflow
count, byte size, and explicit timeout when set by user
- Add terminal tab progress bar via OSC 9;4 sequences for iTerm2, Ghostty, and
ConEmu, with tmux/screen DCS passthrough support
- Extend AnsiOutputDisplay with optional totalLines/totalBytes/timeoutMs fields
- Add ShellStatsBar component for rendering shell output statistics
* fix(cli): address review feedback — use formatDuration for timeout, pass displayHeight to ShellStatsBar
- Use existing formatDuration() from formatters.ts instead of inline
timeout formatting for correct precision (e.g., "2m 3s" not "2m")
- Add displayHeight prop to ShellStatsBar so +N lines overflow
calculation respects actual terminal height, not hardcoded DEFAULT_HEIGHT
* fix(cli): guard terminal progress bar against non-TTY stdout
Check process.stdout.isTTY in isProgressBarSupported() so escape sequences
are not emitted when stdout is piped, redirected to log files, or running
in CI environments where TERM_PROGRAM may be set but stdout is not a TTY.
Also add defensive isProgressBarSupported() guard in the effect cleanup.
* fix(cli): format tool elapsed time with minutes/hours for long-running tools
Previously showed raw seconds (e.g. "3600s") for long-running tools.
Now formats as "3s" for under a minute, "1m 30s" for minutes, and
"2h 15m" for hours, while keeping compact integer seconds for short
durations.
* fix(cli): audit fixes for terminal progress and shell output stats
Three issues found by post-merge audit:
- useTerminalProgress: WT_SESSION was wrongly used to exclude Windows
Terminal. WT 1.6+ actually supports OSC 9;4 progress sequences (per
Microsoft docs), so treat it as a positive indicator like iTerm2 and
Ghostty.
- useTerminalProgress: add process.on('exit'|'SIGINT'|'SIGTERM') handler
that writes PROGRESS_CLEAR. Without it, killing the CLI mid-tool (Ctrl+C,
SIGTERM) left the terminal tab stuck showing an indeterminate progress
indicator because React cleanup never ran. Mirrors the useBracketedPaste
cleanup pattern.
- shell.ts: ANSI totalBytes used token.text.length (character count),
inconsistent with the string path's Buffer.byteLength(..., 'utf-8').
Multi-byte chars (CJK, emoji) now count as their true UTF-8 byte length
in both paths.
* refactor(cli): right-align tool elapsed time, extract to its own component
Move the executing-tool elapsed-seconds indicator out of
ToolStatusIndicator (where it sat immediately after the spinner on the
left edge) and into a new right-aligned ToolElapsedTime component.
The left placement caused layout jitter: every second the elapsed text
width would change (e.g. "9s" → "10s" → "1m" → "1m 15s"), shifting the
tool name and description horizontally. Right-aligning the elapsed keeps
the tool name anchored and only the far-right timer moves.
- New packages/cli/src/ui/components/shared/ToolElapsedTime.tsx owns the
setInterval + formatElapsed logic.
- ToolStatusIndicator is now pure status again; the executionStartTime
prop is gone from it.
- ToolMessage and CompactToolGroupDisplay mount ToolElapsedTime as the
last flex child of the status row, with marginLeft=1.
- ToolInfo gains flexGrow=1 so the description fills the middle and the
timer sits flush at the right edge of the row.
* fix(core): measure tool elapsed from executing-transition, not validating-entry
trackedCall.startTime is stamped when a tool is first registered with the
scheduler (validating state), then preserved through awaiting_approval,
scheduled, and executing transitions. Using it for the executing-row
elapsed display meant any approval-wait time was counted as execution
time — a tool that waited 30s for user approval would flash "30s"
immediately when it actually began running.
Add a separate executionStartTime on ExecutingToolCall, stamped at the
moment of the transition into 'executing', and pipe that through
useReactToolScheduler into IndividualToolCallDisplay.executionStartTime.
startTime is kept as-is for durationMs bookkeeping.
Also stops piping executionStartTime for validating/scheduled states,
since those don't have a meaningful execution duration yet.
* fix(cli): only hook 'exit' for terminal progress cleanup, not SIGINT/SIGTERM
Registering SIGINT/SIGTERM handlers that neither re-raise nor exit
inhibits Node's default termination behavior. If this hook were ever the
only signal handler in play, Ctrl+C would leave the process hanging.
Drop the signal handlers and rely on 'exit' alone. Other parts of the
CLI already own the signal-to-shutdown path (gemini.tsx, telemetry
shutdown, sharedTokenManager, etc.) and ultimately call process.exit(),
which fires 'exit' and runs this cleanup. SIGKILL cannot be cleaned up
either way.
* fix(cli): thread executionStartTime through agent-view tool groups
The main TUI renders per-tool elapsed time via IndividualToolCallDisplay.
executionStartTime, but the agent-view adapter
(agentHistoryAdapter.ts) constructed its display items without this
field, so sub-agent tool groups never showed the elapsed indicator.
Thread it through the sub-agent event pipeline:
- AgentToolOutputUpdateEvent gains an optional executionStartTime,
emitted once per callId by agent-core.onToolCallsUpdate the first time
a call is seen in the scheduler's 'executing' state (carrying
ExecutingToolCall.executionStartTime). This also fires for tools that
produce no live output, so their elapsed indicator appears too.
- AgentInteractive tracks executionStartTimes in a callId→timestamp map,
analogous to liveOutputs/shellPids. First TOOL_OUTPUT_UPDATE with a
value wins; later events that re-carry it are ignored. Cleared on
TOOL_RESULT.
- AgentChatView passes the map as the new fifth argument to
agentMessagesToHistoryItems.
- The adapter reads the map for Executing tools and sets
IndividualToolCallDisplay.executionStartTime, matching the main-view
plumbing. Agent-view tool_groups now render the same elapsed-time
indicator the main view does.
Adds three test cases covering set-when-executing, skip-when-completed,
and skip-when-map-absent.
* fix(core): skip stats accounting for string shell chunks
totalLines/totalBytes are only emitted alongside AnsiOutputDisplay in
the ANSI-array branch of updateOutput. Computing split('\n') and
Buffer.byteLength for string chunks was wasted work — the values never
left the function.
Only compute stats when event.chunk is an AnsiLine[] now.
Summary
Problem: When tools (e.g.
npm install) execute for a long time, users see only a generic spinner with no indication of progress or elapsed time — leading to premature Ctrl+C interruptions.Solution: three improvements to tool-execution visibility:
3s,1m 30s,2h 15m) right-aligned on the tool row, for all tools (not just shell)+N lines, UTF-8 byte count, and explicit timeout below streamed ANSI outputBefore / After
The elapsed timer is right-aligned so it does not shift the tool name/description as its width changes (
9s→10s→1m→1m 15s).Changed files
packages/core/src/tools/tools.tsAnsiOutputDisplaywithtotalLines/totalBytes/timeoutMspackages/core/src/tools/shell.tspackages/cli/src/ui/types.tsexecutionStartTimetoIndividualToolCallDisplaypackages/cli/src/ui/hooks/useReactToolScheduler.tsstartTimethroughmapToDisplaypackages/cli/src/ui/components/shared/ToolElapsedTime.tsxpackages/cli/src/ui/components/AnsiOutput.tsxShellStatsBarcomponentpackages/cli/src/ui/components/messages/ToolMessage.tsxToolInfogrows to flush timer to the right edgepackages/cli/src/ui/components/messages/CompactToolGroupDisplay.tsxToolElapsedTimein compact modepackages/cli/src/ui/hooks/useTerminalProgress.tspackages/cli/src/ui/AppContainer.tsxOut of scope
Test plan
Automated
ToolMessage.test.tsxmock forShellStatsBaruseToolScheduler.test.tswithexecutionStartTimeassertionsManual
Setup
1. Per-tool elapsed time
Prompt:
Expected: after 3 seconds, the bash tool row shows
3s→4s→ ... →8sright-aligned.Minute-scale formatting:
Expected:
59s→1m→1m 1s→ ... →1m 15s.Non-shell tools: any tool that takes >3s (slow
grep, a long subagent run) should also show the indicator.2. Shell output stats bar
Overflow (
+N lines):Expected: gray stats row under the output, e.g.
+176 linesfor a ~24-line terminal.Byte count:
Expected: stats bar shows
... KiBor... MiB.Timeout display (requires the model to pass
timeoutto the tool):Expected: while executing, stats bar shows
timeout 3s.CJK byte counting (regression test for the UTF-8 fix):
Expected: the displayed byte count is ≈ 3× the character count. Before the fix the ANSI path counted characters rather than bytes, so CJK/emoji undercounted.
3. Terminal tab progress bar
Requires one of: iTerm2 3.6.6+, Ghostty 1.2+, ConEmu, or Windows Terminal 1.6+. Nothing is emitted in GNOME Terminal / Alacritty / generic xterm / non-TTY / CI.
Expected: the terminal tab shows an indeterminate progress ring while the tool runs; it clears immediately when the tool finishes.
Abrupt-exit cleanup (regression test for the SIGINT fix):
While the tool is still running, press
Ctrl+Cto kill the CLI. Expected: the tab indicator clears immediately — before the fix it could stay stuck.tmux passthrough:
tmux npm start # trigger any long toolExpected: the outer terminal's tab indicator still animates (DCS wrapping works).
4. Quick-tool smoke
Expected: no elapsed indicator appears (tool finishes well under the 3s threshold).
tool-execution-progress.mp4