feat(companion): rename Tabby companion to Sagi, replace cat avatar with mascot

Full rename across files, identifiers, CSS classes, and user-facing strings
(en/vi locales, ARCHITECTURE.md). Replaces the hand-drawn animated cat
avatar with the pig-superhero mascot artwork, split into parts to keep
per-eye cursor tracking and blink animation working; whole-mascot mood
transforms (breathe/bob/shake/tilt) and new zzz/bang/sparkle overlays
carry the rest of the mood expression since the traced art has no shared
palette to key off of.
This commit is contained in:
2026-08-19 11:07:59 +07:00
parent dce54c8a3a
commit 4cc39f5069
25 changed files with 1390 additions and 1041 deletions
+30 -30
View File
@@ -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/<br/>(floating cat companion)"]
TB["Sagi/<br/>(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<br/>reads prefs, fires<br/>browser notifications"]
TABBY_H["useTabbyBrain<br/>derives cat mood +<br/>speech from WS stream"]
SAGI_H["useSagiBrain<br/>derives mascot mood +<br/>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<br/>reduceTabby reducer +<br/>deriveMood state machine<br/>(injected clock, no side effects)"]
BRAIN["brain.ts<br/>reduceSagi reducer +<br/>deriveMood state machine<br/>(injected clock, no side effects)"]
INTENTS["intents.ts<br/>local Q&A over cached status;<br/>unmatched → Run handoff"]
QUIPS["quips.ts<br/>mood → phrase pools"]
PREFS["prefs.ts<br/>localStorage enabled/muted<br/>(cross-tab sync)"]
end
subgraph "Hook (only eventBus consumer)"
HOOK["useTabbyBrain.ts<br/>wires brain to real timers<br/>(idle/sleep/stuck), speech-bubble<br/>queue, mute, clear-alerts"]
HOOK["useSagiBrain.ts<br/>wires brain to real timers<br/>(idle/sleep/stuck), speech-bubble<br/>queue, mute, clear-alerts"]
end
subgraph "Presentational (pure)"
SHELL["Tabby.tsx<br/>shell: open/closed state,<br/>⌘B / Esc, reduced-motion,<br/>navigation"]
AVATAR["CatAvatar.tsx<br/>SVG cat; data-mood drives CSS;<br/>cursor-tracking pupils"]
SHELL["Sagi.tsx<br/>shell: open/closed state,<br/>⌘B / Esc, reduced-motion,<br/>navigation"]
AVATAR["SagiAvatar.tsx<br/>SVG mascot; data-mood drives CSS;<br/>cursor-tracking pupils"]
BUBBLE["SpeechBubble.tsx<br/>bubble"]
PANEL["TabbyPanel.tsx<br/>status + quick actions + Ask box"]
CSS["tabby.css<br/>keyframes + per-mood expressions"]
PANEL["SagiPanel.tsx<br/>status + quick actions + Ask box"]
CSS["sagi.css<br/>keyframes + per-mood expressions"]
end
BUS["lib/eventBus.ts<br/>(existing WS stream)"]
@@ -2186,44 +2186,44 @@ graph TD
flowchart LR
WSS["Server WebSocket<br/>broadcast"] --> UWS["useWebSocket"]
UWS --> PUB["eventBus.publish"]
PUB --> SUB["useTabbyBrain<br/>(subscriber)"]
PUB --> SUB["useSagiBrain<br/>(subscriber)"]
SUB --> DERIVED["derived state<br/>{ mood, status, bubble }"]
DERIVED --> AVATAR["CatAvatar"]
DERIVED --> AVATAR["SagiAvatar"]
DERIVED --> BUBBLE["SpeechBubble"]
DERIVED --> PANEL["TabbyPanel"]
DERIVED --> PANEL["SagiPanel"]
PANEL -->|"unmatched Ask"| RUN["/run?prompt=…<br/>(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 `<Tabby />` once, as a sibling of `<UpdateNotifier />`. |
| `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 `<Sagi />` once, as a sibling of `<UpdateNotifier />`. |
| `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). |
---