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
+55 -1
View File
@@ -1,6 +1,7 @@
/**
* @file Settings.test.tsx
* @description The install-wide Settings pages: General and Notifications never
* @description The install-wide Settings pages: System shows the server's
* runtime, install and database facts; General and Notifications never
* read `/api/settings/info` (it COUNTs whole tables), Data reads it once and
* coalesces a burst of activity instead of re-reading per event, a purge takes
* a whole number of days and runs only after a dry-run count is confirmed, and
@@ -38,9 +39,21 @@ vi.mock("../../lib/api", async (importOriginal) => {
...actual,
api: {
pricing: { totalCost: vi.fn(async () => ({ total_cost: 1 })) },
updates: {
status: vi.fn(async () => ({
git_repo: false,
update_available: true,
install_kind: "plugin",
repo_root: "/home/u/.claude/plugins/cache/agenthub/9.9.8",
local_version: "9.9.8",
latest_release: "v9.9.9",
})),
},
settings: {
info: (...a: unknown[]) => info(...(a as [])),
cleanup: (p: { dry_run?: boolean }) => cleanup(p),
autostart: { get: vi.fn(async () => ({ available: false })) },
tray: { get: vi.fn(async () => ({ available: false })) },
clearData: () => clearData(),
gc: vi.fn(async () => ({ reaped: [], capped: [] })),
exportData: () => "/api/settings/export",
@@ -81,6 +94,47 @@ describe("Settings", () => {
expect(info).not.toHaveBeenCalled();
});
it("shows the server's runtime, install and database facts on System", async () => {
info.mockImplementationOnce(
async () =>
({
db: {
path: "/data/dashboard.db",
size: 5 * 1024 * 1024,
counts: {},
pragmas: { journal_mode: "wal" },
load_stats: { m5: 3, m15: 9, h1: 40 },
},
hooks: { installed: true, path: "", hooks: {} },
server: {
version: "1",
uptime: 3700,
node_version: "v26.0.0",
platform: "linux",
arch: "x64",
cpus: 8,
cpu_load: [0.5, 0.25, 0.125],
total_mem: 16 * 1024 ** 3,
free_mem: 4 * 1024 ** 3,
memory: { rss: 200 * 1024 ** 2, heapUsed: 50 * 1024 ** 2, heapLimit: 4 * 1024 ** 3 },
ws_connections: 2,
},
}) as never
);
render(<Settings section="about" />);
expect(await screen.findByText("/data/dashboard.db")).toBeInTheDocument();
expect(screen.getByText("1h 1m")).toBeInTheDocument();
expect(screen.getByText(/linux · x64 · Node.js v26.0.0/)).toBeInTheDocument();
expect(screen.getByText("8 cores · load 0.50 / 0.25 / 0.13")).toBeInTheDocument();
expect(screen.getByText("200.0 MB RSS · heap 50.0 MB of 4.0 GB")).toBeInTheDocument();
expect(screen.getByText("12.0 GB used of 16.0 GB")).toBeInTheDocument();
expect(screen.getByText("wal")).toBeInTheDocument();
expect(screen.getByText("3 in 5 min · 9 in 15 min · 40 in 1 h")).toBeInTheDocument();
expect(await screen.findByText("Claude Code plugin")).toBeInTheDocument();
expect(screen.getByText("v9.9.9")).toBeInTheDocument();
expect(screen.getByText("Update available")).toBeInTheDocument();
});
it("reads info once on Data and coalesces a burst of activity", async () => {
render(<Settings section="data" />);
await screen.findByText("/tmp/x.db");