eb90244182
Settings moves from a tab bar to /settings/<section>: a left nav grouped General / Agents / Routing / Git / Projects (a select on narrow screens), a search box over every section (Ctrl/Cmd+F, Enter opens the first match), and row primitives. Each AI tool has its page under agents/<id>, the AI Tools modal becomes the Agents page, Pricing shows every vendor, Git → Worktrees sets which untracked worktrees the sidebar lists, and each project gets a page with its own worktree override and Forget all. ?tool= and #ai-tools links redirect to the path form.
94 lines
4.4 KiB
TypeScript
94 lines
4.4 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.
|
|
* @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;
|
|
}
|
|
|
|
/** 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<string, number>();
|
|
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);
|
|
}
|