mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 08:02:12 +00:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user