/** * @file TabbyPanel.tsx * @description Expanded Tabby panel: a live status strip (live / waiting / * errored stat chips + connection state), quick navigation actions, and a * local "Ask" box. Pure presentational - all data and the ask/navigation * behavior are injected by the container. * @author Nguyễn Ngọc Trí Vĩ */ /* ============================================================================= * MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed) * ============================================================================= * **Purpose:** Tabby is the optional on-screen cat assistant — quips, intents, and lightweight event reactions layered above the dashboard chrome. * * ## Design constraints * - Local-first: no telemetry leaves the machine unless the user configures webhooks. * - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that * philosophy by degrading gracefully (empty states, stale badges, reconnect loops). * - Destructive flows stay behind explicit confirmation modals and server-side gates. * - Internationalization: user-visible strings belong in i18n JSON, not literals here. * * ## Remote data & SSH * Remote Data Sources let operators aggregate multiple machines. SSH entries describe * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * * ## Internal dependencies * - `./brain` * * ## Public surface * - `TabbyPanel` — exported API; see TSDoc on the symbol for behavior. * * ## Testing pointers * - Prefer colocated `__tests__` with Vitest + Testing Library for UI. * - Server contract changes require `npm run test:server` and OpenAPI sync. * - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`. * * ## Related docs * - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline. * - `docs/API.md` — REST reference. * - `.claude/skills/file-headers/` — mandatory `@author` header policy. * ============================================================================= */ /* ----------------------------------------------------------------------------- * EXPORT CATALOG — quick index of symbols defined below (documentation only). * ----------------------------------------------------------------------------- * **TabbyPanel** * Part of this module's public contract. Downstream imports should treat * the signature and return type as stable unless release notes say otherwise. * When behavior changes, update the `@file` overview and relevant tests. * * ----------------------------------------------------------------------------- */ import { useState, type FormEvent, type ReactNode } from "react"; import { Play, Activity, LayoutList, Bell, BellOff, Trash2, X, Send, AlertTriangle, Hourglass, Radio, type LucideIcon, } from "lucide-react"; import type { TabbyStatus } from "./brain"; interface TabbyPanelProps { status: TabbyStatus; muted: boolean; onToggleMute: () => void; onClearAlerts: () => void; onNavigate: (route: string) => void; /** Returns an answer to display, or null when the query was handed off. */ onAsk: (query: string) => string | null; onClose: () => void; } export function TabbyPanel({ status, muted, onToggleMute, onClearAlerts, onNavigate, onAsk, onClose, }: TabbyPanelProps) { const [query, setQuery] = useState(""); const [answer, setAnswer] = useState(null); const submit = (e: FormEvent) => { e.preventDefault(); const result = onAsk(query); setAnswer(result); // null means it handed off (container navigates/closes) setQuery(""); }; return (
{/* header */}
🐾 Tabby {status.connected ? "Live" : "Offline"}
{/* status stat chips */}
0 ? "accent" : "muted"} /> 0 ? "amber" : "muted"} /> 0 ? "red" : "muted"} />
{/* quick actions */}
onNavigate("/run")} /> onNavigate("/activity")} /> onNavigate("/sessions")} /> onNavigate("/sessions")} />
{/* ask */}
setQuery(e.target.value)} aria-label="Ask Tabby" />
{answer && (

{answer}

)}
); } interface Tone { wrap: string; value: string; icon: string; } const TONE_MUTED: Tone = { wrap: "border-border bg-surface-1", value: "text-fg-secondary", icon: "text-fg-muted", }; const TONES: Record = { accent: { wrap: "border-accent/30 bg-accent/10", value: "text-fg-primary", icon: "text-accent" }, amber: { wrap: "border-status-warning/30 bg-status-warning/10", value: "text-status-warning", icon: "text-status-warning", }, red: { wrap: "border-status-danger/30 bg-status-danger/10", value: "text-status-danger", icon: "text-status-danger", }, muted: TONE_MUTED, }; function StatChip({ icon: Icon, label, value, tone, }: { icon: LucideIcon; label: string; value: number; tone: string; }): ReactNode { const t = TONES[tone] ?? TONE_MUTED; return (
{value} {label}
); } function ActionButton({ icon: Icon, label, onClick, disabled, }: { icon: LucideIcon; label: string; onClick: () => void; disabled?: boolean; }) { return ( ); }