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
+158 -90
View File
@@ -1,6 +1,6 @@
/**
* @file Settings.tsx
* @description The install-wide settings, as three pages of the Settings nav (`SettingsHub.tsx`): `section="data"` — total cost, database overview, workspace housekeeping (dry run first, confirm before a real run), import, remote sources, push ingest (`components/settings/PushIngestSettings.tsx`), privacy (`components/settings/PrivacySettings.tsx`), export, this browser's preferences backup, session cleanup (whole days ≥ 1, a dry-run count in its confirm) and the danger zone (typed confirmation) —, `section="about"` — the start-at-login switch (`AutostartSettings`) and system information — and `section="notifications"` — browser notifications (per-browser, with a reset) and alerts (server-wide). Server info (`/api/settings/info`) is read only by Data and About, refreshed on activity at most every 10 s. Language, theme and the pet live on the Appearance page (`components/settings/AppearanceSettings.tsx`); per-tool settings live in that tool's page under `/settings/agents/<id>`.
* @description The install-wide settings, as three pages of the Settings nav (`SettingsHub.tsx`): `section="data"` — total cost, database overview, workspace housekeeping (dry run first, confirm before a real run), import, remote sources, push ingest (`components/settings/PushIngestSettings.tsx`), privacy (`components/settings/PrivacySettings.tsx`), export, this browser's preferences backup, session cleanup (whole days ≥ 1, a dry-run count in its confirm) and the danger zone (typed confirmation) —, `section="about"` (the System page) — the start-at-login switch and Quit (`AutostartSettings`), then the server process (release, uptime, platform, CPU, memory, live connections), how AgentHub is installed (the update check's cached status) and the database file — and `section="notifications"` — browser notifications (per-browser, with a reset) and alerts (server-wide). Server info (`/api/settings/info`) is read only by Data and About, refreshed on activity at most every 10 s. Language, theme and the pet live on the Appearance page (`components/settings/AppearanceSettings.tsx`); per-tool settings live in that tool's page under `/settings/agents/<id>`.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
/* =============================================================================
@@ -55,7 +55,7 @@
* ----------------------------------------------------------------------------- */
import { Checkbox } from "../components/Checkbox";
import { useEffect, useRef, useState, useCallback } from "react";
import { useEffect, useRef, useState, useCallback, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import {
DollarSign,
@@ -64,7 +64,6 @@ import {
HardDrive,
AlertTriangle,
RotateCcw,
Server,
Bell,
BellOff,
BellRing,
@@ -76,10 +75,6 @@ import {
ShieldCheck,
ShieldAlert,
ShieldX,
Clock,
Cpu,
Globe,
Wifi,
Activity,
Users,
Layers,
@@ -116,7 +111,7 @@ import {
SettingsSegmented,
SettingsSwitchRow,
} from "../components/settings/SettingsRow";
import type { WSMessage } from "../lib/types";
import type { UpdateStatusPayload, WSMessage } from "../lib/types";
import { confirmDialog } from "../lib/confirm";
import { toast } from "../lib/toast";
import { useDialog } from "../hooks/useDialog";
@@ -145,22 +140,13 @@ const NOTIF_TYPES: [NotifType, string][] = [
// ─── Helpers ───
interface SystemInfo {
db: { path: string; size: number; counts: Record<string, number> };
hooks: { installed: boolean; path: string; hooks: Record<string, boolean> };
server: {
version: string;
uptime: number;
node_version: string;
platform: string;
ws_connections: number;
};
}
type SystemInfo = Awaited<ReturnType<typeof api.settings.info>>;
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
if (bytes < 1024 ** 3) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
}
function formatUptime(seconds: number): string {
@@ -1133,85 +1119,167 @@ export function Settings({
)}
{section === "about" && (
<section id="about" className="scroll-mt-24">
<div className="mb-4">
<AutostartSettings />
</div>
<p className="text-xs text-fg-muted mb-4">{t("about.description")}</p>
<div id="about" className="space-y-6 scroll-mt-24">
<AutostartSettings />
{sysInfo ? (
<div className="card p-5">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
<div className="bg-surface-2 rounded-lg px-4 py-3">
<div className="flex items-center gap-2 mb-1.5">
<Server className="w-4 h-4 text-accent" />
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
{t("about.release")}
</p>
</div>
<p className="text-sm font-semibold text-fg-secondary font-mono">
v{sysInfo.server.version}
</p>
{sysInfo.server.version !== __APP_VERSION__ && (
<p className="text-[10px] text-status-warning/90 mt-1">
{t("about.uiBuild", { version: __APP_VERSION__ })}
</p>
)}
</div>
<div className="bg-surface-2 rounded-lg px-4 py-3">
<div className="flex items-center gap-2 mb-1.5">
<Clock className="w-4 h-4 text-accent" />
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
{t("about.uptime")}
</p>
</div>
<p className="text-sm font-semibold text-fg-secondary">
{formatUptime(sysInfo.server.uptime)}
</p>
</div>
<div className="bg-surface-2 rounded-lg px-4 py-3">
<div className="flex items-center gap-2 mb-1.5">
<Cpu className="w-4 h-4 text-status-success" />
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
{t("about.nodejs")}
</p>
</div>
<p className="text-sm font-semibold text-fg-secondary font-mono">
{sysInfo.server.node_version}
</p>
</div>
<div className="bg-surface-2 rounded-lg px-4 py-3">
<div className="flex items-center gap-2 mb-1.5">
<Globe className="w-4 h-4 text-syntax-keyword" />
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
{t("about.platform")}
</p>
</div>
<p className="text-sm font-semibold text-fg-secondary">
{sysInfo.server.platform}
</p>
</div>
<div className="bg-surface-2 rounded-lg px-4 py-3">
<div className="flex items-center gap-2 mb-1.5">
<Wifi className="w-4 h-4 text-status-warning" />
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
{t("about.wsClients")}
</p>
</div>
<p className="text-sm font-semibold text-fg-secondary">
{sysInfo.server.ws_connections}
</p>
</div>
</div>
</div>
<SystemDetails info={sysInfo} />
) : (
<p className="text-xs text-fg-muted">{t("about.loadingInfo")}</p>
)}
</section>
</div>
)}
</div>
);
}
/** A read-only value on the right of a System row. */
function Value({ children, mono = false }: { children: ReactNode; mono?: boolean }) {
return (
<span className={`text-sm text-fg-primary ${mono ? "font-mono text-xs" : ""}`}>{children}</span>
);
}
/**
* The System page's facts: the server process, how AgentHub is installed (from
* the update check's last answer — it never fetches), and the database file.
*/
function SystemDetails({ info }: { info: SystemInfo }) {
const { t } = useTranslation("settings");
const [upd, setUpd] = useState<UpdateStatusPayload | null>(null);
useEffect(() => {
api.updates
.status()
.then(setUpd)
.catch(() => {});
}, []);
const srv = info.server;
const mem = srv.memory;
const heapLimit = mem ? (mem.heapLimit ?? mem.heapTotal) : 0;
const load = srv.cpu_load?.map((n) => n.toFixed(2)).join(" / ");
const kind = upd?.install_kind;
return (
<>
<SettingsSection id="server" title={t("about.server")}>
<SettingsRow
label={t("about.release")}
description={
srv.version !== __APP_VERSION__ ? (
<span className="text-status-warning/90">
{t("about.uiBuild", { version: __APP_VERSION__ })}
</span>
) : undefined
}
control={<Value mono>v{srv.version}</Value>}
/>
<SettingsRow
label={t("about.uptime")}
control={<Value>{formatUptime(srv.uptime)}</Value>}
/>
<SettingsRow
label={t("about.platform")}
control={
<Value mono>
{srv.platform}
{srv.arch ? ` · ${srv.arch}` : ""} · Node.js {srv.node_version}
</Value>
}
/>
{srv.cpus > 0 && (
<SettingsRow
label={t("about.cpu")}
control={<Value>{t("about.cpuValue", { count: srv.cpus, load })}</Value>}
/>
)}
{mem && (
<SettingsRow
label={t("about.memory")}
control={
<Value>
{t("about.memoryValue", {
rss: formatBytes(mem.rss),
used: formatBytes(mem.heapUsed),
limit: formatBytes(heapLimit),
})}
</Value>
}
/>
)}
{srv.total_mem > 0 && (
<SettingsRow
label={t("about.systemMemory")}
control={
<Value>
{t("about.systemMemoryValue", {
used: formatBytes(srv.total_mem - srv.free_mem),
total: formatBytes(srv.total_mem),
})}
</Value>
}
/>
)}
<SettingsRow
label={t("about.wsClients")}
description={t("about.wsClientsDesc")}
control={<Value>{srv.ws_connections}</Value>}
/>
</SettingsSection>
{upd && (
<SettingsSection id="install" title={t("about.install")}>
<SettingsRow
label={kind ? t(`about.installKind.${kind}`) : t("about.unknown")}
description={
upd.repo_root ? (
<span className="font-mono break-all">{upd.repo_root}</span>
) : undefined
}
control={upd.local_version ? <Value mono>v{upd.local_version}</Value> : null}
/>
{upd.latest_release && (
<SettingsRow
label={t("about.latest")}
control={
<span className="flex items-center gap-2">
<Value mono>{upd.latest_release}</Value>
<span
className={`rounded-full px-2 py-0.5 text-[11px] font-medium ${
upd.update_available
? "bg-status-warning/10 text-status-warning"
: "bg-status-success/10 text-status-success"
}`}
>
{upd.update_available ? t("about.updateAvailable") : t("about.upToDate")}
</span>
</span>
}
/>
)}
</SettingsSection>
)}
<SettingsSection id="database" title={t("about.database")}>
<SettingsRow
label={t("about.dbFile")}
description={<span className="font-mono break-all">{info.db.path}</span>}
control={<Value>{formatBytes(info.db.size)}</Value>}
/>
{info.db.pragmas && (
<SettingsRow
label={t("about.journal")}
control={<Value mono>{info.db.pragmas.journal_mode}</Value>}
/>
)}
{info.db.load_stats && (
<SettingsRow
label={t("about.activity")}
control={<Value>{t("about.activityValue", info.db.load_stats)}</Value>}
/>
)}
</SettingsSection>
</>
);
}
/** "Clear all data": the one action here that empties the whole database, so
* it asks for a typed phrase (not a second click) and offers an export first. */
function ClearDataDialog({