diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8a98fef..7b19bf6 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -63,7 +63,7 @@ Architectural overview and technical reference for the Agent Dashboard system, c - [State Management](#state-management) - [Browser Notification System](#browser-notification-system) - [Update Notifier Subsystem](#update-notifier-subsystem) -- [Tabby Companion Subsystem](#tabby-companion-subsystem) +- [Sagi Companion Subsystem](#sagi-companion-subsystem) - [VS Code Extension Architecture](#vs-code-extension-architecture) - [Security Considerations](#security-considerations) - [Performance Characteristics](#performance-characteristics) @@ -576,7 +576,7 @@ graph TD STC[StatCard] STB[StatusBadge] ES[EmptyState] - TB["Tabby/
(floating cat companion)"] + TB["Sagi/
(floating mascot companion)"] end D --> STC & AGC & STB @@ -1003,13 +1003,13 @@ graph TD BC --> WS WS --> EB EB --> SUB1 & SUB2 & SUB3 & SUB4 & SUB5 & SUB6 - EB --> SUB7["Tabby companion subscriber"] + EB --> SUB7["Sagi companion subscriber"] style BC fill:#10b981,stroke:#34d399,color:#fff style EB fill:#f59e0b,stroke:#fbbf24,color:#000 ``` -The **Tabby companion** (see [Tabby Companion Subsystem](#tabby-companion-subsystem)) is an additional read-only `eventBus` subscriber. It consumes the existing message envelope above and introduces **no new WebSocket message types** and no protocol changes. +The **Sagi companion** (see [Sagi Companion Subsystem](#sagi-companion-subsystem)) is an additional read-only `eventBus` subscriber. It consumes the existing message envelope above and introduces **no new WebSocket message types** and no protocol changes. ### Client Reconnection @@ -1847,7 +1847,7 @@ graph TD subgraph "App-Level Hooks" NOTIF_H["useNotifications
reads prefs, fires
browser notifications"] - TABBY_H["useTabbyBrain
derives cat mood +
speech from WS stream"] + SAGI_H["useSagiBrain
derives mascot mood +
speech from WS stream"] end subgraph "Page State" @@ -1865,9 +1865,9 @@ graph TD WSM --> EB EB --> US1 & US2 & US3 & US4 & US5 & US6 & US8 & US7 EB --> NOTIF_H - EB --> TABBY_H + EB --> SAGI_H LS --> NOTIF_H - LS --> TABBY_H + LS --> SAGI_H LS --> US7 ``` @@ -2135,9 +2135,9 @@ The detection layer carries all of the signal value: the dashboard tells the use --- -## Tabby Companion Subsystem +## Sagi Companion Subsystem -Tabby is a **client-only** floating cat companion that reacts to live session activity. It is purely additive UI: there is **no server/backend code**, **no new API routes**, **no new WebSocket message types**, and **no database changes**. Tabby reuses the existing real-time event stream (the same `eventBus` every page already consumes) and the existing **Run** page for its "ask a real question" path. The entire subsystem lives under `client/src/components/Tabby/`. +Sagi is a **client-only** floating mascot companion that reacts to live session activity. It is purely additive UI: there is **no server/backend code**, **no new API routes**, **no new WebSocket message types**, and **no database changes**. Sagi reuses the existing real-time event stream (the same `eventBus` every page already consumes) and the existing **Run** page for its "ask a real question" path. The entire subsystem lives under `client/src/components/Sagi/`. The design follows a strict **pure-core / hook / presentational** split: a framework-free brain (a `WSMessage` reducer plus a mood state machine with an injected clock and zero side effects) is fully unit-tested in isolation, a single React hook is the only consumer of the global `eventBus` and the only owner of timers and side effects, and the SVG/markup components are pure presentational views driven by props. @@ -2146,22 +2146,22 @@ The design follows a strict **pure-core / hook / presentational** split: a frame ```mermaid graph TD subgraph "Pure Core (framework-free, unit-tested)" - BRAIN["brain.ts
reduceTabby reducer +
deriveMood state machine
(injected clock, no side effects)"] + BRAIN["brain.ts
reduceSagi reducer +
deriveMood state machine
(injected clock, no side effects)"] INTENTS["intents.ts
local Q&A over cached status;
unmatched → Run handoff"] QUIPS["quips.ts
mood → phrase pools"] PREFS["prefs.ts
localStorage enabled/muted
(cross-tab sync)"] end subgraph "Hook (only eventBus consumer)" - HOOK["useTabbyBrain.ts
wires brain to real timers
(idle/sleep/stuck), speech-bubble
queue, mute, clear-alerts"] + HOOK["useSagiBrain.ts
wires brain to real timers
(idle/sleep/stuck), speech-bubble
queue, mute, clear-alerts"] end subgraph "Presentational (pure)" - SHELL["Tabby.tsx
shell: open/closed state,
⌘B / Esc, reduced-motion,
navigation"] - AVATAR["CatAvatar.tsx
SVG cat; data-mood drives CSS;
cursor-tracking pupils"] + SHELL["Sagi.tsx
shell: open/closed state,
⌘B / Esc, reduced-motion,
navigation"] + AVATAR["SagiAvatar.tsx
SVG mascot; data-mood drives CSS;
cursor-tracking pupils"] BUBBLE["SpeechBubble.tsx
bubble"] - PANEL["TabbyPanel.tsx
status + quick actions + Ask box"] - CSS["tabby.css
keyframes + per-mood expressions"] + PANEL["SagiPanel.tsx
status + quick actions + Ask box"] + CSS["sagi.css
keyframes + per-mood expressions"] end BUS["lib/eventBus.ts
(existing WS stream)"] @@ -2186,44 +2186,44 @@ graph TD flowchart LR WSS["Server WebSocket
broadcast"] --> UWS["useWebSocket"] UWS --> PUB["eventBus.publish"] - PUB --> SUB["useTabbyBrain
(subscriber)"] + PUB --> SUB["useSagiBrain
(subscriber)"] SUB --> DERIVED["derived state
{ mood, status, bubble }"] - DERIVED --> AVATAR["CatAvatar"] + DERIVED --> AVATAR["SagiAvatar"] DERIVED --> BUBBLE["SpeechBubble"] - DERIVED --> PANEL["TabbyPanel"] + DERIVED --> PANEL["SagiPanel"] PANEL -->|"unmatched Ask"| RUN["/run?prompt=…
(existing Run page)"] style PUB fill:#f59e0b,stroke:#fbbf24,color:#000 style RUN fill:#10b981,stroke:#34d399,color:#fff ``` -The mood state machine in `deriveMood` resolves to a single expression using a fixed priority order: `disconnected > worried > stuck > happy > thinking > watching > sleeping > idle`. The resolved mood is written to a `data-mood` attribute on the SVG cat, and `tabby.css` maps each mood to its keyframe animation and expression. +The mood state machine in `deriveMood` resolves to a single expression using a fixed priority order: `disconnected > worried > stuck > happy > thinking > watching > sleeping > idle`. The resolved mood is written to a `data-mood` attribute on the SVG mascot, and `sagi.css` maps each mood to its keyframe animation and expression. ### Component Responsibilities | Component | Responsibility | | --- | --- | -| **`brain.ts`** | Pure, framework-free core. Exposes a `WSMessage` reducer (`reduceTabby`) and a mood state machine (`deriveMood`) with the priority order `disconnected > worried > stuck > happy > thinking > watching > sleeping > idle`. The clock is injected and there are zero side effects, so the brain is fully unit-tested in isolation. | -| **`useTabbyBrain.ts`** | The **only** consumer of the global `eventBus`. Wires the pure brain to real timers (idle / sleep / stuck), the speech-bubble queue, mute, and clear-alerts. Produces the derived `{ mood, status, bubble }` the presentational components render. | -| **`CatAvatar.tsx`** | Pure presentational SVG cat. The `data-mood` attribute drives CSS; pupils track the cursor. | +| **`brain.ts`** | Pure, framework-free core. Exposes a `WSMessage` reducer (`reduceSagi`) and a mood state machine (`deriveMood`) with the priority order `disconnected > worried > stuck > happy > thinking > watching > sleeping > idle`. The clock is injected and there are zero side effects, so the brain is fully unit-tested in isolation. | +| **`useSagiBrain.ts`** | The **only** consumer of the global `eventBus`. Wires the pure brain to real timers (idle / sleep / stuck), the speech-bubble queue, mute, and clear-alerts. Produces the derived `{ mood, status, bubble }` the presentational components render. | +| **`SagiAvatar.tsx`** | Pure presentational SVG mascot. The `data-mood` attribute drives CSS; pupils track the cursor. | | **`SpeechBubble.tsx`** | Pure presentational speech bubble. | -| **`TabbyPanel.tsx`** | Pure presentational panel: status readout + quick actions + the Ask box. | -| **`Tabby.tsx`** | Shell component. Mounted once in `client/src/components/Layout.tsx` as a sibling of `UpdateNotifier`. Owns open/closed state, the `⌘B` / `Esc` shortcuts, reduced-motion detection, and navigation. | +| **`SagiPanel.tsx`** | Pure presentational panel: status readout + quick actions + the Ask box. | +| **`Sagi.tsx`** | Shell component. Mounted once in `client/src/components/Layout.tsx` as a sibling of `UpdateNotifier`. Owns open/closed state, the `⌘B` / `Esc` shortcuts, reduced-motion detection, and navigation. | | **`intents.ts`** | Pure local Q&A over the cached status snapshot. Queries that don't match a local intent become a handoff to the existing Run page via `/run?prompt=…`. | | **`quips.ts`** | Pure mood → phrase pools. | | **`prefs.ts`** | `localStorage`-backed enabled / muted preferences with cross-tab sync. | -| **`tabby.css`** | Keyframes and per-mood expressions; selected via the `data-mood` attribute. | +| **`sagi.css`** | Keyframes and per-mood expressions; selected via the `data-mood` attribute. | ### Touchpoints Outside the Folder -Tabby's only contact with the rest of the app is four light, additive touchpoints — nothing in the server, database, or WebSocket protocol changes: +Sagi's only contact with the rest of the app is four light, additive touchpoints — nothing in the server, database, or WebSocket protocol changes: | File | Touchpoint | | --- | --- | -| `client/src/components/Layout.tsx` | Mounts `` once, as a sibling of ``. | -| `client/src/pages/Settings.tsx` | On/off toggle wired to `tabbyPrefs` (`localStorage`). | -| `client/src/pages/Workspace.tsx` | Reads `?prompt=` to prefill the prompt box for Tabby's Ask handoff. | -| `client/src/i18n/locales/{en,zh,vi,ko}/settings.json` | `tabby.*` strings for the Settings toggle (en / zh / vi / ko). | +| `client/src/components/Layout.tsx` | Mounts `` once, as a sibling of ``. | +| `client/src/pages/Settings.tsx` | On/off toggle wired to `sagiPrefs` (`localStorage`). | +| `client/src/pages/Workspace.tsx` | Reads `?prompt=` to prefill the prompt box for Sagi's Ask handoff. | +| `client/src/i18n/locales/{en,vi}/settings.json` | `sagi.*` strings for the Settings toggle (zh / ko fall back to the hardcoded English defaults). | --- diff --git a/client/src/components/Layout.tsx b/client/src/components/Layout.tsx index 9b9c46e..28424db 100644 --- a/client/src/components/Layout.tsx +++ b/client/src/components/Layout.tsx @@ -1,7 +1,7 @@ /** * @file Layout.tsx * @description Application shell that frames every authenticated route: persistent - * sidebar, main content column, update notifier, and the Tabby assistant overlay. + * sidebar, main content column, update notifier, and the Sagi assistant overlay. * The layout is the single parent route in {@link App} — child pages render inside * React Router's `` so navigation never remounts chrome. * @@ -38,7 +38,7 @@ * ## Internal dependencies * - `./Sidebar` * - `./UpdateNotifier` - * - `./Tabby/Tabby` + * - `./Sagi/Sagi` * * ## Public surface * - `Layout` — exported API; see TSDoc on the symbol for behavior. @@ -68,7 +68,7 @@ import { Outlet } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Sidebar, SIDEBAR_STORAGE_KEY, loadCollapsed } from "./Sidebar"; import { UpdateNotifier } from "./UpdateNotifier"; -import { Tabby } from "./Tabby/Tabby"; +import { Sagi } from "./Sagi/Sagi"; /** Props for {@link Layout}. */ interface LayoutProps { @@ -100,7 +100,7 @@ export function Layout({ wsConnected }: LayoutProps) { {t("skipToContent")} - +
(null); const [style, setStyle] = useState({ visibility: "hidden" }); @@ -129,8 +129,8 @@ function TabbyFlyout({ anchor, children }: { anchor: Anchor; children: ReactNode let left = anchor.side === "left" ? anchor.left : anchor.left + anchor.size - w; left = Math.min(vw - w - VIEWPORT_MARGIN, Math.max(VIEWPORT_MARGIN, left)); - // Vertical: prefer above the cat (feels natural). Only drop below when - // there isn't room above - i.e. the cat is near the top edge. + // Vertical: prefer above the mascot (feels natural). Only drop below when + // there isn't room above - i.e. the mascot is near the top edge. const above = anchor.top - h - FLYOUT_GAP; const below = anchor.top + anchor.size + FLYOUT_GAP; let top = above >= VIEWPORT_MARGIN ? above : below; @@ -153,22 +153,22 @@ function TabbyFlyout({ anchor, children }: { anchor: Anchor; children: ReactNode }, [place]); return ( -
+
{children}
); } -export function Tabby() { - const [enabled, setEnabled] = useState(() => tabbyPrefs.getEnabled()); +export function Sagi() { + const [enabled, setEnabled] = useState(() => sagiPrefs.getEnabled()); const [open, setOpen] = useState(false); const reducedMotion = usePrefersReducedMotion(); const navigate = useNavigate(); - const brain = useTabbyBrain(); - const place = useTabbyPosition(); + const brain = useSagiBrain(); + const place = useSagiPosition(); // Keep enabled in sync with Settings / other tabs. - useEffect(() => tabbyPrefs.subscribe(() => setEnabled(tabbyPrefs.getEnabled())), []); + useEffect(() => sagiPrefs.subscribe(() => setEnabled(sagiPrefs.getEnabled())), []); // ⌘B / Ctrl+B toggles the panel; Esc closes it. useEffect(() => { @@ -218,10 +218,10 @@ export function Tabby() { return ( <> - {/* Flyouts are hidden while dragging so they don't chase the cat. */} + {/* Flyouts are hidden while dragging so they don't chase the mascot. */} {!place.dragging && open && ( - - + setOpen(false)} /> - + )} {!place.dragging && !open && brain.bubble && ( - + - + )} @@ -189,10 +189,10 @@ export function TabbyPanel({
setQuery(e.target.value)} - aria-label="Ask Tabby" + aria-label="Ask Sagi" />