Repository navigation
fix(sidebar): a title that only names an agent reads "No status reported", not "Idle" - #24083
brennanb2025 wants to merge 2 commits into
Conversation
…ted", not "Idle" A terminal title that holds just an agent's name (grok, agy, OpenCode, aider, a hooks-off Codex) said which agent was in the pane, and the sidebar read it as Idle, grey dot and all, even while the agent worked. The title classifier gains a third answer, `unreported`: an agent's name is in the title and nothing in it says what the agent is doing. The sidebar row builder (including #23948's plain-title default) and smart attention read it through `readAgentTitleActivity`; the row shows "No status reported" beside the existing dashed ring in the muted colour. `detectAgentStatusFromTitle` and `classifyTitleActivity` keep their contract (a bare name is still idle), so readiness, mail, completion and cleanup are unchanged; a ratchet test pins their importers so the list can only shrink. The dashboard card and pop-out keep publishing `idle` for the new state, the way `unverifiable` already does, because main validates card states against a fixed list.
|
Warning Review limit reachedNext included review available in 4 minutes. View limit detailsLimit details: You’ve used all 10 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (25)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
| case 'unreported': | ||
| return 'no status reported' |
There was a problem hiding this comment.
Summary label remains English When the UI is set to Spanish, French, Japanese, Korean, or Chinese, the compact worktree summary and its accessible expansion label still say “no status reported” in English, while the agent row and dot use the translated phrase. Translating this summary label would keep the new state consistent across the same card.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
…e-no-status-reported
There was a problem hiding this comment.
ℹ️ No critical issues — one minor test-coverage gap. Behaviour looks correct and the strangler approach is sound.
Reviewed changes
- New
unreportedtitle activity.computeAgentTitleActivityreturnsunreportedfor a title that names an agent and says nothing else;readAgentTitleActivityexposes it, anddetectAgentStatusFromTitlebecomes a temporary wrapper that maps it back toidleso every settle/deliver/notify/cleanup reader is byte-for-byte unchanged. - Only the display readers move. The title-derived row builder (
worktree-title-derived-agent-rows.ts, including #23948's?? 'unreported'default and the Cursor literal) andsmart-attention.tsread the new value;unreportedranks with idle and is never attention. - Row/dot vocabulary.
AgentRowStateandAgentDotStategainunreported; the dot renderslucide-circle-dashedintext-muted-foreground, andauto.lib.agentRowState.noStatusReportedis added to en/es/fr/ja/ko/zh. - Dashboard stays on the published vocabulary.
dashboardCardDotStateprojectsunreportedtoidle, so main's fixedDashboardCardDotStateallowlist never sees an unknown value and the card is not dropped. - Tests. Committed-recording-backed cases (OpenCode boot title, Grok boot title), hand-built row/summary/dot tests, and
agent-title-idle-wrapper-ratchet.test.tspinning the 35 files that still read the name-as-idle guess.
I read the full diff, ran the six changed/added test files with the repo vitest config (82 passing), and confirmed verify:localization-catalogs passes. I also traced that unreported cannot reach activity threads (ActivityLiveAgentState excludes it) or the published dashboard card vocabulary.
ℹ️ agent-row-tool-preview.test.ts lost the exhaustiveness its comment promises
src/renderer/src/lib/agent-row-tool-preview.test.ts:7 builds ROW_STATES as [...AGENT_STATUS_STATES, 'idle', 'unverifiable'] and its comment states the enumeration exists "so a new one has to make this decision explicitly rather than defaulting into showing a stale tool." Nothing enforces that: readonly AgentRowState[] accepts a subset, so the new unreported member was silently omitted and showsAgentToolPreview('unreported') is untested. The runtime behaviour is correct (it returns false), so this is coverage, not a defect — but this is the first AgentRowState member added since the guard was written, and it did not force the decision it was meant to.
Technical details
# Make the tool-preview state enumeration exhaustive
## Affected sites
- `src/renderer/src/lib/agent-row-tool-preview.test.ts:7` — `ROW_STATES` omits the new `unreported` member; the array's type does not require completeness.
## Required outcome
- Adding a future `AgentRowState` member must fail typecheck until it appears in this list, matching the compile-time guard already used in `src/renderer/src/components/AgentStateDot.test.ts` (its `UncoveredState = Exclude<AgentDotState, (typeof ALL_STATES)[number]>` pattern).
## Suggested approach (optional)
- Add `'unreported'` to `ROW_STATES` now, and add an `Exclude<AgentRowState, (typeof ROW_STATES)[number]> extends never` assertion (as `AgentStateDot.test.ts` does) so the list can never drift again.DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
ℹ️ No new issues — the only commit since the previous review is a merge of
main, with no change to the PR's diff.
Reviewed changes
- No new code. Since my prior review at
0a89780, the branch gained one commit,f8f7303, which isMerge remote-tracking branch 'origin/main'. The resulting diff against the new merge-base is identical to the version I reviewed (same 25 files, same blob hashes). - The one minor point from the prior review therefore still stands:
src/renderer/src/lib/agent-row-tool-preview.test.ts:7omits the newunreportedmember from itsROW_STATESenumeration, whose comment claims the list forces an explicit decision for every state. Runtime behaviour is correct (showsAgentToolPreview('unreported') === false); adding'unreported'plus a compile-time exhaustiveness assertion would restore the guard.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
Review status: ready for review, no blocking findings (head
|













ELI5
When a terminal's title is just an agent's name (
grok,agy,OpenCode,aider, a Codex pane with hooks off, …), Orca's sidebar showed that agent as Idle: a grey dot and the word "Idle", even while the agent was busy working. A name only tells Orca which agent is in the pane, never what it is doing, so "Idle" was a guess presented as a fact.After this PR the same row says "No status reported" next to a muted dashed ring. The row stays; it just stops claiming the agent is resting.
What you will notice: agents without working hooks (hooks off, missing or silent: Grok, aider, Copilot, OpenCode, Antigravity, Codex with hooks off, and the rest in the table below) that sit at rest with a plain title go from "Idle" to "No status reported". That is the honest answer: the same title shows while they work, so the old "Idle" carried no information. While their title shows a spinner or another marker, they still read "Running" / "Needs input" / "Idle" exactly as before.
This PR is step 1 of a plan to stop Orca from reading a bare agent name as "Idle" anywhere. It changes only what the desktop sidebar and dashboard rows display; nothing Orca does on a title changes yet (see "Surfaces that still act on the bare name").
What Changed
The problem, as the user sees it. Start Antigravity in Orca and give it work: the sidebar row reads "Idle" the whole time it works (STA-7143). The same happens to any agent whose title carries only its name and whose hooks are off, missing, or silent. #23948 (merged 2026-09-30 as
fceca5cecec, queued for the next release by STA-8993) made this more common: to keep a hooks-off Codex row from vanishing when Codex retitles its tab to the project name, it defaulted any plain title on a process-identified pane to Idle (const status = titleStatus ?? 'idle'), so a busy agent titled with its folder name now also reads "Idle".Why it happened. One function,
detectAgentStatusFromTitle, answers two questions: is there an agent in this pane? and what is it doing? For a title that holds nothing but an agent's name it has to answer the first with "yes" (a non-null value), so it answered the second with a guess:return 'idle'.nullcould not be the honest answer, becausenullalready means "no agent here" and "the agent exited" to its readers.Before → after (desktop sidebar and dashboard rows):
grok,agy,OpenCode,aider,Codex, …): "Idle" + grey dot → "No status reported" + muted dashed ring.cursor agenttitle ([Bug]: Cursor Agent session are not shown in the left sidebar #10258 kept this row from vanishing by calling it Idle): "Idle" → "No status reported". The row still appears.✳/◇/rest keywords → Idle, permission words → Needs input), hook-reported rows, the stale-hook "No update in 34m" amber ring, theclaude agentsmanagement screen (still Idle), and the order rows sort in (the new state sorts with idle and is never an attention item).The mechanism.
src/shared/agent-title-core.ts: new typeAgentTitleActivity = AgentStatus | 'unreported'.unreportedmeans "an agent's name is in the title and nothing in it says what the agent is doing".nullkeeps its single meaning, "no agent in this title".src/shared/agent-title-status.ts: the classifier's last line (a name and nothing else) now returns'unreported', exposed asreadAgentTitleActivity.detectAgentStatusFromTitlebecomes a temporary wrapper that maps'unreported'back to'idle', so every reader that settles, delivers, notifies or cleans up on a title is byte-for-byte unchanged in this PR.classifyTitleActivity(the renderer's pass-through, about 22 call sites in 15 files before this PR, including cleanup and draft paste) keeps its contract too.worktree-title-derived-agent-rows.ts, including fix(sidebar): an agent's row stays while it runs, whatever its tab title #23948's plain-title default and the Cursor literal) and smart attention's title panes (smart-attention.ts, whereunreportedranks with idle, never as attention).AgentRowStateandAgentDotStategainunreported; the dot renders the existing dashed ring intext-muted-foreground(theunverifiablering stays amber); the one label, "No status reported", is localized (auto.lib.agentRowState.noStatusReported, added to en, es, fr, ja, ko and zh) and used for the row text, the dot's tooltip and its aria label. The compiler found every exhaustive switch andRecordover these types (activity pane grouping and labels, worktree summary order, subagent child rows).unreportedstays renderer-side and publishesidleto the dashboard card and pop-out, exactly asunverifiablealready does, because main validates card states against a fixed list and would drop an unknown value (the card would disappear).entry.statefor anunreportedrow is'working', the same synthetic value today's title-derived Idle rows carry (entryState = rowState === 'waiting' ? 'waiting' : 'working'; the row'sobservation.origin = 'title'tag marks it). Readers of the row userow.state(unreported) for the dot, text, summary and bucket; the readers of that syntheticentry.stateon a dashboard row are the verdict mark (agentVerdictDisplayMark, which only acts ondone/failed/interrupted) and the finished-timestamp helper (acts only ondone), so neither changes. The activity pane builds from hook entries, not title rows, so it never sees this row.unreportedis never persisted and never enters the host's agent-status store; it is re-derived from the current title on every render (cached per title string), so no stored flag can go stale and there is nothing to clean up.src/shared/agent-title-idle-wrapper-ratchet.test.ts, called a "ratchet" in the test name) lists the 35 production files that still read the name-as-idle guess (detectAgentStatusFromTitle/classifyTitleActivity). It fails if a new file starts reading it, and fails if a listed file stops reading it until the list is shortened, so the list can only shrink. The wrapper's docstring names its removal (step 4).Surfaces that still act on the bare name after this PR (for the same name-only pane):
done). Removed by step 5.orca terminalagent status over the wire: stillstatus: 'idle'. Removed by step 4 ({ isRunningAgent: true, status: null }, a shape old clients already handle).terminal wait --for tui-idle: still settle on the bare name. Removed by step 2+3.A bare
geminipane still reads "Idle". Title normalization rewrites a baregeminititle to Gemini's own rest title◇ Gemini CLIbefore it is stored, and◇is rest evidence. Verified live in the QA app below (the pane title is stored as◇ Gemini CLIand the summary reads "Gemini idle"). The normalization fix belongs with the settle change (step 2+3), because the host also stores that rewritten title and settles readiness on it.Per-agent table (all 43 agents). "Bare-name title row" is a pane whose title is only the agent's process name, with no hook row for that pane (hooks off, not installed, or silent). "Process-read row" is #23948's row for a pane whose foreground process Orca reads as that agent while the title carries no marker (local and daemon panes; WSL and tmux panes have no process read). Hook-reported rows, and titles with a marker, are unchanged for every agent.
claude)✳ …still Idle)claude)claude agentsscreen still Idle)claudeis read as Claude Code, so this row is labelled Claude Codeopenclaude)codex)codex)Codex readytitle still Idledevin)hermes)opencode)OpenCode;OC | …still Idle)mimo)agy)aider)copilot)grok)gemini)◇ Gemini CLI, see above)pi/ompis Pi's own rest-shaped title, which is evidence)droid)cursor-agent)cursor agenttitle: Idle → No status reported; a bareCursortitle yields no agent label, so no row (unchanged)zcodeis a name the classifier recognises, so it now reads unreported instead of idle, but it still yields no label and no row)Counts: 12 + gemini + 2 + 2 + droid + cursor + 24 = 43 agents (codex has two rows, one per hook mode).
Why
Why a third answer instead of another per-agent exception. Orca already works around this guess in five separate places, each for one agent or one reader: the Droid escape hatch in the classifier, Cursor's literal title that the host records as no status (
orca-runtime-apply-tracked-pty-title.ts, #10258) and the sidebar re-labels as Idle, the per-agent corroboration list intui-idle-evidence.ts, the explicit-idle filter interminal-wait-detection.ts, and its copy inagent-send-title-status.ts. Every new agent inherits the lie by default and needs its own opt-out at each reader. A bare name lies in every way the repo has recorded: mid-turn (Droid, Cursor), at boot before the agent can take input (Grok's recording writesgrokat 274 ms and its input box at 607 ms), as the agent's own boot title (OpenCode's recording writesOpenCodethree times before its session title), and when a shell auto-title writes the command name. No rule over the string separates those cases, so the uniform rule is: a name says which agent, never what it is doing.unreportedis that rule as a value, for every agent at once.Why not
null.nullalready means "no agent here" (the row would vanish, the #10258/#23767 bug) and "the agent exited" (the title tracker reads working/idle →nullas an exit). A non-null member keeps every=== nullcheck correct and makes every=== 'idle'check safe by default.Why not reuse
unverifiableor "No recent update". That state means "Orca heard this agent and then the stream went quiet"; it carries a clock ("No update in 34m") and amber warning weight. A title-only row was never heard from, and nothing went wrong, so it gets its own phrase and the muted colour. The ring is the existing "incomplete information" glyph so the vocabulary does not grow a new shape.Why only the display now. Moving the settle paths (readiness, turn end, mail, completion, cleanup) off the guess needs replacement signals first, per agent, from recorded terminal output; doing it without them would make worker start time out or automations wait for hours. That is steps 2+3. This PR removes the visible lie without changing any behaviour, and the one-way guard test keeps the old reader from spreading while the later steps retire it.
Linked Issue
This PR closes no ticket.
titleStatus ?? 'idle'default.The whole plan (a terminal title that holds only an agent's name must never be read as "Idle"):
status: nullon the wire.Visual Proof
Same QA app, same three panes, before (main
7afa4ee3dc6) and after (this branch). Pane 1's title isgrok(the hooks-off, name-only case). Pane 2's title is✳ Claude Code(a real rest title, the Idle dot). Pane 3 has a hook row last heard 34 minutes ago (the amber "No update in 34m" ring, truncated to "No u…" by the sidebar width).Light, before (main): Grok reads "Idle" with the same grey dot as Claude's real rest title.
Light, after: Grok reads "No status reported" with a muted dashed ring; Idle and the amber ring are unchanged.
Dark, before (main):
Dark, after:
Close-ups of the workspace card (supplemental; same captures):
How these were made: an isolated dev instance (its own profile and HOME, background launch with
ORCA_BACKGROUND_LAUNCH=1, no window shown or focused), driven over the Chrome DevTools Protocol with CDP screenshots of the hidden renderer. No agent CLI was run: each pane's title was set by a plain shell loop,printf '\033]0;grok\007', so this reproduces the title mechanism, not a live Grok or Antigravity session. The stale hook row was seeded into the renderer store. "Before" is the same running app after checking out main's versions of the changed source files (the dev server reloaded them), so everything else on screen is identical. The DOM confirms the glyph:lucide-circle-dashed text-muted-foreground, computed colourrgb(161, 161, 161)in dark,aria-label="No status reported".Testing
New and changed tests. Every test that pinned a bare name as Idle was rewritten deliberately, not regenerated. Provenance is stated per test.
worktree-title-derived-agent-rows.test.ts: OpenCode's boot title read from committed recording bytes (src/main/daemon/__fixtures__/pty-transcripts/opencode.txt, first OSC titleOpenCode, opencode 1.18.32) →unreported, "No status reported"; Grok's boot title from committed recording bytes (src/shared/__fixtures__/grok-startup-pty-trace.ts,grokat 274 ms, grok 1.0.0) → same; hand-builtagy→ same, and the row still exists; control, hand-builtagy ready→ Idle (passes on main too; shows the evidence path is untouched). Existing cases rewritten from Idle tounreported: Antigravity, Cursor's literal, the Codex retitle and process-read cases (fix(sidebar): an agent's row stays while it runs, whatever its tab title #23948), and four split-pane attribution cases whose "finished" pane was a bare name (all hand-built titles).worktree-card-unreported-agent-row.test.ts(hand-built titles): the rendered compact row shows "No status reported", the dashed ring,text-muted-foreground, no amber, no "Idle"; the worktree summary reads "1 no status reported, 1 idle"; the dashboard card projection publishesidle, identical to an idle row.AgentStateDot.test.ts:unreportedrenders the muted dashed ring with the right aria label; added to the exhaustive state list (a compile-time guard).src/shared/agent-title-activity.test.ts(hand-built): bare names →unreported; marker titles unchanged;nulltitles unchanged (includingDroidandCursor Agent); the wrapper still returnsidlefor bare names.smart-attention-bare-agent-name.test.ts(hand-built): regression guard, labelled as such (it passes on main, where a bare name read idle, which ranked the same): a bare-name pane ranks with idle; positive control⠋ agyreaches the working class.agent-title-idle-wrapper-ratchet.test.ts: the one-way guard on the files that still read the old guess (fails on a new reader and on a stale entry).terminal-title-classification-corpus.test.tsand the other existing classifier suites pass unchanged, which is the evidence thatdetectAgentStatusFromTitle's answers did not change.Ablation at the final head (revert one producer, run the six named test files, restore). Each reddens for its stated reason:
'idle': 18 fail (both recording-backed cases,agy, the rendered-row and summary tests, the Antigravity/Codex/split-pane cases, six classifier cases).?? 'idle': 4 fail (the process-read row cases).'idle': 1 fails (the Cursor row case).unreportedbranch removed: 2 fail (dot test, rendered row).unreported: 1 fails.unreportedthrough: 6 fail (the wrapper-contract cases).Checks run:
tc-gate pnpm tc:webandtc-gate pnpm tc:nodepass;pnpm run check:code-quality:changedpasses (0 findings, including the design-system gate); fulloxlinton every touched file passes (no max-lines disables);check:react-doctor:changedpasses;verify:localization-catalogs,-catalog,-extraction,-coverageand-runtime-catalogpass. At the final head, all 82 test files that import the changed modules pass (1,282 tests). An earlier, larger parallel run hit one unrelated 30 s timeout inWorktreeCard.compact-hover.test.tsx, which passes alone and in the final run.Platforms. Tested on macOS only. The change is renderer-side string classification and styling with no platform branch; the colour token is theme-defined for light and dark. SSH relay, daemon and local panes derive these rows the same way from their titles. A paired remote client follows its own build for the sidebar: an older client keeps showing "Idle", which is today's behaviour. Nothing crosses the wire, and nothing is persisted.
Review
Please check:
entry.state = 'working'on the synthetic entry of anunreportedtitle row (same as today's Idle title rows; explained above).Differences from the reference implementations (the desktop terminal app and the host-daemon app Orca's lifecycle design is compared against). Each row was checked against the reference sources in review:
unreportedauthorizes nothing. Matches the desktop reference's handling of its unknown state, which drives no lifecycle transition.idlefor this state. Temporary, following the existingunverifiableprecedent, until a card-validator extension that old pop-out windows tolerate.What I verified and what I did not. Verified: the unit tests and ablations above; both typechecks; lint and localization gates; the rendered rows in light and dark in a real (dev) app; that a bare
geminititle is stored as◇ Gemini CLIand reads Idle. Not verified: any live agent session (no agent CLI was run; titles were set withprintf); Linux, Windows, SSH relay or WSL panes (no platform-specific code changed); a packaged build; the phone (unchanged by design).Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation. (Not applicable: no agent skill is touched.)Notes
unreportedis never persisted and never published; the dashboard card and pop-out receiveidle, which old windows already accept.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred): scoped typechecks, lint, localization and the affected tests ran locally; CI covers the full suite and build.