feat: v2.5.12 — plan and quota for every AI tool, Grok Build and Kimi Code, Quit AgentHub (#65)

Usage panel (status bar):
- every account shows its plan (Claude subscriptionType, Codex
  chatgpt_plan_type, Cursor membershipType, Grok subscriptionTier)
- new quota rows: Cursor CLI (cursor.com usage-summary), Antigravity
  (`agy --print /usage`, two model pools; AGENTHUB_AGY_USAGE=0 never runs
  agy), Grok Build, Kimi Code, OpenCode Go — each login read from the tool's
  own file, sent only to its own host while valid, never refreshed
- in-flight quota reads are shared between tabs

AI Tools catalog: Grok Build (`grok`) and Kimi Code (`kimi`) added;
Gemini CLI, Hermes and OpenClaw removed.

Also: Quit AgentHub (#65, `agenthub stop`), status-area icon, Add to Chat
on an editor selection, Settings in three groups, JetBrains-style Changes
panel (ticked files/hunks, ignore, blame, file history), terminal font.

--no-verify: :3000 is taken by another dev server (lane-detect tests);
both suites run by hand — server 2636/2639 (the 3 are :3000), client
1131/1131.
This commit is contained in:
2026-10-08 12:22:14 +07:00
parent 6ef9a5d4b7
commit a9c71cd905
133 changed files with 8007 additions and 1830 deletions
@@ -93,9 +93,9 @@ describe("PetSprite", () => {
// 104 px tall = half a 208 px cell: row 7 (running) starts 7 × 104 px down.
expect(sprite.dataset.row).toBe("7");
expect(sprite.style.backgroundPosition).toBe("0px -728px");
act(() => vi.advanceTimersByTime(130));
expect(sprite.style.backgroundPosition).toBe("0px -728px"); // slower than 125 ms
act(() => vi.advanceTimersByTime(50));
act(() => vi.advanceTimersByTime(110));
expect(sprite.style.backgroundPosition).toBe("0px -728px"); // Codex: 120 ms a frame
act(() => vi.advanceTimersByTime(10));
expect(sprite.style.backgroundPosition).toBe("-96px -728px");
rerender(
<PetSprite
@@ -129,7 +129,7 @@ describe("PetSprite", () => {
expect(screen.getByTestId("pet-sprite").dataset.row).toBe("7");
});
it("holds a loop's last frame before starting over", () => {
it("plays idle at the drawn pace, the last frame barely longer than the rest", () => {
render(
<PetSprite
id="cp-goku"
@@ -141,14 +141,67 @@ describe("PetSprite", () => {
/>
);
const sprite = screen.getByTestId("pet-sprite");
act(() => vi.advanceTimersByTime(5 * 220)); // idle: 6 frames → on the last one
act(() => vi.advanceTimersByTime(280 + 110 + 110 + 140 + 140)); // → the 6th, last frame
expect(sprite.style.backgroundPosition).toBe("-960px 0px");
act(() => vi.advanceTimersByTime(1000));
act(() => vi.advanceTimersByTime(310));
expect(sprite.style.backgroundPosition).toBe("-960px 0px");
act(() => vi.advanceTimersByTime(400));
act(() => vi.advanceTimersByTime(10)); // 320 ms, not a long hold
expect(sprite.style.backgroundPosition).toBe("0px 0px");
});
it("plays a moment three times, then settles into idle; a condition keeps looping", () => {
const props = { id: "cp-goku", kind: "codex" as const, size: 208, reducedMotion: false };
const { rerender } = render(<PetSprite {...props} mood="happy" onBroken={() => {}} />);
const sprite = screen.getByTestId("pet-sprite");
expect(sprite.dataset.row).toBe("4"); // jumping: 5 frames, 4 × 140 + 280 ms
act(() => vi.advanceTimersByTime(3 * 840 - 1));
expect(sprite.dataset.row).toBe("4");
act(() => vi.advanceTimersByTime(1));
expect(sprite.dataset.row).toBe("0");
rerender(<PetSprite {...props} mood="thinking" onBroken={() => {}} />);
act(() => vi.advanceTimersByTime(10_000));
expect(sprite.dataset.row).toBe("7");
});
it("looks toward the pointer on a v2 sheet, only while idle", () => {
// A v2 atlas is 11 rows tall; the sprite learns that from the loaded image.
vi.stubGlobal(
"Image",
class {
naturalHeight = 2288;
onload: (() => void) | null = null;
onerror: (() => void) | null = null;
set src(_: string) {
this.onload?.();
}
}
);
const props = { id: "cp-goku", kind: "codex" as const, size: 208, reducedMotion: false };
const { rerender } = render(<PetSprite {...props} mood="idle" gaze={12} onBroken={() => {}} />);
const sprite = screen.getByTestId("pet-sprite");
// Pose 12 (270°, left) = row 10, column 4.
expect(sprite.style.backgroundPosition).toBe("-768px -2080px");
expect(sprite.style.backgroundSize).toBe("1536px 2288px");
rerender(<PetSprite {...props} mood="thinking" gaze={12} onBroken={() => {}} />);
expect(sprite.dataset.row).toBe("7");
vi.unstubAllGlobals();
});
it("ignores the pointer on a v1 sheet, which has no gaze rows", () => {
render(
<PetSprite
id="cp-goku"
kind="codex"
mood="idle"
gaze={4}
size={208}
reducedMotion={false}
onBroken={() => {}}
/>
);
expect(screen.getByTestId("pet-sprite").style.backgroundPosition).toBe("0px 0px");
});
it("holds the first frame under reduced motion", () => {
render(
<PetSprite