// — root of the Preact rewrite. Wires `Header` + `StatusChips` // to the real `/api/state` snapshot via `useAgentState`. Still a slice, // not the whole page — see main.tsx's file comment for what's left // (the live SSE stream, login flow, inbox/todos, term input, overflow). import { useRef, useState } from 'preact/hooks'; import type { BadgeTone } from '@hive/shared/badge.js'; import { Header } from './components/Header.js'; import { StatusChips } from './components/StatusChips.js'; import { LiveStream, type LiveStreamHandle } from './components/LiveStream.js'; import { HeaderPill } from './components/HeaderPill.js'; import { SidePanel } from './components/SidePanel.js'; import { InboxPanel } from './components/InboxPanel.js'; import { TodosPanel } from './components/TodosPanel.js'; import { TermInput } from './components/TermInput.js'; import { useAgentState } from './hooks/useAgentState.js'; import { useTodos } from './hooks/useTodos.js'; import { fmtAge, fmtTokens } from './lib/format.js'; import { resolveDashboardBase } from './lib/dashboardBase.js'; import { submitPauseResume } from './lib/pauseAction.js'; import { postModel, postEffort } from './lib/modelEffort.js'; import type { TokenUsage } from './types.js'; type OpenPanel = 'inbox' | 'todos' | null; const ALIVE_LABELS: Record = { online: { glyph: '●', text: 'alive', tone: 'positive' }, rate_limited: { glyph: '⊘', text: 'rate limited', tone: 'warning' }, needs_login_idle: { glyph: '◌', text: 'needs login', tone: 'warning' }, needs_login_in_progress: { glyph: '◌', text: 'logging in', tone: 'warning' }, }; const STATE_LABELS: Record = { idle: { glyph: '💤', text: 'idle', tone: 'neutral' }, thinking: { glyph: '🧠', text: 'thinking', tone: 'accent' }, compacting: { glyph: '📦', text: 'compacting', tone: 'warning' }, }; const STATE_TOOLTIPS: Record = { offline: 'harness unreachable or claude not logged in', idle: 'turn loop running, no claude invocation in flight', thinking: 'claude is executing the current turn', compacting: "operator-triggered /compact running on the persistent session", }; // `total` here matches app.js's `renderOneUsage` exactly: input + both // cache buckets, output_tokens deliberately excluded (it's shown in the // title breakdown, not folded into the headline number). function tokenTotal(u: TokenUsage | null): number | null { if (!u) return null; return (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0); } export function Root() { const { state, refresh } = useAgentState(); const { todos, refresh: refreshTodos } = useTodos(); const [openPanel, setOpenPanel] = useState(null); const liveStreamRef = useRef(null); const termInput = (
liveStreamRef.current?.pushNote(text)} onClear={() => liveStreamRef.current?.clear()} />
); const pills = ( <> setOpenPanel('inbox')} /> setOpenPanel('todos')} /> ); // Kept mounted regardless of `openPanel` (open/closed toggles just the // `open` prop) rather than conditionally rendering the whole // component — SidePanel's slide/fade CSS transitions only fire on a // class change after mount, not on an already-open initial render, so // mount-on-open would skip both the open AND the close animation. const panelTitle = openPanel === 'inbox' ? `inbox · ${state?.inbox.length ?? 0}` : openPanel === 'todos' ? `todos · ${todos.length}` : ''; const panel = ( setOpenPanel(null)}> {openPanel === 'inbox' ? ( ) : openPanel === 'todos' ? ( ) : null} ); if (!state) { return ( <>
{}} effort="" availableEfforts={[]} onSelectEffort={() => {}} paused={false} onTogglePause={() => {}} />
{termInput} {panel} ); } const alive = ALIVE_LABELS[state.status] ?? { glyph: '○', text: 'offline', tone: 'negative' as BadgeTone }; // Mirrors app.js's refreshState: any non-'online' status forces the // turn-state badge to 'offline' regardless of the server's last-known // turn_state (login/rate-limit takes visual priority over a stale // idle/thinking reading from before the harness went offline). const effectiveTurnState = state.status === 'online' ? state.turn_state : 'offline'; const turnDef = STATE_LABELS[effectiveTurnState] ?? { glyph: '○', text: 'offline', tone: 'negative' as BadgeTone }; const stateAge = fmtAge(Date.now() - state.turn_state_since * 1000); const ctx = tokenTotal(state.ctx_usage); const cost = tokenTotal(state.cost_usage); return ( <>
{ postModel(name).then(() => refresh()); }} effort={state.effort} availableEfforts={state.available_efforts} onSelectEffort={(level) => { postEffort(level).then(() => refresh()); }} ctxLabel={ctx !== null ? fmtTokens(ctx) : undefined} costLabel={cost !== null ? fmtTokens(cost) : undefined} paused={state.paused} onTogglePause={() => { submitPauseResume(resolveDashboardBase(state.dashboard_port), state.label, state.paused ? 'resume' : 'pause'); }} />
{termInput} {panel} ); }