0cb5e41faf
- Fix: an errored session alerted on every update, and Clear alerts only dropped it in memory, so its next update (or a reload) brought the alert straight back. An error now alerts on the transition into error only; cleared sessions are remembered (agenthub.pet.dismissedErrors) and stay quiet until they recover and fail anew. - Quips, greetings, notifications and Ask answers move to i18n (EN/VI); Ask understands Vietnamese keywords and names the sessions it counts. - The pet goes by the worn pet's name; click it in the panel to rename (agenthub.pet.names, synced to the account). - Bubbles name the session they are about, with an Open button (never a resume); a burst of the same event coalesces into one count. - Moments: a new session waves, a finish hops, a subagent peeks, a wait taps, an error flinches — on Sagi, Codex sprites and still images. Idle fidgets, a once-per-session greeting, a "Needs you" list, an amber waiting dot. - Pat (heart button, Ask, or stroking the pet) reacts on the pet itself, so it shows while the panel or chat is open. - Panel drops Run Claude / Activity / Sessions / Errored. - Ask hands off to a chat window (PetChat) on the default AI tool (Claude/Codex): transcript as bubbles, follow-ups and CLI menus through an off-screen attached terminal; no new server route. Verified on HEAD + this diff in a clean worktree: tsc -b, the full client suite (1334/1335; the one failure, ConversationView.follow, is flaky under load and passes 3/3 alone) and server me-prefs.test.js. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
122 lines
5.1 KiB
TypeScript
122 lines
5.1 KiB
TypeScript
/**
|
||
* @file quips.ts
|
||
* @description Sagi's personality: pools of short phrases keyed by pulse/mood
|
||
* (and greetings, pat reactions), read from the `settings:sagi.*` i18n arrays
|
||
* so they follow the UI language, plus a deterministic-by-injection picker.
|
||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||
*/
|
||
/* =============================================================================
|
||
* MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed)
|
||
* =============================================================================
|
||
* **Purpose:** Sagi is the optional on-screen mascot 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`
|
||
* - `../../i18n`
|
||
*
|
||
* ## Public surface
|
||
* - `QuipKey` — exported API; see TSDoc on the symbol for behavior.
|
||
* - `pickQuip` — exported API; see TSDoc on the symbol for behavior.
|
||
* - `pickLine` — exported API; see TSDoc on the symbol for behavior.
|
||
* - `greetingKey` — exported API; see TSDoc on the symbol for behavior.
|
||
* - `ALL_QUIP_KEYS` — 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.
|
||
*
|
||
* ## Related docs
|
||
* - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline.
|
||
* - `docs/API.md` — REST reference.
|
||
* - `skills/file-headers/` — mandatory `@author` header policy.
|
||
* ============================================================================= */
|
||
/* -----------------------------------------------------------------------------
|
||
* EXPORT CATALOG — quick index of symbols defined below (documentation only).
|
||
* -----------------------------------------------------------------------------
|
||
* **QuipKey**
|
||
* 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.
|
||
*
|
||
* **pickQuip**
|
||
* 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.
|
||
*
|
||
* **ALL_QUIP_KEYS**
|
||
* 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 i18n from "../../i18n";
|
||
import type { Mood, SagiPulse } from "./brain";
|
||
|
||
export type QuipKey = NonNullable<SagiPulse> | Mood;
|
||
|
||
export const ALL_QUIP_KEYS: QuipKey[] = [
|
||
// Pulses (event-driven, transient bubbles)
|
||
"session_done",
|
||
"session_start",
|
||
"subagent_spawn",
|
||
"waiting",
|
||
"error",
|
||
"run_done",
|
||
// Moods (steady-state flavor)
|
||
"disconnected",
|
||
"worried",
|
||
"stuck",
|
||
"happy",
|
||
"thinking",
|
||
"watching",
|
||
"sleeping",
|
||
"idle",
|
||
];
|
||
const KNOWN = new Set<string>(ALL_QUIP_KEYS);
|
||
|
||
/**
|
||
* Pick one line from an i18n string array under `settings:sagi.<path>`.
|
||
* `rand` is injectable for deterministic tests. Returns "" for a missing or
|
||
* empty pool (never throws).
|
||
*/
|
||
export function pickLine(
|
||
path: string,
|
||
rand: () => number = Math.random,
|
||
vars?: Record<string, unknown>
|
||
): string {
|
||
const pool = i18n.t(`settings:sagi.${path}`, { returnObjects: true, ...vars }) as unknown;
|
||
if (!Array.isArray(pool) || pool.length === 0) return "";
|
||
const i = Math.min(pool.length - 1, Math.max(0, Math.floor(rand() * pool.length)));
|
||
const line = pool[i];
|
||
return typeof line === "string" ? line : "";
|
||
}
|
||
|
||
/**
|
||
* Pick a quip for a key, in the UI language. Returns "" only for an unknown
|
||
* key (never throws).
|
||
*/
|
||
export function pickQuip(key: QuipKey, rand: () => number = Math.random): string {
|
||
return KNOWN.has(key) ? pickLine(`quips.${key}`, rand) : "";
|
||
}
|
||
|
||
/** Which greeting pool fits the hour: morning 5–11, afternoon 12–17, evening 18–22, else night. */
|
||
export function greetingKey(hour: number): "morning" | "afternoon" | "evening" | "night" {
|
||
if (hour >= 5 && hour < 12) return "morning";
|
||
if (hour >= 12 && hour < 18) return "afternoon";
|
||
if (hour >= 18 && hour < 23) return "evening";
|
||
return "night";
|
||
}
|