mirror of
https://github.com/stablyai/orca.git
synced 2026-10-04 00:02:21 +00:00
* fix(ui): label agent state glyphs and swap monitoring to a heartbeat The monitoring glyph read as unlabeled: AgentStateDot set only aria-label, which renders no hover tooltip, so hovering it showed the row's own title — the same truncated text already visible. Its row siblings (agent icon, model chip) both had hover titles, leaving this glyph the odd one out. Give every state a native title in the shared primitive, so done/working/ blocked/idle gain the same affordance across the sidebar, tab bar, dashboard, kanban, cmd-J palette and AI Vault at once. Callers can override via a new optional title prop; AiVaultSessionSubagents drops its now-redundant wrapper. Native title rather than the Radix tooltip: AgentStateDot renders in two surfaces with no TooltipProvider above it — the dashboard popout is its own React root, and AgentMapScene — so Radix would throw there. StatusIndicator already sets a native title for the same reason. Also swap lucide Radio for Activity. Radio reads as "broadcasting"; the heartbeat line reads as "still running", which is what the state means. Mobile keeps its documented 1:1 parity with the desktop primitive. Fixes STA-5794 * fix(ui): avoid duplicate agent state tooltips * fix(ui): preserve disabled agent tooltip reason * fix(ui): stop the state dot from shadowing a row's disabled reason The shared AgentStateDot now emits a native title on every state, so at any call site nested inside an element that already has a title, the dot's generic state word wins on hover over the more useful ancestor text. That regressed the sidebar agent row, which carries `sendTargetDisabledReason ?? rowTitle`: hovering the dot showed "Working" instead of the actionable send-target reason. Same guard the review-notes send menu already uses. Also covers three hunks that shipped untested: the Radix opt-outs in ActivityPrototypePage and the AI Vault subagent line's dropped wrapper title both stayed green when reverted, and the suppression test was a `not.toContain` sweep that passed against the pre-fix tree. * fix(ui): preserve heartbeat hover tooltips * fix(ui): preserve lineage drop hit zones * Use styled tooltips for state indicators * Update jump palette tooltip assertions * Limit status tooltips to agents * Restore agent workspace status tooltips * Keep status tooltips on agent indicators * Clarify agent status tooltip ownership * Restore agent-derived workspace status tooltips
101 lines
2.8 KiB
TypeScript
101 lines
2.8 KiB
TypeScript
import { useEffect, useRef } from 'react'
|
|
import { Activity } from 'lucide-react-native'
|
|
import { Animated, Easing, StyleSheet, View } from 'react-native'
|
|
import type { AgentWorkingMode } from '../../../src/shared/agent-status-types'
|
|
|
|
type WorktreeStatus = 'working' | 'active' | 'permission' | 'done' | 'inactive'
|
|
|
|
// Why: colors and sizing are 1:1 with the desktop StatusIndicator
|
|
// (src/renderer/src/components/sidebar/StatusIndicator.tsx) so the mobile
|
|
// worktree list reads identically to the sidebar — same yellow spinner for
|
|
// 'working', same emerald dot for 'active'/'done', same neutral-500 @ 40%
|
|
// for 'inactive', same red for 'permission'. Diverging palettes here lose
|
|
// the design intent ('moving' vs 'alive' vs 'completed') the desktop encodes.
|
|
const STATUS_COLORS: Record<WorktreeStatus, string> = {
|
|
working: '#eab308',
|
|
active: '#10b981',
|
|
done: '#10b981',
|
|
permission: '#ef4444',
|
|
inactive: 'rgba(115,115,115,0.4)'
|
|
}
|
|
|
|
export function AgentSpinner({
|
|
status,
|
|
workingMode
|
|
}: {
|
|
status: WorktreeStatus
|
|
workingMode?: AgentWorkingMode
|
|
}) {
|
|
const spinValue = useRef(new Animated.Value(0)).current
|
|
const monitoring = status === 'working' && workingMode === 'monitoring'
|
|
|
|
useEffect(() => {
|
|
if (status === 'working' && !monitoring) {
|
|
const animation = Animated.loop(
|
|
Animated.timing(spinValue, {
|
|
toValue: 1,
|
|
duration: 1000,
|
|
easing: Easing.linear,
|
|
useNativeDriver: true
|
|
})
|
|
)
|
|
animation.start()
|
|
return () => animation.stop()
|
|
}
|
|
spinValue.setValue(0)
|
|
}, [monitoring, status, spinValue])
|
|
|
|
const color = STATUS_COLORS[status] ?? STATUS_COLORS.inactive
|
|
|
|
if (monitoring) {
|
|
return (
|
|
<View style={styles.wrapper} accessibilityLabel="Monitoring background tasks">
|
|
<Activity size={12} color={STATUS_COLORS.working} />
|
|
</View>
|
|
)
|
|
}
|
|
|
|
if (status === 'working') {
|
|
const rotate = spinValue.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: ['0deg', '360deg']
|
|
})
|
|
return (
|
|
<View style={styles.wrapper}>
|
|
<Animated.View style={[styles.spinner, { borderColor: color, transform: [{ rotate }] }]} />
|
|
</View>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<View style={styles.wrapper}>
|
|
<View style={[styles.dot, { backgroundColor: color }]} />
|
|
</View>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
// Why: 12x12 wrapper centered around an 8x8 inner glyph mirrors the
|
|
// desktop's `inline-flex h-3 w-3 ... items-center justify-center` shell
|
|
// around `size-2` indicator — keeps row height/baseline alignment stable
|
|
// across status transitions.
|
|
wrapper: {
|
|
width: 12,
|
|
height: 12,
|
|
alignItems: 'center',
|
|
justifyContent: 'center'
|
|
},
|
|
dot: {
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: 4
|
|
},
|
|
spinner: {
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: 4,
|
|
borderWidth: 2,
|
|
borderTopColor: 'transparent'
|
|
}
|
|
})
|