revert(sidebar): restore emerald dot for workspace 'done' status (#1294)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson
2026-04-30 20:17:55 -07:00
committed by GitHub
co-authored by Orca
parent 28b25f0519
commit ff8771b609
5 changed files with 33 additions and 50 deletions
@@ -12,8 +12,9 @@ describe('AgentStateDot', () => {
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'
// visually distinct from other emerald-adjacent states across surfaces.
// Note: the sidebar's StatusIndicator intentionally diverges and uses an
// emerald dot for 'done'. 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')
+13 -10
View File
@@ -3,11 +3,15 @@ import { CircleCheck } from 'lucide-react'
import { cn } from '@/lib/utils'
// 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.
// agent hover share a single state vocabulary. Most states render as a dot;
// 'working' renders a spinner. 'done' intentionally diverges from the
// sidebar's StatusIndicator: the dashboard uses a check icon so completion
// is visually distinct from 'idle' (grey dot) and the sidebar's 'active'
// (emerald dot), while the sidebar collapses 'done'/'active' to the same
// emerald dot and relies on a tooltip. 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'
@@ -70,11 +74,10 @@ 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).
// Why: the dashboard lists many agents, so a check glyph scans well for
// agent-reported completion and keeps 'done' visually distinct from
// 'idle' and other dot states at a glance. The sidebar's StatusIndicator
// intentionally diverges (emerald dot + tooltip) — see file header.
return (
<span
className={cn('inline-flex shrink-0 items-center justify-center', box, className)}
@@ -23,16 +23,9 @@ describe('StatusIndicator', () => {
expect(classNames).toContain('bg-emerald-500')
})
it('renders done as an emerald check icon, not a dot', () => {
const markup = renderMarkup('done')
it('renders done as an emerald dot', () => {
const classNames = renderDotClassNames('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(/<span class="[^"]*rounded-full[^"]*bg-emerald-500/)
expect(classNames).toContain('bg-emerald-500')
})
})
@@ -1,5 +1,4 @@
import React from 'react'
import { CircleCheck } from 'lucide-react'
import { cn } from '@/lib/utils'
import { getWorktreeStatusLabel, type WorktreeStatus } from '@/lib/worktree-status'
@@ -19,12 +18,12 @@ const StatusIndicator = React.memo(function StatusIndicator({
title,
...rest
}: StatusIndicatorProps) {
// Why: surface the status label as a native tooltip so hovering the
// indicator reveals the state — matters especially for 'active' vs
// 'done' (dot vs check both in emerald). Callers pass aria-hidden="true"
// alongside an sr-only label, so the `title` attribute is ignored by AT
// and only serves sighted users on hover. Callers can override by
// passing their own `title`.
// Why: surface the status label as a native tooltip so hovering the dot
// reveals the state — matters especially for 'active' vs 'done', which
// share the same emerald dot. Callers pass aria-hidden="true" alongside
// an sr-only label, so the `title` attribute is ignored by AT and only
// serves sighted users on hover. Callers can override by passing their
// own `title`.
const resolvedTitle = title ?? getWorktreeStatusLabel(status)
if (status === 'working') {
@@ -39,23 +38,6 @@ const StatusIndicator = React.memo(function StatusIndicator({
)
}
if (status === 'done') {
// Why: agent-reported completion gets a check icon instead of a dot so
// it is visually distinct from 'active' (terminal open, quiet), which
// also renders emerald. Before this, users with the experimental
// agent-tracking toggle couldn't tell a newly-opened quiet terminal
// apart from a completed agent — both were emerald dots.
return (
<span
className={cn('inline-flex h-3 w-3 shrink-0 items-center justify-center', className)}
title={resolvedTitle}
{...rest}
>
<CircleCheck className="size-3 text-emerald-500" aria-hidden="true" />
</span>
)
}
return (
<span
className={cn('inline-flex h-3 w-3 shrink-0 items-center justify-center', className)}
@@ -67,8 +49,12 @@ const StatusIndicator = React.memo(function StatusIndicator({
'block size-2 rounded-full',
status === 'permission'
? 'bg-red-500'
: status === 'active'
? 'bg-emerald-500'
: status === 'done' || status === 'active'
? // Green dot for both hook-reported 'done' and the heuristic
// 'active' (terminal open, quiet). Working uses a yellow
// spinner so working vs done differ by motion; 'inactive'
// stays grey.
'bg-emerald-500'
: 'bg-neutral-500/40'
)}
/>
@@ -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 indicator keeps showing after the agent process exits,
// consulted too so the done dot keeps glowing 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 indicator.
// in progress" signal is more informative than a retained completion dot.
// 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