From 185253f044d6b3c36801eea1a79eb97a6ff13be2 Mon Sep 17 00:00:00 2001 From: Natalie Date: Mon, 29 Jun 2026 21:54:07 -0400 Subject: [PATCH] feat(web): implement AutopilotView (Wave B) on cocotte ui + getAutopilot -- web/src/views/AutopilotView.tsx Co-Authored-By: Claude Opus 4.8 --- web/src/views/AutopilotView.tsx | 127 ++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 web/src/views/AutopilotView.tsx diff --git a/web/src/views/AutopilotView.tsx b/web/src/views/AutopilotView.tsx new file mode 100644 index 0000000..ef91d9b --- /dev/null +++ b/web/src/views/AutopilotView.tsx @@ -0,0 +1,127 @@ +import { usePoll, formatTime } from '../usePoll'; +import { getAutopilot, type AutopilotItem } from '../api'; +import { navigate } from '../useHashRoute'; +import { Button, Card, ErrText, HStack, Muted, Pill, StatusDot, Title, VStack } from '../ui'; + +/** Live per-thread autopilot states (Wave B unit). + * Title + scrollable list of live states backed by GET /autopilot (runner/scheduler/tasks). + * Each row: StatusDot + clickable handle (navigates to prospect detail), state badge (Pill colored per drafting/deciding/sending/sent/paused), + * eta or at, campaign (warning Pill), leaf (mono), why, open action (ghost Button). + * Renders using *only* cocotte ui/ primitives (no legacy classes, no shared file edits). + * Real data; 4s poll for live feel. Matches prototype AUTOPILOT live activity (buildAutopilot) shape + tasks for states. + */ +export function AutopilotView(): JSX.Element { + const { data, error, loading } = usePoll<{ items: AutopilotItem[] }>(() => getAutopilot(20), 4000); + + if (error) { + return ( + + + Autopilot + {error} + + + ); + } + + if (loading && !data) { + return ( + + + Autopilot + Loading live states… + + + ); + } + + const items = data?.items ?? []; + + return ( + + + Autopilot + what the AI is doing · live + + + {items.length === 0 ? ( + No live autopilot activity. + ) : ( + + {items.map((item) => ( + + ))} + + )} + + ); +} + +function AutopilotRow({ item }: { item: AutopilotItem }): JSX.Element { + const open = () => navigate('prospect', item.handle); + const tone = getToneForState(item.state); + const dotStatus = getDotForState(item.state); + const timeLabel = getEtaOrAt(item); + + return ( + + + + + + {item.state} + {timeLabel ? {timeLabel} : null} +
+ {item.campaign} + + {item.leaf} + + + why → {item.why} + + + + + + ); +} + +function getToneForState(state: string): 'accent' | 'neutral' | 'success' | 'warning' | 'error' { + const s = (state || '').toLowerCase(); + if (s === 'sending') return 'warning'; + if (s === 'sent') return 'success'; + if (s === 'paused') return 'neutral'; + if (s === 'deciding' || s === 'drafting') return 'accent'; + return 'neutral'; +} + +function getDotForState(state: string): 'up' | 'degraded' | 'down' { + const s = (state || '').toLowerCase(); + if (s === 'sent') return 'up'; + if (s === 'paused') return 'down'; + return 'degraded'; +} + +function getEtaOrAt(item: AutopilotItem): string { + if (item.eta != null) { + return `~${item.eta}s`; + } + if (item.at) { + return formatTime(item.at); + } + return ''; +}