298 lines · 7.3 KB · TypeScript
RawDownload
import { isElectron } from "~/env"; export type SettingsPath =  | "/settings/general"  | "/settings/appearance"  | "/settings/keybindings"  | "/settings/providers"  | "/settings/integrations"  | "/settings/source-control"  | "/settings/connections"  | "/settings/archived"; export interface SettingsSearchItem {  readonly id: string;  readonly title: string;  readonly to: SettingsPath;  readonly targetId?: string;  // Its row only renders in the desktop app, so a browser result would land on  // an anchor that isn't there.  readonly desktopOnly?: boolean;} /** * Section labels in sidebar order. The sidebar nav and the search-result * subtitles both render from this record, so each label exists once. */export const SETTINGS_SECTION_LABELS: Readonly<Record<SettingsPath, string>> = {  "/settings/general": "General",  "/settings/appearance": "Appearance",  "/settings/keybindings": "Keybindings",  "/settings/providers": "Providers",  "/settings/integrations": "Integrations",  "/settings/source-control": "Source Control",  "/settings/connections": "Connections",  "/settings/archived": "Archive",}; /** * Every searchable setting, in result order. This catalog is the single * source of truth for anchor ids and visible titles: panels render both via * `searchableSetting`, so a retitle (or, later, a translation pass) happens * here once instead of separately in the panel and the index. */export const SETTINGS_SEARCH_ITEMS = [  {    id: "color-scheme",    title: "Color scheme",    to: "/settings/appearance",    // The scheme tiles sit at the top of the Appearance section.    targetId: "appearance",  },  {    id: "theme",    title: "Themes",    to: "/settings/appearance",    // Theme cards live directly under the scheme tiles; the section is the    // stable scroll destination for both.    targetId: "appearance",  },  {    // Prefixed because the slider control already owns the `appearance-contrast` id.    id: "setting-appearance-contrast",    title: "Contrast",    to: "/settings/appearance",  },  {    // Prefixed because the slider control already owns the `glass-opacity` id.    id: "setting-glass-opacity",    title: "Glass opacity",    to: "/settings/appearance",  },  {    id: "environment-identification",    title: "Environment identification",    to: "/settings/appearance",    // The setting is stage-dependent, so its parent section is the stable destination.    targetId: "appearance",  },  {    id: "interface-font",    title: "Interface font",    to: "/settings/appearance",  },  {    id: "prompt-font",    title: "Prompt font",    to: "/settings/appearance",  },  {    id: "code-font",    title: "Code font",    to: "/settings/appearance",  },  {    id: "terminal-font",    title: "Terminal font",    to: "/settings/appearance",  },  {    id: "font-smoothing",    title: "Font smoothing",    to: "/settings/appearance",  },  {    id: "word-wrap",    title: "Word wrap",    to: "/settings/appearance",  },  {    id: "project-grouping",    title: "Project grouping",    to: "/settings/general",  },  {    id: "auto-settle-inactive-threads",    title: "Auto-settle inactive threads",    to: "/settings/general",  },  {    id: "auto-settle-merged-threads",    title: "Auto-settle merged threads",    to: "/settings/general",  },  {    id: "time-format",    title: "Time format",    to: "/settings/general",  },  {    id: "hide-whitespace-changes",    title: "Hide whitespace changes",    to: "/settings/general",  },  {    id: "skills-in-slash-menu",    title: "Show skills in slash menu",    to: "/settings/general",  },  {    id: "provider-update-checks",    title: "Provider update checks",    to: "/settings/general",  },  {    id: "new-threads",    title: "New threads",    to: "/settings/general",  },  {    id: "start-from-origin",    title: "Start from origin",    to: "/settings/general",    targetId: "new-threads",  },  {    id: "add-project-starts-in",    title: "Add project starts in",    to: "/settings/general",  },  {    id: "archive-confirmation",    title: "Archive confirmation",    to: "/settings/general",  },  {    id: "delete-confirmation",    title: "Delete confirmation",    to: "/settings/general",  },  {    id: "quit-confirmation",    title: "Hold to quit",    to: "/settings/general",    desktopOnly: true,  },  {    id: "text-generation-model",    title: "Text generation model",    to: "/settings/general",  },  {    id: "diagnostics",    title: "Diagnostics",    to: "/settings/general",  },  {    id: "legacy-plan-mode",    title: "Plan mode (legacy)",    to: "/settings/general",  },  {    id: "legacy-token-streaming",    title: "Stream token by token (legacy)",    to: "/settings/general",  },  {    id: "legacy-sidebar",    title: "Sidebar (legacy)",    to: "/settings/general",  },  {    id: "keybindings",    title: "Keybindings",    to: "/settings/keybindings",  },  {    id: "providers",    title: "Providers",    to: "/settings/providers",  },  {    id: "agent-browser-access",    title: "Agent browser access",    to: "/settings/integrations",    targetId: "browser",  },  {    id: "browser-default-viewport",    title: "Default browser viewport",    to: "/settings/integrations",    targetId: "browser",  },  {    id: "browser-default-zoom",    title: "Default browser zoom",    to: "/settings/integrations",    targetId: "browser",  },  {    id: "browser-default-appearance",    title: "Default browser appearance",    to: "/settings/integrations",    targetId: "browser",  },  {    id: "browser-auto-show-floating-preview",    title: "Auto-show floating preview",    to: "/settings/integrations",    targetId: "browser",  },  {    id: "source-control",    title: "Source control",    to: "/settings/source-control",  },  {    id: "remote-environments",    title: "Remote environments",    to: "/settings/connections",  },  {    id: "archive",    title: "Archived threads",    to: "/settings/archived",  },] as const satisfies ReadonlyArray<SettingsSearchItem>; export type SettingsSearchItemId = (typeof SETTINGS_SEARCH_ITEMS)[number]["id"]; const SEARCH_ITEMS_BY_ID = Object.fromEntries(  SETTINGS_SEARCH_ITEMS.map((item) => [item.id, item]),) as Readonly<Record<SettingsSearchItemId, SettingsSearchItem>>; /** * `id` and `title` props for the element a search item anchors to. Panels * spread (or pick from) this instead of restating the strings, so the catalog * and the rendered settings cannot drift apart. */export function searchableSetting(id: SettingsSearchItemId): {  readonly id: string;  readonly title: string;} {  const { id: anchorId, title } = SEARCH_ITEMS_BY_ID[id];  return { id: anchorId, title };} function normalizeSearchText(value: string): string {  return value    .normalize("NFKD")    .replace(/[\u0300-\u036f]/g, "")    .toLocaleLowerCase()    .replace(/\s+/g, " ")    .trim();} export function searchSettings(  query: string,  items: ReadonlyArray<SettingsSearchItem> = SETTINGS_SEARCH_ITEMS,): ReadonlyArray<SettingsSearchItem> {  const normalizedQuery = normalizeSearchText(query);  if (normalizedQuery.length === 0) return [];   return items.filter(    (item) =>      (isElectron || item.desktopOnly !== true) &&      normalizeSearchText(item.title).includes(normalizedQuery),  );}