Skip to content

Render a semantic activity state in the status ticker, not raw tool names - #419

Merged
TheGreatAxios merged 3 commits into
mainfrom
cl-5732-show-a-semantic-activity-state-in-the-status-ticker-instead
Aug 8, 2026
Merged

Render a semantic activity state in the status ticker, not raw tool names#419
TheGreatAxios merged 3 commits into
mainfrom
cl-5732-show-a-semantic-activity-state-in-the-status-ticker-instead

Conversation

@TheGreatAxios

@TheGreatAxios TheGreatAxios commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • CL-5732: the bottom-left status ticker rendered whatever raw tool identifier was currently executing (run_shell, grep, MCP names, ...), leaking plumbing vocabulary into a product surface and duplicating what the transcript already shows.
  • resolveTurnLabel in src/tui-opentui/session-chrome.ts now 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 explicit working fallback for anything unmapped (built-in, MCP, or plugin). Consumed at the actual leak site inside paintPhaseAt in runtime-bridge.ts.
  • Stalled reuses the existing isStalledForDisplay signal from stall-watchdog.ts — no second notion of stuck. Blocked/waiting still derives from the existing gate signal (blockedGateCount / status === "blocked"); only the rendered literal changed from blocked to waiting so it reads distinctly from working.
  • The closed set is now compiler-enforced, not just test-enforced: AppShell.lockupPhase and LockupFrame.phase in shell.ts are typed ActivityState | null, so a raw identifier reaching setLockupFrame is a type error. lockup.ts's LockupInput.phase deliberately stays a generic string — 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 the setLockupFrame call site.

Scope decisions (recorded per review)

  • Rendering case: lowercase, unchanged. The operator's ask was for human words over raw identifiers; the ticker's existing house style (thinking, working, etc.) is lowercase and unpunctuated by design (see resolveTurnLabel'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.ts already holds two tables keyed on the same tool-name space (TOOL_DISPLAY_NAMES for transcript display, plus the MCP humanizer). TOOL_ACTIVITY_STATES in session-chrome.ts is 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 tok was deliberately removed from the label, not collateral damage — the closed-set guarantee requires the rendered string to always be a fixed member of ACTIVITY_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 as working).

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/types version-drift errors remain in src/subagent/run.ts / src/tui/runner.ts, neither touched here
  • bun run build
  • CI's transcript-long-log-scroll.test.ts failure 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 touch long-log.ts, shell.ts's scroll path, or harness.ts at all. Timing-sensitive/flaky, not caused by this change.

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.
@linear-code

linear-code Bot commented Aug 8, 2026

Copy link
Copy Markdown

CL-5732

- 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.
@TheGreatAxios
TheGreatAxios merged commit 2d91477 into main Aug 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant