mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
revert(agents): restore the yellow Radio glyph for monitoring
Brennan chose #14205's original treatment over the turquoise dot, so the visual goes back to nwparker's: lucide Radio in text-yellow-500 across the sidebar, dashboard dot, cmd-j palette and agent-map ring. Reverts only the visual surface. The turn-boundary notification pins stay — the monitoring predicate and the turnCompletedAt stamp share an expression, so a rename can silently drop the stamp and kill a completion that works today with nothing else going red. The --agent-monitoring token is removed with its last consumer rather than left dead in main.css.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Radio } from 'lucide-react-native'
|
||||
import { Animated, Easing, StyleSheet, View } from 'react-native'
|
||||
import type { AgentWorkingMode } from '../../../src/shared/agent-status-types'
|
||||
|
||||
@@ -17,7 +18,6 @@ const STATUS_COLORS: Record<WorktreeStatus, string> = {
|
||||
permission: '#ef4444',
|
||||
inactive: 'rgba(115,115,115,0.4)'
|
||||
}
|
||||
const MONITORING_COLOR = '#8abeb7'
|
||||
|
||||
export function AgentSpinner({
|
||||
status,
|
||||
@@ -48,11 +48,9 @@ export function AgentSpinner({
|
||||
const color = STATUS_COLORS[status] ?? STATUS_COLORS.inactive
|
||||
|
||||
if (monitoring) {
|
||||
// Why: static dot, not a spinner — the lead turn is over. Keep MONITORING_COLOR
|
||||
// in step with --agent-monitoring in the desktop main.css.
|
||||
return (
|
||||
<View style={styles.wrapper} accessibilityLabel="Monitoring background tasks">
|
||||
<View style={[styles.dot, { backgroundColor: MONITORING_COLOR }]} />
|
||||
<Radio size={12} color={STATUS_COLORS.working} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Radio } from 'lucide-react-native'
|
||||
import { Animated, Easing, StyleSheet, View } from 'react-native'
|
||||
import type { AgentDotState } from '../worktree/agent-row-display'
|
||||
|
||||
@@ -7,10 +8,8 @@ import type { AgentDotState } from '../worktree/agent-row-display'
|
||||
// emerald for 'done', red for blocked/waiting/interrupted (attention), neutral
|
||||
// for idle. Distinct from the worktree-level AgentSpinner, which collapses the
|
||||
// agent vocabulary into the 5-state rollup the sidebar dot uses.
|
||||
const DOT_COLORS: Record<Exclude<AgentDotState, 'working'>, string> = {
|
||||
const DOT_COLORS: Record<Exclude<AgentDotState, 'working' | 'monitoring'>, string> = {
|
||||
done: '#10b981',
|
||||
// Keep in step with --agent-monitoring in the desktop main.css.
|
||||
monitoring: '#8abeb7',
|
||||
blocked: '#ef4444',
|
||||
waiting: '#ef4444',
|
||||
interrupted: '#ef4444',
|
||||
@@ -47,11 +46,16 @@ export function AgentStateDot({ state }: { state: AgentDotState }) {
|
||||
)
|
||||
}
|
||||
|
||||
if (state === 'monitoring') {
|
||||
return (
|
||||
<View style={styles.wrapper} accessibilityLabel="Monitoring background tasks">
|
||||
<Radio size={10} color={WORKING_COLOR} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.wrapper}
|
||||
accessibilityLabel={state === 'monitoring' ? 'Monitoring background tasks' : undefined}
|
||||
>
|
||||
<View style={styles.wrapper}>
|
||||
<View style={[styles.dot, { backgroundColor: DOT_COLORS[state] }]} />
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -4,38 +4,22 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AgentSpinner } from './AgentSpinner'
|
||||
import { AgentStateDot } from './AgentStateDot'
|
||||
|
||||
// Why: pinned to --agent-monitoring in the desktop main.css. Mobile has no CSS
|
||||
// tokens, so this literal is the only thing keeping the two surfaces in step.
|
||||
const DESKTOP_MONITORING_COLOR = '#8abeb7'
|
||||
const DESKTOP_DONE_COLOR = '#10b981'
|
||||
const DESKTOP_WORKING_COLOR = '#eab308'
|
||||
|
||||
type MonitoringTestRenderer = {
|
||||
readonly root: {
|
||||
findAllByType(type: string): { props: Record<string, unknown> }[]
|
||||
findByType(type: string): { props: Record<string, unknown> }
|
||||
}
|
||||
unmount(): void
|
||||
}
|
||||
|
||||
/** Background colour of the inner dot (the wrapper View is index 0). */
|
||||
function dotColor(renderer: MonitoringTestRenderer | null): string | undefined {
|
||||
const inner = renderer?.root.findAllByType('View')[1]
|
||||
const style = inner?.props.style
|
||||
const layers = Array.isArray(style) ? style : [style]
|
||||
for (const layer of layers) {
|
||||
const color = (layer as { backgroundColor?: string } | undefined)?.backgroundColor
|
||||
if (typeof color === 'string') {
|
||||
return color
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const { animationLoop, animationTiming, setValue } = vi.hoisted(() => ({
|
||||
animationLoop: vi.fn(() => ({ start: vi.fn(), stop: vi.fn() })),
|
||||
animationTiming: vi.fn(() => ({})),
|
||||
setValue: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('lucide-react-native', () => ({ Radio: 'Radio' }))
|
||||
vi.mock('react-native', () => ({
|
||||
Animated: {
|
||||
Value: function Value() {
|
||||
@@ -64,33 +48,30 @@ describe('mobile monitoring indicators', () => {
|
||||
renderer = null
|
||||
})
|
||||
|
||||
it('renders a static turquoise dot for a monitoring agent', async () => {
|
||||
it('renders a static Radio for a monitoring agent', async () => {
|
||||
await act(async () => {
|
||||
renderer = create(createElement(AgentStateDot, { state: 'monitoring' }))
|
||||
})
|
||||
|
||||
expect(dotColor(renderer)).toBe(DESKTOP_MONITORING_COLOR)
|
||||
// Why: the lead turn is over — spinning here is the bug this state exists to fix.
|
||||
expect(renderer?.root.findByType('Radio').props).toMatchObject({
|
||||
color: DESKTOP_WORKING_COLOR,
|
||||
size: 10
|
||||
})
|
||||
expect(animationTiming).not.toHaveBeenCalled()
|
||||
expect(animationLoop).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps a monitoring agent dot separable from the done dot', async () => {
|
||||
await act(async () => {
|
||||
renderer = create(createElement(AgentStateDot, { state: 'monitoring' }))
|
||||
})
|
||||
|
||||
expect(dotColor(renderer)).not.toBe(DESKTOP_DONE_COLOR)
|
||||
})
|
||||
|
||||
it('renders a static turquoise dot for an all-monitoring workspace', async () => {
|
||||
it('renders a static Radio for an all-monitoring workspace', async () => {
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
createElement(AgentSpinner, { status: 'working', workingMode: 'monitoring' })
|
||||
)
|
||||
})
|
||||
|
||||
expect(dotColor(renderer)).toBe(DESKTOP_MONITORING_COLOR)
|
||||
expect(renderer?.root.findByType('Radio').props).toMatchObject({
|
||||
color: DESKTOP_WORKING_COLOR,
|
||||
size: 12
|
||||
})
|
||||
expect(animationTiming).not.toHaveBeenCalled()
|
||||
expect(animationLoop).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -62,7 +62,6 @@
|
||||
--color-ring: var(--ring);
|
||||
--color-agent-question: var(--agent-question);
|
||||
--color-agent-question-text: var(--agent-question-text);
|
||||
--color-agent-monitoring: var(--agent-monitoring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
@@ -182,13 +181,6 @@
|
||||
--agent-question: var(--color-orange-600);
|
||||
/* Legible weight for text/glyphs on a tinted --agent-question surface. */
|
||||
--agent-question-text: var(--color-orange-700);
|
||||
/* "The turn is over; background work is still registered." Turquoise, not a
|
||||
green or a yellow: it has to stay separable from the emerald done/active dot
|
||||
at dot size, and from working-yellow on the map. Same literal in both themes,
|
||||
like --workspace-status-done — a status keeps its identity when the theme
|
||||
flips. Deliberately NOT read from the terminal theme at runtime: this is a
|
||||
fixed UI value like every other status colour. */
|
||||
--agent-monitoring: #8abeb7;
|
||||
--status-success: #15803d;
|
||||
--status-success-background: color-mix(in srgb, var(--status-success) 10%, transparent);
|
||||
--status-success-border: color-mix(in srgb, var(--status-success) 25%, transparent);
|
||||
@@ -299,7 +291,6 @@
|
||||
--ai-action-accent: var(--color-violet-400);
|
||||
--agent-question: var(--color-orange-500);
|
||||
--agent-question-text: var(--color-orange-300);
|
||||
--agent-monitoring: #8abeb7;
|
||||
--status-success: #86efac;
|
||||
--status-success-background: color-mix(in srgb, var(--status-success) 10%, transparent);
|
||||
--status-success-border: color-mix(in srgb, var(--status-success) 25%, transparent);
|
||||
|
||||
@@ -42,15 +42,13 @@ describe('AgentStateDot', () => {
|
||||
expect(markup).toContain('motion-reduce:border-t-yellow-500')
|
||||
})
|
||||
|
||||
it('renders monitoring as a static turquoise dot, never a spinner', () => {
|
||||
it('renders monitoring as a static yellow radio glyph', () => {
|
||||
const markup = renderMarkup('monitoring')
|
||||
|
||||
expect(markup).toContain('aria-label="Monitoring background tasks"')
|
||||
expect(markup).toContain('bg-agent-monitoring')
|
||||
// Why: the turn is over — a spinner here is the bug this state exists to fix.
|
||||
expect(markup).toContain('lucide-radio')
|
||||
expect(markup).toContain('text-yellow-500')
|
||||
expect(markup).not.toContain('data-agent-spinner')
|
||||
// Why: must stay visually separable from the done/active dot.
|
||||
expect(markup).not.toContain('bg-emerald-500')
|
||||
})
|
||||
|
||||
it('renders done as an emerald check icon', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { CircleCheck } from 'lucide-react'
|
||||
import { CircleCheck, Radio } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { AgentQuestionIcon } from '@/components/AgentQuestionIcon'
|
||||
import { AgentWorkingSpinner } from '@/components/AgentWorkingSpinner'
|
||||
@@ -84,6 +84,17 @@ export const AgentStateDot = React.memo(function AgentStateDot({
|
||||
)
|
||||
}
|
||||
|
||||
if (state === 'monitoring') {
|
||||
return (
|
||||
<span
|
||||
className={cn('inline-flex shrink-0 items-center justify-center', box, className)}
|
||||
aria-label={agentStateLabel(state)}
|
||||
>
|
||||
<Radio className={cn('text-yellow-500', icon)} aria-hidden="true" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (state === 'done') {
|
||||
// Why: the dashboard lists many agents, so a check glyph scans well for
|
||||
// agent-reported completion and keeps 'done' visually distinct from
|
||||
@@ -121,11 +132,7 @@ export const AgentStateDot = React.memo(function AgentStateDot({
|
||||
inner,
|
||||
state === 'blocked' || state === 'interrupted' || state === 'failed'
|
||||
? 'bg-red-500'
|
||||
: // Why: static dot, not a spinner — the turn is over. Turquoise stays
|
||||
// separable from the emerald done dot at this size.
|
||||
state === 'monitoring'
|
||||
? 'bg-agent-monitoring'
|
||||
: 'bg-neutral-500/40'
|
||||
: 'bg-neutral-500/40'
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('palette live status', () => {
|
||||
await render()
|
||||
|
||||
expect(testContainer.querySelector('[data-spinner]')).toBeNull()
|
||||
expect(testContainer.querySelector('.bg-agent-monitoring')).not.toBeNull()
|
||||
expect(testContainer.querySelector('.lucide-radio')?.classList).toContain('text-yellow-500')
|
||||
expect(dotLabels()).toEqual(['Monitoring background tasks'])
|
||||
})
|
||||
|
||||
@@ -276,7 +276,7 @@ describe('palette live status', () => {
|
||||
})
|
||||
|
||||
expect(testContainer.querySelector('[data-spinner]')).toBeNull()
|
||||
expect(testContainer.querySelector('.bg-agent-monitoring')).not.toBeNull()
|
||||
expect(testContainer.querySelector('.lucide-radio')?.classList).toContain('text-yellow-500')
|
||||
expect(dotLabels()).toEqual(['Monitoring background tasks'])
|
||||
expect(testContainer.querySelector('[title="Monitoring background tasks"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
@@ -420,10 +420,8 @@
|
||||
stroke: var(--color-yellow-500);
|
||||
}
|
||||
|
||||
/* Why: static turquoise, distinct from working-yellow — the turn is over but
|
||||
background work is still registered. Same token as the sidebar/dashboard dot. */
|
||||
.fleet-status-monitoring .agent-map-agent-mark {
|
||||
stroke: var(--agent-monitoring);
|
||||
stroke: var(--color-yellow-500);
|
||||
}
|
||||
|
||||
.fleet-status-blocked .agent-map-agent-mark {
|
||||
|
||||
@@ -362,7 +362,8 @@ describe('DashboardAgentRow', () => {
|
||||
)
|
||||
|
||||
expect(markup).toContain('Monitoring background tasks')
|
||||
expect(classTokens(markup)).toContain('bg-agent-monitoring')
|
||||
expect(markup).toContain('lucide-radio')
|
||||
expect(classTokens(markup)).toContain('text-yellow-500')
|
||||
expect(markup).not.toContain('data-agent-spinner')
|
||||
expect(markup).not.toContain('data-agent-row-tool-slot')
|
||||
})
|
||||
|
||||
@@ -31,13 +31,13 @@ describe('StatusIndicator', () => {
|
||||
expect(markup).toContain('motion-reduce:border-t-yellow-500')
|
||||
})
|
||||
|
||||
it('renders monitoring as a static turquoise dot, never a spinner', () => {
|
||||
it('renders monitoring as a static radio glyph', () => {
|
||||
const markup = renderMarkup('monitoring')
|
||||
|
||||
expect(markup).toContain('title="Monitoring background tasks"')
|
||||
expect(markup).toContain('bg-agent-monitoring')
|
||||
expect(markup).toContain('lucide-radio')
|
||||
expect(markup).toContain('text-yellow-500')
|
||||
expect(markup).not.toContain('data-agent-spinner')
|
||||
expect(markup).not.toContain('bg-emerald-500')
|
||||
})
|
||||
|
||||
it('renders permission as the shared question glyph', () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Radio } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { AgentQuestionIcon } from '@/components/AgentQuestionIcon'
|
||||
import { AgentWorkingSpinner } from '@/components/AgentWorkingSpinner'
|
||||
@@ -40,6 +41,18 @@ const StatusIndicator = React.memo(function StatusIndicator({
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'monitoring') {
|
||||
return (
|
||||
<span
|
||||
className={cn('inline-flex h-3 w-3 shrink-0 items-center justify-center', className)}
|
||||
title={resolvedTitle}
|
||||
{...rest}
|
||||
>
|
||||
<Radio className="size-3 text-yellow-500" aria-hidden="true" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'permission') {
|
||||
return (
|
||||
<span
|
||||
@@ -66,11 +79,7 @@ const StatusIndicator = React.memo(function StatusIndicator({
|
||||
// 'active' (terminal open, quiet). Working uses a yellow
|
||||
// ring above; 'inactive' stays grey.
|
||||
'bg-emerald-500'
|
||||
: // Why: static turquoise — the lead turn ended, but background work
|
||||
// is still registered, so this is neither working nor done.
|
||||
status === 'monitoring'
|
||||
? 'bg-agent-monitoring'
|
||||
: 'bg-neutral-500/40'
|
||||
: 'bg-neutral-500/40'
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user