From 0a8978074fa5c87f8bb570e06161869087e8d0cc Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:29:19 -0700 Subject: [PATCH] fix(sidebar): a title that only names an agent reads "No status reported", 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. --- .../src/components/AgentStateDot.test.ts | 10 +++ src/renderer/src/components/AgentStateDot.tsx | 16 ++++ .../activity/activity-thread-grouping.ts | 3 +- .../activity/activity-thread-presentation.ts | 3 + .../dashboard/dashboard-row-bucket.ts | 8 +- ...rdAgents.acknowledgement-emphasis.test.tsx | 3 +- .../smart-attention-bare-agent-name.test.ts | 68 +++++++++++++++ .../src/components/sidebar/smart-attention.ts | 13 +-- .../sidebar/worktree-card-agent-summary.ts | 4 + ...worktree-card-unreported-agent-row.test.ts | 81 ++++++++++++++++++ .../sidebar/worktree-subagent-child-rows.ts | 4 +- .../worktree-title-derived-agent-rows.test.ts | 84 +++++++++++++++---- .../worktree-title-derived-agent-rows.ts | 51 ++++++----- src/renderer/src/i18n/locales/en.json | 3 + src/renderer/src/i18n/locales/es.json | 3 + src/renderer/src/i18n/locales/fr.json | 3 + src/renderer/src/i18n/locales/ja.json | 3 + src/renderer/src/i18n/locales/ko.json | 3 + src/renderer/src/i18n/locales/zh.json | 3 + src/renderer/src/lib/agent-row-decay-state.ts | 13 ++- src/renderer/src/lib/agent-row-dot-state.ts | 1 + src/shared/agent-title-activity.test.ts | 32 +++++++ src/shared/agent-title-core.ts | 2 + .../agent-title-idle-wrapper-ratchet.test.ts | 63 ++++++++++++++ src/shared/agent-title-status.ts | 27 ++++-- 25 files changed, 449 insertions(+), 55 deletions(-) create mode 100644 src/renderer/src/components/sidebar/smart-attention-bare-agent-name.test.ts create mode 100644 src/renderer/src/components/sidebar/worktree-card-unreported-agent-row.test.ts create mode 100644 src/shared/agent-title-activity.test.ts create mode 100644 src/shared/agent-title-idle-wrapper-ratchet.test.ts diff --git a/src/renderer/src/components/AgentStateDot.test.ts b/src/renderer/src/components/AgentStateDot.test.ts index a767a61ccf4b..e4c901ee108b 100644 --- a/src/renderer/src/components/AgentStateDot.test.ts +++ b/src/renderer/src/components/AgentStateDot.test.ts @@ -96,6 +96,15 @@ describe('AgentStateDot', () => { expect(markup).not.toContain('data-agent-spinner') }) + it('renders unreported as a muted dashed ring, never the amber lost-contact ring', () => { + const markup = renderMarkup('unreported') + + expect(markup).toContain('lucide-circle-dashed') + expect(markup).toContain('text-muted-foreground') + expect(markup).not.toContain('text-amber-500') + expect(markup).toContain('aria-label="No status reported"') + }) + it.each(['blocked', 'interrupted'] satisfies AgentDotState[])( 'renders %s as a red attention dot', (state) => { @@ -116,6 +125,7 @@ describe('AgentStateDot', () => { 'done', 'idle', 'unverifiable', + 'unreported', 'permission' ] satisfies AgentDotState[] diff --git a/src/renderer/src/components/AgentStateDot.tsx b/src/renderer/src/components/AgentStateDot.tsx index af8ac4efd843..928ffef01dbe 100644 --- a/src/renderer/src/components/AgentStateDot.tsx +++ b/src/renderer/src/components/AgentStateDot.tsx @@ -3,6 +3,7 @@ import { Activity, CircleCheck, CircleDashed } from 'lucide-react' import { cn } from '@/lib/utils' import { AgentQuestionIcon } from '@/components/AgentQuestionIcon' import { AgentWorkingSpinner } from '@/components/AgentWorkingSpinner' +import { agentNoStatusReportedLabel } from '@/lib/agent-row-decay-state' import { StateIndicatorTooltip, type StateIndicatorTooltipSide @@ -35,6 +36,9 @@ export type AgentDotState = // held there, and never rendered as 'done' or 'working' — it asserts nothing about // the agent, only about what Orca last heard. | 'unverifiable' + // Why: the title names the agent and nothing says what it is doing. Not 'idle' (no rest + // evidence) and not 'unverifiable' (Orca never heard a status, so none went quiet). + | 'unreported' // Why: the sidebar's title-based status flow (StatusIndicator/WorktreeCard) // collapses blocked + waiting into a single "needs attention" state. Keep // this as a distinct member so that flow can render without inventing a new @@ -63,6 +67,8 @@ export function agentStateLabel(state: AgentDotState): string { return 'Idle' case 'unverifiable': return 'No recent update' + case 'unreported': + return agentNoStatusReportedLabel() case 'permission': return 'Needs attention' } @@ -134,6 +140,16 @@ export const AgentStateDot = React.memo(function AgentStateDot({