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 && (
-
+
-
+
)}