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