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), );}
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),
);
}