From caee6f91f89c3e0091ca37e160dfad0782d1db88 Mon Sep 17 00:00:00 2001 From: Trevin Chow Date: Thu, 30 Apr 2026 16:09:10 -0700 Subject: [PATCH] fix(sidebar): differentiate 'active' vs 'done' status colors (#1277) Co-authored-by: Orca Co-authored-by: brennanb2025 --- .../src/components/AgentStateDot.test.ts | 22 +++++++++ src/renderer/src/components/AgentStateDot.tsx | 35 ++++++++++---- .../sidebar/StatusIndicator.test.ts | 38 +++++++++++++++ .../components/sidebar/StatusIndicator.tsx | 46 +++++++++++-------- .../src/components/sidebar/WorktreeCard.tsx | 4 +- 5 files changed, 113 insertions(+), 32 deletions(-) create mode 100644 src/renderer/src/components/AgentStateDot.test.ts create mode 100644 src/renderer/src/components/sidebar/StatusIndicator.test.ts diff --git a/src/renderer/src/components/AgentStateDot.test.ts b/src/renderer/src/components/AgentStateDot.test.ts new file mode 100644 index 000000000..717a0ca44 --- /dev/null +++ b/src/renderer/src/components/AgentStateDot.test.ts @@ -0,0 +1,22 @@ +import React from 'react' +import { renderToStaticMarkup } from 'react-dom/server' +import { describe, expect, it } from 'vitest' +import { AgentStateDot, type AgentDotState } from './AgentStateDot' + +function renderMarkup(state: AgentDotState): string { + return renderToStaticMarkup(React.createElement(AgentStateDot, { state })) +} + +describe('AgentStateDot', () => { + it('renders done as an emerald check icon', () => { + const markup = renderMarkup('done') + + // Why: 'done' renders a CircleCheck icon rather than a dot so it is + // visually distinct from other emerald-adjacent states across surfaces + // (mirrors StatusIndicator). Assertion targets the lucide 'circle-check' + // class hook + emerald text color, identifying the check icon without + // coupling to the exact SVG path markup lucide emits. + expect(markup).toContain('lucide-circle-check') + expect(markup).toContain('text-emerald-500') + }) +}) diff --git a/src/renderer/src/components/AgentStateDot.tsx b/src/renderer/src/components/AgentStateDot.tsx index e4068db17..255922213 100644 --- a/src/renderer/src/components/AgentStateDot.tsx +++ b/src/renderer/src/components/AgentStateDot.tsx @@ -1,11 +1,13 @@ import React from 'react' +import { CircleCheck } from 'lucide-react' import { cn } from '@/lib/utils' -// Why: shared state-dot primitive so the dashboard and the sidebar's agent -// hover render the same state vocabulary identically. The dot sits next to the -// agent icon (Claude/Codex/etc.) — they are two distinct glyphs: one for *who* -// (icon) and one for *what state* (dot). Keeping them separate keeps each -// glyph scannable at a glance instead of fused into a single decorated icon. +// Why: shared state-indicator primitive so the dashboard and the sidebar's +// agent hover render the same state vocabulary identically. Most states render +// as a dot; 'working' renders a spinner and 'done' renders a check icon. It +// sits next to the agent icon (Claude/Codex/etc.) — two distinct glyphs: one +// for *who* (agent icon) and one for *what state* (this indicator). Keeping +// them separate keeps each scannable instead of fused into one decorated icon. export type AgentDotState = | 'working' @@ -49,6 +51,7 @@ export const AgentStateDot = React.memo(function AgentStateDot({ }: Props): React.JSX.Element { const box = size === 'md' ? 'h-3 w-3' : 'h-2.5 w-2.5' const inner = size === 'md' ? 'size-2' : 'size-1.5' + const icon = size === 'md' ? 'size-3' : 'size-2.5' if (state === 'working') { return ( @@ -66,6 +69,22 @@ export const AgentStateDot = React.memo(function AgentStateDot({ ) } + if (state === 'done') { + // Why: match StatusIndicator — agent-reported completion renders as an + // emerald check icon instead of an emerald dot so 'done' is visually + // distinct from other emerald states (e.g., sidebar 'active'). Keeping + // the dashboard and sidebar on the same glyph for 'done' is the whole + // point of this shared primitive (see file header). + return ( + + + ) + } + return ( diff --git a/src/renderer/src/components/sidebar/StatusIndicator.test.ts b/src/renderer/src/components/sidebar/StatusIndicator.test.ts new file mode 100644 index 000000000..685ffb3e6 --- /dev/null +++ b/src/renderer/src/components/sidebar/StatusIndicator.test.ts @@ -0,0 +1,38 @@ +import React from 'react' +import { renderToStaticMarkup } from 'react-dom/server' +import { describe, expect, it } from 'vitest' +import StatusIndicator, { type Status } from './StatusIndicator' + +function renderMarkup(status: Status): string { + return renderToStaticMarkup(React.createElement(StatusIndicator, { status })) +} + +function renderDotClassNames(status: Status): string[] { + const markup = renderMarkup(status) + const dotClassName = markup.match(/ { + it('renders active as full emerald dot', () => { + const classNames = renderDotClassNames('active') + + expect(classNames).toContain('bg-emerald-500') + }) + + it('renders done as an emerald check icon, not a dot', () => { + const markup = renderMarkup('done') + + // Why: 'done' uses a CircleCheck icon rather than a rounded-full dot + // so it is visually distinct from 'active' (also emerald). The assertion + // targets the lucide 'circle-check' class hook + emerald text color, + // which together identify the check icon without coupling to the exact + // SVG path markup lucide emits. + expect(markup).toContain('lucide-circle-check') + expect(markup).toContain('text-emerald-500') + expect(markup).not.toMatch(/ + ) + } + return ( diff --git a/src/renderer/src/components/sidebar/WorktreeCard.tsx b/src/renderer/src/components/sidebar/WorktreeCard.tsx index 0d793e8f6..b6bd4add5 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.tsx @@ -157,7 +157,7 @@ const WorktreeCard = React.memo(function WorktreeCard({ // that the spinner flickered; the blocked/waiting/done states don't have // that problem — they're terminal (done) or attention-needed (blocked/ // waiting) and persist until the user acts. Retained "done" snapshots are - // consulted too so the done dot keeps glowing after the agent process exits, + // consulted too so the done indicator keeps showing after the agent process exits, // matching the dashboard's retention behavior. // // Priority (highest first): permission (blocked/waiting) > heuristic @@ -167,7 +167,7 @@ const WorktreeCard = React.memo(function WorktreeCard({ // completion. // heuristic 'working' wins over done because a spinner means the user has // already re-prompted the agent after it reported done — the newer "work - // in progress" signal is more informative than a retained completion dot. + // in progress" signal is more informative than a retained completion indicator. // Only the 'working' heuristic earns this precedence; 'active'/'inactive' // mean "quiet terminal", which shouldn't drown out a recent done. // Why: collapse live hook entries to booleans inside the selector so the