a9c71cd905
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.
342 lines
11 KiB
TypeScript
342 lines
11 KiB
TypeScript
/**
|
|
* @file settingsSearch — the Settings page's search: `SETTINGS_INDEX` names the
|
|
* rows each section holds (as i18n keys, resolved at render), and
|
|
* `rankSettings` scores sections against a query. Per field, exact beats
|
|
* prefix beats substring: section title 900/850/800, entry title 700/650/600,
|
|
* description 500/450/400, keywords 300/250/200; a section scores its best hit.
|
|
* `rankSettingsEntries` ranks the rows themselves, for the result list that
|
|
* deep-links a row's sub-tab and anchor.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
export interface SettingsEntry {
|
|
section: string;
|
|
title: string;
|
|
description?: string;
|
|
keywords?: string[];
|
|
/** The section's own title (scored in the top tier). */
|
|
sectionTitle?: boolean;
|
|
/** Appended to `/settings/<section>`: a sub-tab (`?tab=skills`) and/or an
|
|
* anchor (`#cleanup`) the hub scrolls to and highlights. */
|
|
to?: string;
|
|
}
|
|
|
|
/** One searchable row per section, by i18n key (`settings` namespace unless
|
|
* the key names another, `ccConfig:tabs.skills`). `to` targets a sub-tab and/or
|
|
* a row anchor — an element with that `id` or `data-settings-anchor`. A
|
|
* section whose id starts with `agents/` and is not listed here is a catalog
|
|
* tool's page and takes the `agents/*` entries. */
|
|
export const SETTINGS_INDEX: {
|
|
section: string;
|
|
titleKey: string;
|
|
descKey?: string;
|
|
keywords?: string[];
|
|
to?: string;
|
|
}[] = [
|
|
{
|
|
section: "appearance",
|
|
titleKey: "language.title",
|
|
to: "#language",
|
|
keywords: ["english", "tiếng việt", "vietnamese", "ngôn ngữ"],
|
|
},
|
|
{
|
|
section: "appearance",
|
|
titleKey: "terminalFont.title",
|
|
descKey: "terminalFont.description",
|
|
keywords: ["font", "phông", "maple", "jetbrains", "monospace", "rounded", "bo tròn"],
|
|
to: "#terminal-font",
|
|
},
|
|
{
|
|
section: "data",
|
|
titleKey: "pushIngest.title",
|
|
descKey: "pushIngest.description",
|
|
keywords: ["token", "push", "remote", "batch"],
|
|
to: "#push-ingest",
|
|
},
|
|
{
|
|
section: "data",
|
|
titleKey: "privacy.title",
|
|
descKey: "privacy.description",
|
|
keywords: ["redact", "secrets", "ignore", "folder", "cwd"],
|
|
to: "#privacy",
|
|
},
|
|
{
|
|
section: "data",
|
|
titleKey: "lanesSection.title",
|
|
descKey: "lanesSection.description",
|
|
to: "#lanes",
|
|
},
|
|
{
|
|
section: "data",
|
|
titleKey: "lanesSection.runHousekeeping",
|
|
keywords: ["gc", "logs", "processes"],
|
|
to: "#lanes",
|
|
},
|
|
{
|
|
section: "data",
|
|
titleKey: "import.title",
|
|
keywords: ["history", "transcripts"],
|
|
to: "#import",
|
|
},
|
|
{
|
|
section: "data",
|
|
titleKey: "remoteSources.title",
|
|
keywords: ["ssh", "machines"],
|
|
to: "#remote-sources",
|
|
},
|
|
{ section: "data", titleKey: "data.title", descKey: "data.description", to: "#data" },
|
|
{ section: "data", titleKey: "exportData", keywords: ["backup", "download"], to: "#data" },
|
|
{
|
|
section: "data",
|
|
titleKey: "data.sessionCleanup",
|
|
descKey: "data.cleanupDesc",
|
|
keywords: ["purge", "abandon", "retention"],
|
|
to: "#cleanup",
|
|
},
|
|
{
|
|
section: "data",
|
|
titleKey: "prefsBackup.title",
|
|
descKey: "prefsBackup.description",
|
|
keywords: ["export", "import", "browser", "preferences"],
|
|
to: "#prefs",
|
|
},
|
|
{
|
|
section: "data",
|
|
titleKey: "danger.title",
|
|
keywords: ["clear", "delete", "wipe"],
|
|
to: "#danger",
|
|
},
|
|
{
|
|
section: "system",
|
|
titleKey: "about.title",
|
|
keywords: ["about", "version", "server", "database", "uptime", "memory", "cpu", "install"],
|
|
},
|
|
{
|
|
section: "system",
|
|
titleKey: "autostart.label",
|
|
keywords: ["autostart", "boot", "login", "systemd", "launchd", "service"],
|
|
to: "#autostart",
|
|
},
|
|
{
|
|
section: "system",
|
|
titleKey: "quit.label",
|
|
keywords: ["quit", "stop", "kill", "shutdown", "exit", "background"],
|
|
to: "#autostart",
|
|
},
|
|
{
|
|
section: "system",
|
|
titleKey: "tray.label",
|
|
keywords: ["tray", "icon", "taskbar", "menu bar", "status area", "notification area"],
|
|
to: "#autostart",
|
|
},
|
|
{
|
|
section: "appearance",
|
|
titleKey: "sagi.title",
|
|
descKey: "sagi.description",
|
|
keywords: ["sagi", "mascot", "pet"],
|
|
to: "#pet",
|
|
},
|
|
{ section: "appearance", titleKey: "sagi.bubbles", descKey: "sagi.bubblesDesc", to: "#pet" },
|
|
{ section: "appearance", titleKey: "sagi.pet.size", to: "#pet" },
|
|
{
|
|
section: "appearance",
|
|
titleKey: "sagi.pet.choose",
|
|
keywords: ["codex-pets", "upload", "import"],
|
|
to: "#pet",
|
|
},
|
|
{
|
|
section: "reports",
|
|
titleKey: "snapshots.title",
|
|
descKey: "snapshots.description",
|
|
keywords: ["share", "freeze", "link", "redact"],
|
|
to: "#snapshots",
|
|
},
|
|
{
|
|
section: "reports",
|
|
titleKey: "reports.title",
|
|
descKey: "reports.description",
|
|
keywords: ["schedule", "report", "email", "summary", "html", "json", "export"],
|
|
},
|
|
{ section: "appearance", titleKey: "appearance.mode", keywords: ["dark", "light", "system"] },
|
|
{ section: "appearance", titleKey: "appearance.darkThemes", keywords: ["theme", "colors"] },
|
|
{ section: "appearance", titleKey: "appearance.lightThemes" },
|
|
{
|
|
section: "notifications",
|
|
titleKey: "notifications.enable",
|
|
keywords: ["push", "browser"],
|
|
to: "#notifications",
|
|
},
|
|
{ section: "notifications", titleKey: "notifications.awaitingInput", to: "#notifications" },
|
|
{ section: "notifications", titleKey: "notifications.turnComplete", to: "#notifications" },
|
|
{
|
|
section: "notifications",
|
|
titleKey: "notifications.sound.title",
|
|
keywords: ["chime", "audio"],
|
|
to: "#notifications",
|
|
},
|
|
{
|
|
section: "notifications",
|
|
titleKey: "notifications.sendTest",
|
|
keywords: ["test"],
|
|
to: "#notifications",
|
|
},
|
|
{
|
|
section: "notifications",
|
|
titleKey: "alertsHub.title",
|
|
descKey: "alertsHub.description",
|
|
to: "#alerts",
|
|
},
|
|
{
|
|
section: "notifications",
|
|
titleKey: "webhooks.title",
|
|
keywords: ["slack", "discord", "telegram", "channels"],
|
|
to: "#alerts",
|
|
},
|
|
{
|
|
section: "agents",
|
|
titleKey: "aiTools.title",
|
|
descKey: "aiTools.description",
|
|
keywords: ["install", "update", "default"],
|
|
},
|
|
{ section: "agents/claude-code", titleKey: "hooks.title" },
|
|
{ section: "agents/claude-code", titleKey: "claudeHome.title" },
|
|
{ section: "agents/claude-code", titleKey: "ccConfig:tabs.skills", to: "?tab=skills" },
|
|
{ section: "agents/claude-code", titleKey: "ccConfig:tabs.agents", to: "?tab=agents" },
|
|
{ section: "agents/claude-code", titleKey: "ccConfig:tabs.commands", to: "?tab=commands" },
|
|
{
|
|
section: "agents/claude-code",
|
|
titleKey: "ccConfig:tabs.memory",
|
|
keywords: ["claude.md"],
|
|
to: "?tab=memory",
|
|
},
|
|
{ section: "agents/claude-code", titleKey: "ccConfig:tabs.plugins", to: "?tab=plugins" },
|
|
{
|
|
section: "agents/claude-code",
|
|
titleKey: "ccConfig:tabs.marketplaces",
|
|
to: "?tab=marketplaces",
|
|
},
|
|
{
|
|
section: "agents/claude-code",
|
|
titleKey: "ccConfig:tabs.mcp",
|
|
keywords: ["mcp"],
|
|
to: "?tab=mcp",
|
|
},
|
|
{
|
|
section: "agents/claude-code",
|
|
titleKey: "ccConfig:tabs.hooks",
|
|
keywords: ["hooks"],
|
|
to: "?tab=hooks",
|
|
},
|
|
{
|
|
section: "agents/claude-code",
|
|
titleKey: "ccConfig:tabs.keybindings",
|
|
keywords: ["shortcuts"],
|
|
to: "?tab=keybindings",
|
|
},
|
|
{
|
|
section: "agents/claude-code",
|
|
titleKey: "ccConfig:tabs.outputStyles",
|
|
to: "?tab=outputStyles",
|
|
},
|
|
{
|
|
section: "agents/claude-code",
|
|
titleKey: "ccConfig:tabs.settings",
|
|
keywords: ["settings.json", "statusline", "permissions"],
|
|
to: "?tab=settings",
|
|
},
|
|
{
|
|
section: "agents/claude-code",
|
|
titleKey: "ccConfig:statusline.title",
|
|
keywords: ["statusline"],
|
|
to: "?tab=settings",
|
|
},
|
|
{
|
|
section: "agents/claude-code",
|
|
titleKey: "ccConfig:profiles.title",
|
|
keywords: ["account", "login", "quota", "switch"],
|
|
},
|
|
{ section: "agents/claude-code", titleKey: "ccConfig:backups.title", keywords: ["restore"] },
|
|
{ section: "agents/codex", titleKey: "codexHooks.title" },
|
|
{ section: "agents/codex", titleKey: "codexHome.title" },
|
|
{ section: "agents/codex", titleKey: "codexConfig:tabs.skills", to: "?tab=skills" },
|
|
{ section: "agents/codex", titleKey: "codexConfig:tabs.agents", to: "?tab=agents" },
|
|
{ section: "agents/codex", titleKey: "codexConfig:tabs.rules", to: "?tab=rules" },
|
|
{
|
|
section: "agents/codex",
|
|
titleKey: "codexConfig:tabs.memory",
|
|
keywords: ["agents.md"],
|
|
to: "?tab=memory",
|
|
},
|
|
{
|
|
section: "agents/codex",
|
|
titleKey: "codexConfig:tabs.config",
|
|
keywords: ["config.toml"],
|
|
to: "?tab=config",
|
|
},
|
|
{ section: "agents/codex", titleKey: "codexConfig:tabs.mcp", keywords: ["mcp"], to: "?tab=mcp" },
|
|
{ section: "agents/codex", titleKey: "codexConfig:tabs.plugins", to: "?tab=plugins" },
|
|
{ section: "agents/antigravity", titleKey: "antigravityHooks.title", keywords: ["agy", "hooks"] },
|
|
{ section: "agents/*", titleKey: "toolTab.defaultModel", keywords: ["model", "config"] },
|
|
{ section: "ai-proxy", titleKey: "aiProxy.title", keywords: ["router", "endpoint", "key"] },
|
|
{ section: "pricing", titleKey: "pricing.title", keywords: ["cost", "price", "tokens"] },
|
|
{ section: "git", titleKey: "gitCreds.title", keywords: ["login", "token"] },
|
|
{
|
|
section: "git",
|
|
titleKey: "worktrees.showUntracked",
|
|
keywords: ["hide", "suggestions"],
|
|
to: "#worktrees",
|
|
},
|
|
{ section: "git", titleKey: "worktrees.claude", to: "#worktrees" },
|
|
{ section: "git", titleKey: "worktrees.agenthub", to: "#worktrees" },
|
|
];
|
|
|
|
function tier(hay: string | undefined, q: string, exact: number): number {
|
|
if (!hay) return 0;
|
|
const h = hay.toLowerCase();
|
|
if (h === q) return exact;
|
|
if (h.startsWith(q)) return exact - 50;
|
|
if (h.includes(q)) return exact - 100;
|
|
return 0;
|
|
}
|
|
|
|
/** Score of one entry against an already-lowercased query (0 = no match). */
|
|
function scoreEntry(e: SettingsEntry, q: string): number {
|
|
return Math.max(
|
|
tier(e.title, q, e.sectionTitle ? 900 : 700),
|
|
tier(e.description, q, 500),
|
|
...(e.keywords ?? []).map((k) => tier(k, q, 300))
|
|
);
|
|
}
|
|
|
|
/** Entries matching `query`, best first (ties keep order), at most `limit`. */
|
|
export function rankSettingsEntries(
|
|
query: string,
|
|
entries: SettingsEntry[],
|
|
limit = 8
|
|
): (SettingsEntry & { score: number })[] {
|
|
const q = query.trim().toLowerCase();
|
|
if (!q) return [];
|
|
return entries
|
|
.map((e) => ({ ...e, score: scoreEntry(e, q) }))
|
|
.filter((e) => e.score > 0)
|
|
.sort((a, b) => b.score - a.score)
|
|
.slice(0, limit);
|
|
}
|
|
|
|
/** Sections matching `query`, best first; ties keep the entries' order. */
|
|
export function rankSettings(
|
|
query: string,
|
|
entries: SettingsEntry[]
|
|
): { section: string; score: number }[] {
|
|
const q = query.trim().toLowerCase();
|
|
if (!q) return [];
|
|
const best = new Map<string, number>();
|
|
for (const e of entries) {
|
|
const score = scoreEntry(e, q);
|
|
if (score > (best.get(e.section) ?? 0)) best.set(e.section, score);
|
|
}
|
|
return [...best]
|
|
.map(([section, score]) => ({ section, score }))
|
|
.sort((a, b) => b.score - a.score);
|
|
}
|