/** * @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. * @author Nguyễn Ngọc Trí Vĩ */ export interface SettingsEntry { section: string; title: string; description?: string; keywords?: string[]; /** The section's own title (scored in the top tier). */ sectionTitle?: boolean; } /** One searchable row per section, by i18n key in the `settings` namespace. 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[]; }[] = [ { section: "general", titleKey: "lanesSection.title", descKey: "lanesSection.description" }, { section: "general", titleKey: "lanesSection.runHousekeeping" }, { section: "general", titleKey: "import.title", keywords: ["history", "transcripts"] }, { section: "general", titleKey: "remoteSources.title", keywords: ["ssh", "machines"] }, { section: "general", titleKey: "sagi.title", descKey: "sagi.description" }, { section: "general", titleKey: "data.title", descKey: "data.description" }, { section: "general", titleKey: "danger.title", keywords: ["clear", "delete", "export"] }, { section: "general", titleKey: "about.title", keywords: ["version", "server", "database"] }, { 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"] }, { section: "notifications", titleKey: "notifications.awaitingInput" }, { section: "notifications", titleKey: "notifications.turnComplete" }, { section: "notifications", titleKey: "alertsHub.title", descKey: "alertsHub.description" }, { section: "notifications", titleKey: "webhooks.title", keywords: ["slack", "discord"] }, { section: "agents", titleKey: "aiTools.title", descKey: "aiTools.description", keywords: ["install", "update", "default"], }, { section: "agents/claude-code", titleKey: "pricing.title", keywords: ["cost", "price"] }, { section: "agents/claude-code", titleKey: "hooks.title" }, { section: "agents/claude-code", titleKey: "claudeHome.title", keywords: ["profiles"] }, { section: "agents/codex", titleKey: "pricing.title", keywords: ["cost", "price"] }, { section: "agents/codex", titleKey: "codexHooks.title" }, { section: "agents/codex", titleKey: "codexHome.title" }, { section: "agents/*", titleKey: "toolTab.defaultModel", keywords: ["model", "config"] }, { section: "ai-router", titleKey: "aiRouter.title", keywords: ["proxy", "endpoint", "key"] }, { section: "pricing", titleKey: "pricing.title", keywords: ["cost", "price", "tokens"] }, { section: "git-credentials", titleKey: "gitCreds.title", keywords: ["login", "token"] }, { section: "worktrees", titleKey: "worktrees.showUntracked", keywords: ["hide", "sidebar"] }, { section: "worktrees", titleKey: "worktrees.claude" }, { section: "worktrees", titleKey: "worktrees.agenthub" }, ]; 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; } /** 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(); for (const e of entries) { const score = Math.max( tier(e.title, q, e.sectionTitle ? 900 : 700), tier(e.description, q, 500), ...(e.keywords ?? []).map((k) => tier(k, q, 300)) ); 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); }