feat(native-chat): give the strip's monitor heartbeat the sidebar amber

The glyph matched AgentStateDot but the colour did not, so a monitor in the
strip did not read as the monitor in the agent sidebar. One shared tone helper
now serves the header segment and the expanded row, so they cannot diverge.

Monitoring is a state the app already colours; the other four kinds are plain
markers and stay neutral. A running turn still dims the whole set.
This commit is contained in:
Merge Sim
2026-09-09 01:36:58 -07:00
parent 7359131228
commit a9703936c9
2 changed files with 66 additions and 9 deletions
@@ -97,6 +97,51 @@ describe('background-tasks strip header', () => {
}
})
it('gives the monitor heartbeat the sidebar amber and leaves other kinds neutral', () => {
const header = renderHeader([
{ id: 'a1', kind: 'agent' },
{ id: 'm1', kind: 'monitor' }
])
// Same glyph AND same colour as AgentStateDot/StatusIndicator, or a monitor
// here does not read as the monitor there.
expect(header.querySelector('.lucide-activity')?.classList).toContain('text-yellow-500')
expect(header.querySelector('.lucide-bot')?.classList).toContain('text-muted-foreground')
expect(header.querySelector('.lucide-bot')?.classList).not.toContain('text-yellow-500')
})
it('dims the monitor amber while a turn owns the voice', () => {
render(
<NativeChatBackgroundTasksStatus
isVisible
tasks={[{ id: 'm1', kind: 'monitor' }]}
settledTasks={[]}
indicatorActive={false}
supportsTaskStop={false}
supportsStopAll={false}
stoppingTaskIds={new Set()}
stoppingAll={false}
onStop={() => {}}
/>
)
const header = screen.getByRole('button', { expanded: false })
expect(header.querySelector('.lucide-activity')?.classList).toContain('text-yellow-500/40')
})
it('carries the monitor amber on the expanded row too', () => {
const header = renderHeader([
{ id: 'm1', kind: 'monitor', description: 'watcher' },
{ id: 'c1', kind: 'command', description: 'sleep 90' }
])
fireEvent.click(header)
// Each kind group is its own labelled list, so scope to the monitor one.
const monitors = screen.getByRole('list', { name: 'Monitors' })
expect(monitors.querySelector('.lucide-activity')?.classList).toContain('text-yellow-500')
const shell = screen.getByRole('list', { name: 'Shell' })
expect(shell.querySelector('.lucide-square-terminal')?.classList).toContain(
'text-muted-foreground'
)
})
it('carries no icon on a collapsed total, which spans kinds', () => {
const header = renderHeader([
{ id: 'a1', kind: 'agent' },
@@ -53,6 +53,15 @@ const KIND_ICONS = {
unknown: CircleHelp
} as const
/** Monitoring is a STATE the app colours the same on every surface — the agent
* sidebar and `AgentStateDot` both draw an amber heartbeat — so the strip must
* match it or the two stop reading as the same thing. The other four are plain
* kind markers and stay neutral. `dimmed` is the running-turn treatment. */
function kindIconTone(kind: AgentSessionBackgroundTask['kind'], dimmed: boolean): string {
const tone = kind === 'monitor' ? 'text-yellow-500' : 'text-muted-foreground'
return dimmed ? `${tone}/40` : tone
}
function BackgroundTaskRow(props: {
entry: BackgroundRosterTask
now: number
@@ -75,7 +84,10 @@ function BackgroundTaskRow(props: {
.join(' · ')
return (
<li className="flex h-6 min-w-0 items-center gap-2 text-foreground/80">
<Icon aria-hidden="true" className="size-3.5 shrink-0 text-muted-foreground" />
<Icon
aria-hidden="true"
className={`size-3.5 shrink-0 ${kindIconTone(entry.task.kind, false)}`}
/>
<AgentStateDot state={entry.state} size="sm" title={null} />
<span className="min-w-0 flex-1 truncate">
<span className="font-medium text-foreground">{entry.name}</span>
@@ -157,19 +169,19 @@ export function NativeChatBackgroundTasksStatus(props: {
<span className="min-w-0 truncate">
{header.segments.map((segment, index) => {
// A collapsed total spans kinds, so no single icon can stand for it.
const Icon = segment.kind ? KIND_ICONS[segment.kind] : null
const kind = segment.kind
const Icon = kind ? KIND_ICONS[kind] : null
return (
<span key={segment.text}>
{index > 0 ? <span className="text-border"> · </span> : null}
{Icon ? (
{Icon && kind ? (
<Icon
aria-hidden="true"
className={`mr-1 inline size-3 align-[-0.125em] ${
// The turn owns the voice: same icons, dimmed.
props.indicatorActive
? 'text-muted-foreground'
: 'text-muted-foreground/40'
}`}
// The turn owns the voice: same icons, dimmed until it ends.
className={`mr-1 inline size-3 align-[-0.125em] ${kindIconTone(
kind,
!props.indicatorActive
)}`}
/>
) : null}
<span className="font-medium text-foreground">{segment.text}</span>