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
@@ -0,0 +1,78 @@
/**
* @file intents.test.ts
* @description Unit tests for Sagi's intent matcher — natural-language queries mapped to dashboard navigation and status intents.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { describe, it, expect } from "vitest";
import { matchIntent } from "../intents";
import type { SagiStatus } from "../brain";
const status = (over: Partial<SagiStatus> = {}): SagiStatus => ({
liveCount: 0,
waitingCount: 0,
errorCount: 0,
connected: true,
...over,
});
describe("matchIntent", () => {
it("reports live sessions", () => {
const r = matchIntent("what's running?", status({ liveCount: 3 }));
expect(r).toEqual({ kind: "answer", text: expect.stringContaining("3 sessions live") });
});
it("singularizes correctly", () => {
const r = matchIntent("anything active", status({ liveCount: 1 }));
expect(r.kind).toBe("answer");
if (r.kind === "answer") expect(r.text).toContain("1 session live");
});
it("says all quiet when nothing live", () => {
const r = matchIntent("what is running", status());
if (r.kind === "answer") expect(r.text).toContain("nothing's running");
});
it("reports errors and prioritizes error intent over live", () => {
const r = matchIntent("any failed runs?", status({ liveCount: 2, errorCount: 1 }));
if (r.kind === "answer") expect(r.text).toContain("1 session errored");
});
it("clean when no errors", () => {
const r = matchIntent("are there errors", status({ liveCount: 2 }));
if (r.kind === "answer") expect(r.text).toContain("all clean");
});
it("gives a combined status summary", () => {
const r = matchIntent(
"status",
status({ liveCount: 2, waitingCount: 1, errorCount: 1, connected: true })
);
if (r.kind === "answer") expect(r.text).toBe("2 live · 1 waiting · 1 errored · connected.");
});
it("reports sessions waiting on the user", () => {
const r = matchIntent("anything waiting on me?", status({ liveCount: 2, waitingCount: 1 }));
if (r.kind === "answer") expect(r.text).toContain("1 session waiting on you");
});
it("reflects offline in summary", () => {
const r = matchIntent("overview", status({ connected: false }));
if (r.kind === "answer") expect(r.text).toContain("offline");
});
it("explains itself on help", () => {
const r = matchIntent("help", status());
if (r.kind === "answer") expect(r.text.toLowerCase()).toContain("watch your sessions");
});
it("empty query nudges the user", () => {
const r = matchIntent(" ", status());
expect(r.kind).toBe("answer");
});
it("hands unknown questions to Claude, preserving original casing", () => {
const r = matchIntent("Refactor my auth module", status());
expect(r).toEqual({ kind: "handoff", prompt: "Refactor my auth module" });
});
});