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:
+158
-90
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user