Files
orca/mobile/src/components/AgentStateDot.tsx
T
Brennan BensonandNeil ec4687c434 feat(agents): distinguish Claude background monitoring (takes over #14205) (#16201)
* feat(agents): distinguish Claude background monitoring

Adds an optional `workingMode: 'monitoring'` discriminator for a Claude
session whose lead turn finished but which still has background shell tasks
or session crons registered. The wire state stays `working`, so older peers
that never read the field keep rendering Working.

(cherry picked from commit d5d54b4bdd)

Rebased onto current main (554 commits of drift) by Brennan Benson;
conflicts resolved by keeping both sides where main and this branch made
independent additions to the same construct.

* fix(sidebar): keep monitoring status visible

(cherry picked from commit fd6b38654d)

* test(agents): cover Claude monitoring drain

(cherry picked from commit ce4d61ebf8)

* test(mobile): avoid unresolved renderer test type

(cherry picked from commit bbcfa35ff9)

* feat(agents): render Claude monitoring as a static turquoise dot

Replaces the yellow Radio glyph from #14205 with a static dot in a new
--agent-monitoring token (#8abeb7), defined once for light and once for
dark like --workspace-status-done, so the status keeps its identity when
the theme flips. Deliberately a fixed UI value: it never reads terminal
theme state at runtime.

Adds the turn-boundary notification pins. The monitoring predicate and
the turnCompletedAt stamp are computed from the same "lead said done but
the pane resolves to working" expression, so a rename can silently drop
the stamp and kill a completion notification that works today with
nothing else going red.

* 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.

---------

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-08-24 15:24:32 -07:00

76 lines
2.3 KiB
TypeScript

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'
// Per-agent state indicator, 1:1 with desktop AgentStateDot
// (src/renderer/src/components/AgentStateDot.tsx): yellow spinner for 'working',
// 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' | 'monitoring'>, string> = {
done: '#10b981',
blocked: '#ef4444',
waiting: '#ef4444',
interrupted: '#ef4444',
idle: 'rgba(115,115,115,0.4)'
}
const WORKING_COLOR = '#eab308'
export function AgentStateDot({ state }: { state: AgentDotState }) {
const spinValue = useRef(new Animated.Value(0)).current
useEffect(() => {
if (state === 'working') {
const animation = Animated.loop(
Animated.timing(spinValue, {
toValue: 1,
duration: 1000,
easing: Easing.linear,
useNativeDriver: true
})
)
animation.start()
return () => animation.stop()
}
spinValue.setValue(0)
return undefined
}, [state, spinValue])
if (state === 'working') {
const rotate = spinValue.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '360deg'] })
return (
<View style={styles.wrapper}>
<Animated.View style={[styles.spinner, { transform: [{ rotate }] }]} />
</View>
)
}
if (state === 'monitoring') {
return (
<View style={styles.wrapper} accessibilityLabel="Monitoring background tasks">
<Radio size={10} color={WORKING_COLOR} />
</View>
)
}
return (
<View style={styles.wrapper}>
<View style={[styles.dot, { backgroundColor: DOT_COLORS[state] }]} />
</View>
)
}
const styles = StyleSheet.create({
wrapper: { width: 10, height: 10, alignItems: 'center', justifyContent: 'center' },
dot: { width: 6, height: 6, borderRadius: 3 },
spinner: {
width: 6,
height: 6,
borderRadius: 3,
borderWidth: 1.5,
borderColor: WORKING_COLOR,
borderTopColor: 'transparent'
}
})