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
+39 -12
View File
@@ -57,6 +57,7 @@ const {
listTree,
listTreeAll,
readFileAt,
blameFile,
readImageAt,
} = require("../lib/git-diff");
const { runGitAction } = require("../lib/git-ops");
@@ -1010,6 +1011,29 @@ router.get("/:id/git/file", async (req, res) => {
}
});
/**
* Annotate (`?path=`): who last changed each line of the working-tree file —
* git-diff.js's `blameFile`, with the whole-file read's guards (lexical lane
* guard, 403 ESECRET, a size cap). A read like `/git/file`, so unguarded too.
*/
router.get("/:id/git/blame", async (req, res) => {
const t = await gitTargetOr404(req, res);
if (!t) return;
try {
const blame = await blameFile(t.cwd, typeof req.query.path === "string" ? req.query.path : "");
res.json({ available: true, ...blame });
} catch (err) {
if (err.code === "ESECRET") {
return res.status(403).json({ error: { code: err.code, message: err.message } });
}
if (["EBADPATH", "ENOFILE", "ETOOBIG"].includes(err.code)) {
return res.status(400).json({ error: { code: err.code, message: err.message } });
}
// Not tracked (git blame has no history for it) or not a repo.
res.json({ available: false });
}
});
/**
* One image's bytes (`?path=&rev=work|HEAD|<sha>`) for the viewer's `<img>`:
* `readImageAt`'s guards (the file read's, plus an image-extension allowlist
@@ -1145,9 +1169,10 @@ router.post("/:id/git/commit-message", sameOriginGuard, async (req, res) => {
const t = await gitTargetOr404(req, res);
if (!t) return;
try {
res.json({ message: await suggestCommitMessage(t.cwd) });
const paths = Array.isArray(req.body?.paths) ? req.body.paths : undefined;
res.json({ message: await suggestCommitMessage(t.cwd, { paths }) });
} catch (err) {
const status = ["ENOSTAGED", "ENOTOOL"].includes(err.code)
const status = ["ENOSTAGED", "ENOTOOL", "EBADPATH"].includes(err.code)
? 400
: err.code === "EAI"
? 502
@@ -1200,6 +1225,7 @@ router.post("/:id/git/:action", sameOriginGuard, async (req, res) => {
"EBADSHA",
"EMERGECOMMIT",
"ENOPARENT",
"ETRACKED",
];
// A numeric code is git's own exit status (execFile reports one that way):
// git ran and refused — a rejected push, a failed credential, a diverged
@@ -1209,16 +1235,17 @@ router.post("/:id/git/:action", sameOriginGuard, async (req, res) => {
// EHUNKGONE is the same kind of answer: the tree moved under the click.
const status = badRequest.includes(err.code)
? 400
: exited || [
"EHUNKGONE",
"EGITAUTH",
"EMERGING",
"ENOMERGE",
"ECONFLICTS",
"EPUSHED",
"EEMPTY",
"ESTALE",
].includes(err.code)
: exited ||
[
"EHUNKGONE",
"EGITAUTH",
"EMERGING",
"ENOMERGE",
"ECONFLICTS",
"EPUSHED",
"EEMPTY",
"ESTALE",
].includes(err.code)
? 409
: 500;
// git's own stderr is the useful half of a failed push or pull (a