feat(pet): cleared alerts stay cleared, contextual bubbles, i18n, moments, chat window

- 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>
This commit is contained in:
2026-10-14 12:14:29 +07:00
parent 0c2f49876d
commit 0cb5e41faf
25 changed files with 2378 additions and 290 deletions
@@ -225,6 +225,43 @@ describe("clearErrors", () => {
expect(statusOf(s).liveCount).toBe(1);
expect(s.worriedUntil).toBe(0);
});
it("keeps a cleared session quiet while it stays errored", () => {
let s = initialSagiState(T0);
({ state: s } = reduceSagi(s, sessionMsg("b", "error"), T0));
s = clearErrors(s);
const again = reduceSagi(s, sessionMsg("b", "error", T0 + 1), T0 + 1);
expect(again.pulse).toBeNull();
expect(statusOf(again.state).errorCount).toBe(0);
expect(again.state.worriedUntil).toBe(0);
const ev = reduceSagi(
again.state,
{ type: "new_event", data: { session_id: "b", event_type: "toolError" }, timestamp: "" } as WSMessage,
T0 + 2
);
expect(ev.pulse).toBeNull();
expect(seedSessions(s, [{ id: "b", status: "error" }], T0).sessions.b).toBeUndefined();
});
it("alerts again once a cleared session recovers and fails anew", () => {
let s = initialSagiState(T0);
({ state: s } = reduceSagi(s, sessionMsg("b", "error"), T0));
s = clearErrors(s);
({ state: s } = reduceSagi(s, sessionMsg("b", "active"), T0));
expect(s.dismissed).not.toHaveProperty("b");
const r = reduceSagi(s, sessionMsg("b", "error"), T0);
expect(r.pulse).toBe("error");
});
});
describe("error alerts fire on the transition only", () => {
it("does not re-alert on each update of a still-errored session", () => {
let s = initialSagiState(T0);
({ state: s } = reduceSagi(s, sessionMsg("a", "error"), T0));
const r = reduceSagi(s, sessionMsg("a", "error"), T0 + 10_000);
expect(r.pulse).toBeNull();
expect(statusOf(r.state).errorCount).toBe(1);
});
});
// Reference the imported constant so it is exercised and tsc-clean.