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
@@ -1,6 +1,7 @@
/**
* @file AppearanceSettings.tsx
* @description Settings → Appearance (`/settings/appearance`): the interface language, the
* terminal font (`lib/terminalFont.ts`, with a preview line), the
* Dark / Light / System mode switch and one named-theme picker per mode, and the
* corner pet (`PetSettings`, deep-linked as `#pet`) — the old General and Pet
* pages redirect here. Theme state is in `hooks/useTheme.ts`, palettes in
@@ -11,6 +12,7 @@
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Monitor, Moon, RotateCcw, Sun } from "lucide-react";
import { SettingsRow, SettingsSection, SettingsSegmented } from "./SettingsRow";
@@ -18,6 +20,12 @@ import { PetSettings } from "./PetSettings";
import { THEMES, useTheme, type Theme, type ThemeMode } from "../../hooks/useTheme";
import { confirmDialog } from "../../lib/confirm";
import { toast } from "../../lib/toast";
import {
TERMINAL_FONTS,
readTerminalFont,
writeTerminalFont,
type TerminalFont,
} from "../../lib/terminalFont";
const THEME_LABELS: Record<string, string> = {
darcula: "Darcula",
@@ -65,6 +73,11 @@ function Swatch({ mode, name }: { mode: Theme; name: string }) {
export function AppearanceSettings() {
const { t, i18n } = useTranslation("settings");
const { mode, theme, themeFor, setMode, setThemeFor } = useTheme();
const [termFont, setTermFont] = useState(readTerminalFont);
const pickTermFont = (font: TerminalFont) => {
writeTerminalFont(font);
setTermFont(font);
};
const reset = async () => {
const ok = await confirmDialog({
@@ -102,6 +115,37 @@ export function AppearanceSettings() {
/>
</SettingsSection>
<SettingsSection
id="terminal-font"
title={t("terminalFont.title")}
description={t("terminalFont.description")}
>
<SettingsRow
label={t("terminalFont.label")}
description={termFont === "system" ? t("terminalFont.systemNote") : undefined}
control={
<SettingsSegmented
ariaLabel={t("terminalFont.title")}
value={termFont}
onChange={pickTermFont}
options={(Object.keys(TERMINAL_FONTS) as TerminalFont[]).map((f) => ({
value: f,
label: t(`terminalFont.names.${f}`),
}))}
/>
}
/>
<p
data-testid="terminal-font-preview"
aria-label={t("terminalFont.preview")}
className="mt-2 rounded-md border border-border bg-surface-0 px-3 py-2 text-[13px] text-fg-primary"
// No ligatures: xterm draws one glyph per cell, so the terminal has none.
style={{ fontFamily: TERMINAL_FONTS[termFont], fontVariantLigatures: "none" }}
>
❯ Tiếng Việt: ấ ế ố ồ ủ ữ — const x = {"{ ok: true }"}; 0O il1|
</p>
</SettingsSection>
<section id="theme" className="card p-5 space-y-5 scroll-mt-24">
<h3 className="text-sm font-semibold text-fg-primary">{t("appearance.theme")}</h3>
<SettingsRow