Render a semantic activity state in the status ticker, not raw tool names - #419
Merged
TheGreatAxios merged 3 commits intoAug 8, 2026
Conversation
The ticker currently renders whatever raw tool identifier is executing. This test pins the fix: the rendered label must always be a member of a small closed set of activity states, never a tool, MCP server, or plugin name, and stalled/waiting-on-operator must render distinctly.
…ames The ticker rendered whatever tool identifier was currently executing — internal plumbing vocabulary leaking into a product surface, and redundant with the transcript, which already shows the tool call. Replace it with a closed set of human activity states (thinking, planning, researching, building, working, waiting, stalled, stopping). The execution-to-state mapping lives in one place with an explicit fallback to 'working', so an unmapped tool, MCP server, or plugin name can never reach the ticker and adding a tool needs no ticker change. Stalled reuses the existing stall-watchdog signal rather than a second notion of stuck, and waiting on operator approval is now its own state distinct from active work.
- AppShell.lockupPhase and LockupFrame.phase in shell.ts are now typed ActivityState | null instead of string | null, so a raw tool identifier reaching the ticker is a type error at the setLockupFrame boundary, not only a test failure. lockup.ts's LockupInput.phase stays a generic string deliberately — its own tests exercise arbitrary CJK/astral text to check width math unrelated to the activity vocabulary, and the leak boundary is already closed one layer up. - isStalled on resolveTurnLabel is now required, matching resolveRampPhase; a caller that forgets it is the exact bug this state exists to prevent. - runtime-bridge.ts's second stall check site now calls isStalledForDisplay instead of re-deriving 'not quiet' from stallLevel's result, so the two call sites share one definition of stalled. - Dropped unread TurnLabelInput.awaitingResponse. - docs/TUI.md corrected: it described the phase slot as showing 'the running tool's name', which this change makes false; it now points at ACTIVITY_STATES as the source of truth. - Added delete_file/advance_workflow/tool_search/search_agents to the tool-to-state table. - Test fallback case swapped from the fictional 'bash' tool to a real MCP identifier, since 'bash' cannot occur at runtime.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
run_shell,grep, MCP names, ...), leaking plumbing vocabulary into a product surface and duplicating what the transcript already shows.resolveTurnLabelinsrc/tui-opentui/session-chrome.tsnow returns a member of a small closed set —thinking,planning,researching,building,working,waiting,stalled,stopping— via a single tool-to-state table (TOOL_ACTIVITY_STATES) with an explicitworkingfallback for anything unmapped (built-in, MCP, or plugin). Consumed at the actual leak site insidepaintPhaseAtinruntime-bridge.ts.isStalledForDisplaysignal fromstall-watchdog.ts— no second notion of stuck. Blocked/waiting still derives from the existing gate signal (blockedGateCount/status === "blocked"); only the rendered literal changed fromblockedtowaitingso it reads distinctly fromworking.AppShell.lockupPhaseandLockupFrame.phaseinshell.tsare typedActivityState | null, so a raw identifier reachingsetLockupFrameis a type error.lockup.ts'sLockupInput.phasedeliberately stays a genericstring— its own tests feed it arbitrary CJK/astral strings to check column-width math, unrelated to the activity vocabulary, and the leak boundary is already closed one layer up at thesetLockupFramecall site.Scope decisions (recorded per review)
thinking,working, etc.) is lowercase and unpunctuated by design (seeresolveTurnLabel's doc comment), and a single capitalized word would read as a mistake against the rest of the chrome. Kept lowercase.agent-progress.ts's${elapsed} · ${tool}is intentionally out of scope. That string feeds the sub-agent panel rows, not the prompt-box lockup slot this ticket targets. Per the operator: raw tool names on sub-agent rows are desired — that panel is where an operator watches what a delegated agent is actually doing (grep, etc.), and collapsing it to a generic activity word would remove information they want there. The lockup slot answers "is it alive"; the sub-agent rows answer "what is it doing." Left unchanged. No follow-up ticket needed per the operator's ruling.src/tui/tool-formatter.tsalready holds two tables keyed on the same tool-name space (TOOL_DISPLAY_NAMESfor transcript display, plus the MCP humanizer).TOOL_ACTIVITY_STATESinsession-chrome.tsis a third, deliberately different taxonomy (activity category vs. display name) over the same key space — they'll want reconciling when the old TUI goes away, not now.streaming N tokwas deliberately removed from the label, not collateral damage — the closed-set guarantee requires the rendered string to always be a fixed member ofACTIVITY_STATES, which a live token count can't be. The ramp's own cell animation still carries "is it alive" during text streaming (state renders asworking).Test plan
bun run test— 4212 pass, 1 pre-existing unrelated failure (src/agent/lsp-availability.test.ts, environment-dependent language-server detection, untouched by this change)bun run typecheck— clean on all touched files; pre-existing vendor@intx/typesversion-drift errors remain insrc/subagent/run.ts/src/tui/runner.ts, neither touched herebun run buildtranscript-long-log-scroll.test.tsfailure investigated: reproduced clean-main (19d9710) and this branch locally with the exact CI invocation (bun test ./src ./tests ./evals --randomize --seed 424242) plus isolated repeats of that single file on both. It passed every time on both revisions; the diff does not touchlong-log.ts,shell.ts's scroll path, orharness.tsat all. Timing-sensitive/flaky, not caused by this change.