| 1 | import { HOSTED_APP_CHANNEL } from "~/branding"; |
| 2 | import { isElectron } from "~/env"; |
| 3 | import { isMacPlatform, isWindowsPlatform, normalizeSearchText } from "~/lib/utils"; |
| 4 | import { STATIC_KEYBINDING_COMMANDS, type KeybindingCommand } from "@t3tools/contracts"; |
| 5 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 6 | import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection"; |
| 7 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; |
| 8 | import { DEFAULT_KEYBINDINGS } from "@t3tools/shared/keybindings"; |
| 9 | import { commandLabel } from "./KeybindingsSettings.logic"; |
| 10 | import { |
| 11 | validateSettingsScopeSearch, |
| 12 | type ResolvedSettingsScope, |
| 13 | type SettingsScopeSearch, |
| 14 | } from "./settingsScope"; |
| 15 | |
| 16 | export type SettingsPath = |
| 17 | | "/settings/projects" |
| 18 | | "/settings/general" |
| 19 | | "/settings/diagnostics" |
| 20 | | "/settings/appearance" |
| 21 | | "/settings/keybindings" |
| 22 | | "/settings/snap-shot" |
| 23 | | "/settings/providers" |
| 24 | | "/settings/integrations" |
| 25 | | "/settings/scheduled-tasks" |
| 26 | | "/settings/source-control" |
| 27 | | "/settings/storage" |
| 28 | | "/settings/connections" |
| 29 | | "/settings/archived"; |
| 30 | |
| 31 | /** |
| 32 | * Where a setting can be edited. Device-local rows have no scope: they render |
| 33 | * at every selection. `project-defaults` rows accept project overrides, so |
| 34 | * they are reachable from any server-backed selection. |
| 35 | */ |
| 36 | export type SettingsSearchScope = |
| 37 | | "environment" |
| 38 | | "environment-defaults" |
| 39 | | "project-defaults" |
| 40 | | "project" |
| 41 | | "checkout" |
| 42 | | "connections"; |
| 43 | |
| 44 | export interface SettingsSearchItem { |
| 45 | readonly id: string; |
| 46 | readonly title: string; |
| 47 | readonly to: SettingsPath; |
| 48 | readonly targetId?: string; |
| 49 | /** Descriptions, option labels, and aliases people may remember instead of the title. */ |
| 50 | readonly searchTerms?: ReadonlyArray<string>; |
| 51 | readonly scope?: SettingsSearchScope; |
| 52 | // Its row only renders in the desktop app, so a browser result would land on |
| 53 | // an anchor that isn't there. |
| 54 | readonly desktopOnly?: boolean; |
| 55 | readonly macOnly?: boolean; |
| 56 | // Its row only renders on Windows desktop, so other desktop platforms must |
| 57 | // not expose a result that points to a missing anchor. |
| 58 | readonly windowsOnly?: boolean; |
| 59 | // Its row renders only where the release channel can change: the desktop |
| 60 | // app, or a hosted web build deployed with a channel. |
| 61 | readonly releaseChannelOnly?: boolean; |
| 62 | readonly cloudOnly?: boolean; |
| 63 | readonly environmentOnly?: boolean; |
| 64 | readonly providerSettingsOnly?: boolean; |
| 65 | readonly macProviderSettingsOnly?: boolean; |
| 66 | readonly localBackendManagementOnly?: boolean; |
| 67 | readonly localEnvironmentOnly?: boolean; |
| 68 | readonly wslAvailableOnly?: boolean; |
| 69 | // Its row only renders while this environment's T3 Connect managed tunnel is on. |
| 70 | readonly managedTunnelOnly?: boolean; |
| 71 | /** |
| 72 | * Sorts after every other match. Keybinding commands mirror rows on other |
| 73 | * surfaces, so "model" must still lead with Default model, not Model Picker. |
| 74 | */ |
| 75 | readonly secondary?: boolean; |
| 76 | readonly requiresThreadAutoSettlement?: boolean; |
| 77 | } |
| 78 | |
| 79 | export interface SettingsSearchAvailability { |
| 80 | readonly localEnvironmentDisabled?: boolean; |
| 81 | readonly hasCloudPublicConfig: boolean; |
| 82 | readonly hasEnvironment: boolean; |
| 83 | readonly hasProviderSettingsEnvironment: boolean; |
| 84 | readonly hasMacProviderSettingsEnvironment: boolean; |
| 85 | readonly canManageLocalBackend: boolean; |
| 86 | readonly isWslSettingsRowVisible: boolean; |
| 87 | readonly hasThreadAutoSettlement: boolean; |
| 88 | readonly managedTunnelActive?: boolean; |
| 89 | } |
| 90 | |
| 91 | /** |
| 92 | * Section labels in sidebar order. The sidebar nav and the search-result |
| 93 | * subtitles both render from this record, so each label exists once. |
| 94 | */ |
| 95 | export const SETTINGS_SECTION_LABELS: Readonly<Record<SettingsPath, string>> = { |
| 96 | "/settings/diagnostics": "Diagnostics", |
| 97 | "/settings/projects": "Project", |
| 98 | "/settings/general": "General", |
| 99 | "/settings/appearance": "Appearance", |
| 100 | "/settings/keybindings": "Keybindings", |
| 101 | "/settings/snap-shot": "SnapShots", |
| 102 | "/settings/providers": "Providers", |
| 103 | "/settings/integrations": "Integrations", |
| 104 | "/settings/scheduled-tasks": "Scheduled Tasks", |
| 105 | "/settings/source-control": "Source Control", |
| 106 | "/settings/storage": "Storage", |
| 107 | "/settings/connections": "Connections", |
| 108 | "/settings/archived": "Archive", |
| 109 | }; |
| 110 | |
| 111 | /** Anchor id of the first row bound to `command` on the Keybindings page. */ |
| 112 | export function keybindingSearchAnchorId<Command extends KeybindingCommand>(command: Command) { |
| 113 | return `keybinding-${command}` as const; |
| 114 | } |
| 115 | |
| 116 | /** |
| 117 | * One result per built-in command, alphabetical by label. The anchor is |
| 118 | * the command's first row; default keys are searchable so "mod+b" lands on |
| 119 | * Sidebar: Toggle. A command with no default binding may have no row, so it |
| 120 | * points at the section instead. |
| 121 | */ |
| 122 | const KEYBINDING_SEARCH_ITEMS = STATIC_KEYBINDING_COMMANDS.toSorted((left, right) => |
| 123 | commandLabel(left).localeCompare(commandLabel(right)), |
| 124 | ).map((command) => { |
| 125 | const defaultKeys = DEFAULT_KEYBINDINGS.filter((binding) => binding.command === command).map( |
| 126 | (binding) => binding.key, |
| 127 | ); |
| 128 | return { |
| 129 | id: keybindingSearchAnchorId(command), |
| 130 | title: commandLabel(command), |
| 131 | to: "/settings/keybindings" as const, |
| 132 | searchTerms: [command, ...defaultKeys], |
| 133 | secondary: true, |
| 134 | ...(defaultKeys.length === 0 ? { targetId: "keybindings" } : {}), |
| 135 | }; |
| 136 | }); |
| 137 | |
| 138 | /** Anchor id of a source control host's settings form on the Source Control page. */ |
| 139 | export function sourceControlHostSettingsSearchId(kind: string) { |
| 140 | return `source-control-host-${kind}` as const; |
| 141 | } |
| 142 | |
| 143 | /** One result per host whose definition declares settings, searchable by its field titles. */ |
| 144 | const SOURCE_CONTROL_HOST_SEARCH_ITEMS = sourceControlClients.definitions.flatMap((definition) => |
| 145 | definition.settings && definition.kind !== "github" |
| 146 | ? [ |
| 147 | { |
| 148 | id: sourceControlHostSettingsSearchId(definition.kind), |
| 149 | title: `${definition.label} credentials`, |
| 150 | to: "/settings/source-control" as const, |
| 151 | searchTerms: [ |
| 152 | `${definition.label} ${Object.keys(definition.settings.fields).join(" ")} token credentials sign in`, |
| 153 | ], |
| 154 | environmentOnly: true, |
| 155 | scope: "environment-defaults" as const, |
| 156 | }, |
| 157 | ] |
| 158 | : [], |
| 159 | ); |
| 160 | |
| 161 | /** |
| 162 | * Searchable settings and stable destinations, in result order. Rows with a |
| 163 | * dedicated anchor render their id and title via `searchableSetting`; items |
| 164 | * that may not be mounted point at their nearest stable section instead. |
| 165 | */ |
| 166 | export const SETTINGS_SEARCH_ITEMS = [ |
| 167 | { |
| 168 | id: "storage-worktree-keep-when", |
| 169 | targetId: "storage-worktrees", |
| 170 | title: "Keep worktrees with local changes", |
| 171 | to: "/settings/storage", |
| 172 | scope: "project-defaults", |
| 173 | searchTerms: [ |
| 174 | "any local files uncommitted changes edited tracked files ignored env build output cleanup", |
| 175 | ], |
| 176 | }, |
| 177 | { |
| 178 | id: "storage-delete-now", |
| 179 | title: "Delete now", |
| 180 | to: "/settings/storage", |
| 181 | scope: "environment-defaults", |
| 182 | searchTerms: ["storage cleanup run now results report removed kept failed"], |
| 183 | }, |
| 184 | { |
| 185 | id: "storage-worktrees", |
| 186 | title: "Worktree cleanup", |
| 187 | to: "/settings/storage", |
| 188 | scope: "project-defaults", |
| 189 | searchTerms: [ |
| 190 | "disk storage delete deleted archived threads old inactive merged unchanged worktrees retention days project inherit off custom", |
| 191 | ], |
| 192 | }, |
| 193 | { |
| 194 | id: "storage-worktrees-location", |
| 195 | title: "Worktree location", |
| 196 | to: "/settings/storage", |
| 197 | scope: "environment-defaults", |
| 198 | searchTerms: ["worktree location folder directory path drive external disk"], |
| 199 | }, |
| 200 | { |
| 201 | id: "storage-artifacts", |
| 202 | title: "Artifacts and logs", |
| 203 | to: "/settings/storage", |
| 204 | scope: "environment-defaults", |
| 205 | searchTerms: ["disk storage browser screenshots captures rotated logs cleanup retention"], |
| 206 | }, |
| 207 | { |
| 208 | id: "project-defaults", |
| 209 | title: "Project defaults and overrides", |
| 210 | to: "/settings/general", |
| 211 | scope: "project-defaults", |
| 212 | searchTerms: ["model workspace environments projects inheritance checkout"], |
| 213 | }, |
| 214 | { |
| 215 | id: "project-overview", |
| 216 | title: "Project overview", |
| 217 | to: "/settings/projects", |
| 218 | searchTerms: ["name icon emoji image checkout remove delete"], |
| 219 | }, |
| 220 | { |
| 221 | id: "default-model", |
| 222 | title: "Default model", |
| 223 | to: "/settings/general", |
| 224 | scope: "project-defaults", |
| 225 | searchTerms: ["new thread project provider reasoning effort"], |
| 226 | }, |
| 227 | { |
| 228 | id: "default-permissions", |
| 229 | title: "Permissions", |
| 230 | to: "/settings/general", |
| 231 | scope: "project-defaults", |
| 232 | searchTerms: [ |
| 233 | "new thread default runtime mode supervised approvals auto accept edits full access", |
| 234 | ], |
| 235 | }, |
| 236 | { |
| 237 | id: "color-scheme", |
| 238 | title: "Color scheme", |
| 239 | to: "/settings/appearance", |
| 240 | searchTerms: ["appearance light dark system mode"], |
| 241 | // The scheme tiles sit at the top of the Appearance section. |
| 242 | targetId: "appearance", |
| 243 | }, |
| 244 | { |
| 245 | id: "theme", |
| 246 | title: "Themes", |
| 247 | to: "/settings/appearance", |
| 248 | searchTerms: ["appearance colors palette custom import"], |
| 249 | // Theme cards live directly under the scheme tiles; the section is the |
| 250 | // stable scroll destination for both. |
| 251 | targetId: "appearance", |
| 252 | }, |
| 253 | { |
| 254 | // Prefixed because the slider control already owns the `appearance-contrast` id. |
| 255 | id: "setting-appearance-contrast", |
| 256 | title: "Contrast", |
| 257 | to: "/settings/appearance", |
| 258 | searchTerms: ["colors borders interface"], |
| 259 | }, |
| 260 | { |
| 261 | // Prefixed because the slider control already owns the `glass-opacity` id. |
| 262 | id: "setting-glass-opacity", |
| 263 | title: "Glass opacity", |
| 264 | to: "/settings/appearance", |
| 265 | searchTerms: ["transparent transparency solid menus dialogs composer"], |
| 266 | }, |
| 267 | { |
| 268 | id: "diff-color-scheme", |
| 269 | title: "Diff colors", |
| 270 | to: "/settings/appearance", |
| 271 | searchTerms: ["red green blue orange additions deletions changes counts palette colorblind"], |
| 272 | }, |
| 273 | { |
| 274 | id: "chat-width", |
| 275 | title: "Chat width", |
| 276 | to: "/settings/appearance", |
| 277 | searchTerms: ["wide full width column layout messages composer monitor"], |
| 278 | }, |
| 279 | { |
| 280 | id: "panel-animations", |
| 281 | title: "Panel animations", |
| 282 | to: "/settings/appearance", |
| 283 | }, |
| 284 | { |
| 285 | id: "environment-identification", |
| 286 | title: "Environment identification", |
| 287 | to: "/settings/appearance", |
| 288 | searchTerms: ["dev nightly artwork pill label hide none"], |
| 289 | // The setting is stage-dependent, so its parent section is the stable destination. |
| 290 | targetId: "appearance-interface", |
| 291 | }, |
| 292 | { |
| 293 | id: "interface-font", |
| 294 | title: "Interface font", |
| 295 | to: "/settings/appearance", |
| 296 | searchTerms: ["typography family size system sans"], |
| 297 | }, |
| 298 | { |
| 299 | id: "prompt-font", |
| 300 | title: "Prompt font", |
| 301 | to: "/settings/appearance", |
| 302 | searchTerms: ["typography family size composer input"], |
| 303 | }, |
| 304 | { |
| 305 | id: "code-font", |
| 306 | title: "Code font", |
| 307 | to: "/settings/appearance", |
| 308 | searchTerms: ["typography family size monospace code blocks diffs file previews"], |
| 309 | }, |
| 310 | { |
| 311 | id: "terminal-font", |
| 312 | title: "Terminal font", |
| 313 | to: "/settings/appearance", |
| 314 | searchTerms: ["typography family size monospace output"], |
| 315 | }, |
| 316 | { |
| 317 | id: "font-smoothing", |
| 318 | title: "Font smoothing", |
| 319 | to: "/settings/appearance", |
| 320 | searchTerms: ["typography text grayscale anti aliasing macos thin"], |
| 321 | macOnly: true, |
| 322 | }, |
| 323 | { |
| 324 | id: "word-wrap", |
| 325 | title: "Word wrap", |
| 326 | to: "/settings/appearance", |
| 327 | searchTerms: ["long lines code blocks tables diffs file previews"], |
| 328 | }, |
| 329 | { |
| 330 | id: "composer-context", |
| 331 | title: "Composer context", |
| 332 | to: "/settings/appearance", |
| 333 | }, |
| 334 | { |
| 335 | id: "ui-settings-sync", |
| 336 | title: "UI settings sync", |
| 337 | to: "/settings/general", |
| 338 | searchTerms: ["personal profile theme fonts devices sync"], |
| 339 | }, |
| 340 | { |
| 341 | id: "project-grouping", |
| 342 | title: "Project grouping", |
| 343 | to: "/settings/general", |
| 344 | searchTerms: ["combine matching repositories environments sidebar"], |
| 345 | }, |
| 346 | { |
| 347 | id: "project-order", |
| 348 | title: "Project order", |
| 349 | to: "/settings/general", |
| 350 | searchTerms: ["sort projects sidebar manual created recent"], |
| 351 | }, |
| 352 | { |
| 353 | id: "snooze-limited-threads", |
| 354 | title: "Snooze limited threads", |
| 355 | to: "/settings/general", |
| 356 | searchTerms: ["usage quota rate limit reset wake recover continue"], |
| 357 | }, |
| 358 | { |
| 359 | id: "auto-resume-limited-threads", |
| 360 | title: "Auto-resume limited threads", |
| 361 | to: "/settings/general", |
| 362 | searchTerms: ["usage quota rate limit reset recover continue"], |
| 363 | }, |
| 364 | { |
| 365 | id: "working-shelf", |
| 366 | title: "Working section (beta)", |
| 367 | to: "/settings/general", |
| 368 | searchTerms: ["hide fold running monitoring threads inbox sidebar shelf"], |
| 369 | }, |
| 370 | { |
| 371 | id: "auto-settle-inactive-threads", |
| 372 | title: "Auto-settle inactive threads", |
| 373 | to: "/settings/general", |
| 374 | searchTerms: ["sidebar inactivity days no activity automatically"], |
| 375 | requiresThreadAutoSettlement: true, |
| 376 | scope: "project-defaults", |
| 377 | }, |
| 378 | { |
| 379 | id: "auto-settle-merged-threads", |
| 380 | title: "Auto-settle merged threads", |
| 381 | to: "/settings/general", |
| 382 | searchTerms: ["pull request merge closed automatically sidebar"], |
| 383 | requiresThreadAutoSettlement: true, |
| 384 | scope: "project-defaults", |
| 385 | }, |
| 386 | { |
| 387 | id: "days-before-auto-settle", |
| 388 | title: "Days of inactivity before auto-settle", |
| 389 | to: "/settings/general", |
| 390 | targetId: "auto-settle-inactive-threads", |
| 391 | searchTerms: ["thread timeout activity sidebar"], |
| 392 | requiresThreadAutoSettlement: true, |
| 393 | scope: "project-defaults", |
| 394 | }, |
| 395 | { |
| 396 | id: "thread-notifications", |
| 397 | title: "Thread notifications", |
| 398 | to: "/settings/general", |
| 399 | searchTerms: ["notification sound alert completion input approval desktop"], |
| 400 | }, |
| 401 | { |
| 402 | id: "in-app-notifications", |
| 403 | title: "In-app notifications", |
| 404 | to: "/settings/general", |
| 405 | searchTerms: ["notification toast popup completion input approval failure"], |
| 406 | }, |
| 407 | { |
| 408 | id: "time-format", |
| 409 | title: "Time format", |
| 410 | to: "/settings/general", |
| 411 | searchTerms: ["timestamp clock locale system browser os 12 hour 24 hour"], |
| 412 | }, |
| 413 | { |
| 414 | id: "response-streaming", |
| 415 | title: "Response streaming", |
| 416 | to: "/settings/general", |
| 417 | scope: "project-defaults", |
| 418 | searchTerms: ["output token paragraph buffered wait turn legacy"], |
| 419 | }, |
| 420 | { |
| 421 | id: "hide-whitespace-changes", |
| 422 | title: "Hide whitespace changes", |
| 423 | to: "/settings/general", |
| 424 | searchTerms: ["diff ignore spaces edits default"], |
| 425 | }, |
| 426 | { |
| 427 | id: "default-diff-file-state", |
| 428 | title: "Default diff file state", |
| 429 | to: "/settings/general", |
| 430 | searchTerms: ["collapsed expanded collapse expand files pull request pr code tab"], |
| 431 | }, |
| 432 | { |
| 433 | id: "diff-layout", |
| 434 | title: "Diff layout", |
| 435 | to: "/settings/general", |
| 436 | searchTerms: ["stacked split side by side unified inline view"], |
| 437 | }, |
| 438 | { |
| 439 | id: "terminal-position", |
| 440 | title: "Terminal position", |
| 441 | to: "/settings/general", |
| 442 | searchTerms: ["right bottom dock placement panel shell"], |
| 443 | }, |
| 444 | { |
| 445 | id: "proactive-panels", |
| 446 | title: "Proactive panels", |
| 447 | to: "/settings/general", |
| 448 | searchTerms: ["automatically open diff pull request pr right panel agent completion"], |
| 449 | }, |
| 450 | { |
| 451 | id: "skills-in-slash-menu", |
| 452 | title: "Show skills in slash menu", |
| 453 | to: "/settings/general", |
| 454 | searchTerms: ["command menu dollar $ slash /"], |
| 455 | }, |
| 456 | { |
| 457 | id: "composer-rich-text", |
| 458 | title: "Rich text composer", |
| 459 | to: "/settings/general", |
| 460 | searchTerms: ["composer rich text markdown bold italic styled preview"], |
| 461 | }, |
| 462 | { |
| 463 | id: "composer-collapse", |
| 464 | title: "Collapse composer on scroll", |
| 465 | to: "/settings/general", |
| 466 | searchTerms: ["composer rest resting scroll wheel conversation timeline shrink minimize"], |
| 467 | }, |
| 468 | { |
| 469 | id: "send-shortcut", |
| 470 | title: "Send shortcut", |
| 471 | to: "/settings/general", |
| 472 | searchTerms: ["enter return command ctrl multiline prompt new line composer"], |
| 473 | }, |
| 474 | { |
| 475 | id: "follow-up-behavior", |
| 476 | title: "Follow-up behavior", |
| 477 | to: "/settings/general", |
| 478 | searchTerms: ["queue steer running turn send default behavior composer"], |
| 479 | }, |
| 480 | { |
| 481 | id: "provider-update-checks", |
| 482 | title: "Provider update checks", |
| 483 | to: "/settings/general", |
| 484 | searchTerms: ["installed cli versions newer available codex claude cursor grok opencode"], |
| 485 | scope: "environment-defaults", |
| 486 | }, |
| 487 | { |
| 488 | id: "continue-threads-after-server-update", |
| 489 | title: "Continue threads after restarts", |
| 490 | to: "/settings/general", |
| 491 | scope: "project-defaults", |
| 492 | searchTerms: [ |
| 493 | "resume running active interrupted work restart reboot machine crash desktop update automatically", |
| 494 | ], |
| 495 | }, |
| 496 | { |
| 497 | id: "background-activity", |
| 498 | title: "Background activity", |
| 499 | to: "/settings/general", |
| 500 | scope: "environment-defaults", |
| 501 | searchTerms: [ |
| 502 | "balanced performance battery saver advanced git fetch provider health refresh host power monitor idle policy", |
| 503 | ], |
| 504 | }, |
| 505 | { |
| 506 | id: "new-threads", |
| 507 | title: "New threads", |
| 508 | to: "/settings/general", |
| 509 | scope: "project-defaults", |
| 510 | searchTerms: ["default workspace mode draft local worktree"], |
| 511 | }, |
| 512 | { |
| 513 | id: "worktree-submodules", |
| 514 | title: "Submodules", |
| 515 | to: "/settings/general", |
| 516 | scope: "project-defaults", |
| 517 | searchTerms: ["git submodule init recursive top-level none worktree t3.json"], |
| 518 | }, |
| 519 | { |
| 520 | id: "start-from-origin", |
| 521 | title: "Start from origin", |
| 522 | to: "/settings/general", |
| 523 | scope: "project-defaults", |
| 524 | searchTerms: ["new worktrees latest matching remote branch local"], |
| 525 | }, |
| 526 | { |
| 527 | id: "add-project-starts-in", |
| 528 | title: "Add project starts in", |
| 529 | to: "/settings/general", |
| 530 | scope: "environment-defaults", |
| 531 | searchTerms: ["base directory folder browser path home"], |
| 532 | }, |
| 533 | { |
| 534 | id: "unpin-confirmation", |
| 535 | title: "Unpin confirmation", |
| 536 | to: "/settings/general", |
| 537 | searchTerms: ["ask before thread pinned section"], |
| 538 | }, |
| 539 | { |
| 540 | id: "archive-confirmation", |
| 541 | title: "Archive confirmation", |
| 542 | to: "/settings/general", |
| 543 | searchTerms: ["ask before thread second click inline action"], |
| 544 | }, |
| 545 | { |
| 546 | id: "delete-confirmation", |
| 547 | title: "Delete confirmation", |
| 548 | to: "/settings/general", |
| 549 | searchTerms: ["ask before thread chat history"], |
| 550 | }, |
| 551 | { |
| 552 | id: "quit-confirmation", |
| 553 | title: "Quit shortcut", |
| 554 | to: "/settings/general", |
| 555 | searchTerms: ["confirmation desktop app exit direct hold double click press twice"], |
| 556 | desktopOnly: true, |
| 557 | }, |
| 558 | { |
| 559 | id: "text-generation-model", |
| 560 | title: "Text generation model", |
| 561 | to: "/settings/general", |
| 562 | scope: "project-defaults", |
| 563 | searchTerms: ["generated thread titles source control content default provider"], |
| 564 | }, |
| 565 | { |
| 566 | id: "app-version", |
| 567 | title: "Version", |
| 568 | to: "/settings/general", |
| 569 | searchTerms: ["about check for updates download install upgrade release"], |
| 570 | }, |
| 571 | { |
| 572 | id: "update-track", |
| 573 | title: "Update track", |
| 574 | to: "/settings/general", |
| 575 | searchTerms: ["release channel stable latest nightly prerelease"], |
| 576 | releaseChannelOnly: true, |
| 577 | }, |
| 578 | { |
| 579 | id: "cli-command", |
| 580 | title: "t3 command", |
| 581 | to: "/settings/general", |
| 582 | searchTerms: ["cli terminal shell path install command line"], |
| 583 | desktopOnly: true, |
| 584 | }, |
| 585 | { |
| 586 | id: "privacy-policy", |
| 587 | title: "Privacy policy", |
| 588 | to: "/settings/general", |
| 589 | searchTerms: ["telemetry analytics usage data tracking legal opt out"], |
| 590 | }, |
| 591 | { |
| 592 | id: "telemetry-export", |
| 593 | title: "OpenTelemetry export", |
| 594 | to: "/settings/diagnostics", |
| 595 | targetId: "telemetry-export", |
| 596 | scope: "environment", |
| 597 | searchTerms: ["OTel OTLP telemetry export endpoints logs traces metrics Grafana"], |
| 598 | }, |
| 599 | { |
| 600 | id: "diagnostics", |
| 601 | title: "Diagnostics", |
| 602 | to: "/settings/general", |
| 603 | searchTerms: ["logs traces processes resource history failures spans cpu memory"], |
| 604 | }, |
| 605 | { |
| 606 | id: "open-source-licenses", |
| 607 | title: "Open source licenses", |
| 608 | to: "/settings/general", |
| 609 | }, |
| 610 | { |
| 611 | id: "legacy-plan-mode", |
| 612 | title: "Plan mode (legacy)", |
| 613 | to: "/settings/general", |
| 614 | searchTerms: ["build plan composer old"], |
| 615 | }, |
| 616 | { |
| 617 | id: "legacy-context-window-indicator", |
| 618 | title: "Context window indicator (legacy)", |
| 619 | to: "/settings/general", |
| 620 | searchTerms: ["composer meter usage tokens circle old"], |
| 621 | }, |
| 622 | { |
| 623 | id: "legacy-sidebar", |
| 624 | title: "Sidebar (legacy)", |
| 625 | to: "/settings/general", |
| 626 | searchTerms: ["project thread tree old flat list"], |
| 627 | }, |
| 628 | { |
| 629 | id: "keybindings", |
| 630 | title: "Keybindings", |
| 631 | to: "/settings/keybindings", |
| 632 | searchTerms: ["keyboard shortcuts hotkeys commands bindings json"], |
| 633 | }, |
| 634 | ...KEYBINDING_SEARCH_ITEMS, |
| 635 | { |
| 636 | id: "snap-shot-enabled", |
| 637 | title: "SnapShots", |
| 638 | searchTerms: ["window capture screenshot"], |
| 639 | to: "/settings/snap-shot", |
| 640 | }, |
| 641 | { |
| 642 | id: "snap-shot-accessibility", |
| 643 | title: "Include app text", |
| 644 | to: "/settings/snap-shot", |
| 645 | targetId: "snap-shot-enabled", |
| 646 | searchTerms: [ |
| 647 | "capture accessibility data text UI structure elements privacy omit agent context", |
| 648 | ], |
| 649 | }, |
| 650 | { |
| 651 | id: "snap-shot-shortcut", |
| 652 | title: "Capture shortcut", |
| 653 | to: "/settings/snap-shot", |
| 654 | targetId: "snap-shot-enabled", |
| 655 | }, |
| 656 | { |
| 657 | id: "snap-shot-sound", |
| 658 | title: "Capture sound", |
| 659 | to: "/settings/snap-shot", |
| 660 | targetId: "snap-shot-enabled", |
| 661 | }, |
| 662 | { |
| 663 | id: "snap-shot-flash", |
| 664 | title: "Capture flash", |
| 665 | to: "/settings/snap-shot", |
| 666 | targetId: "snap-shot-enabled", |
| 667 | }, |
| 668 | { |
| 669 | id: "snap-shot-animations", |
| 670 | title: "Capture animations", |
| 671 | to: "/settings/snap-shot", |
| 672 | targetId: "snap-shot-enabled", |
| 673 | }, |
| 674 | { |
| 675 | id: "providers", |
| 676 | title: "Providers", |
| 677 | to: "/settings/providers", |
| 678 | searchTerms: [ |
| 679 | "agents cli codex claude cursor grok opencode antigravity google sign in sign out install subscription instances authentication api key models configuration binary path config directory endpoint arguments environment variables display name accent color custom favorite hidden auto compact", |
| 680 | ], |
| 681 | }, |
| 682 | { |
| 683 | id: "usage-providers", |
| 684 | title: "Usage providers", |
| 685 | to: "/settings/providers", |
| 686 | searchTerms: [ |
| 687 | "usage sources CLIProxyAPI CLI proxy hub quota subscription limits management key add remove", |
| 688 | ], |
| 689 | providerSettingsOnly: true, |
| 690 | }, |
| 691 | { |
| 692 | id: "cursor-keychain-usage", |
| 693 | title: "Cursor account usage", |
| 694 | to: "/settings/providers", |
| 695 | searchTerms: ["cursor macOS keychain usage tokens cost limits permission"], |
| 696 | providerSettingsOnly: true, |
| 697 | macProviderSettingsOnly: true, |
| 698 | }, |
| 699 | { |
| 700 | id: "provider-health-check-interval", |
| 701 | title: "Health check interval", |
| 702 | to: "/settings/providers", |
| 703 | searchTerms: ["refresh availability versions auth state models background probes seconds off"], |
| 704 | providerSettingsOnly: true, |
| 705 | }, |
| 706 | { |
| 707 | id: "agent-browser-access", |
| 708 | title: "Agent browser access", |
| 709 | to: "/settings/integrations", |
| 710 | scope: "project-defaults", |
| 711 | searchTerms: ["allow disable enable open drive preview tools sessions project override"], |
| 712 | }, |
| 713 | { |
| 714 | id: "device-hosts", |
| 715 | title: "Device hosts", |
| 716 | to: "/settings/integrations", |
| 717 | searchTerms: ["ssh remote simulator emulator ios android mac mini identity key connection"], |
| 718 | }, |
| 719 | { |
| 720 | id: "agent-device-access", |
| 721 | title: "Agent device access", |
| 722 | to: "/settings/integrations", |
| 723 | targetId: "devices", |
| 724 | searchTerms: ["allow simulator emulator ios android drive tools sessions"], |
| 725 | }, |
| 726 | { |
| 727 | id: "device-hub", |
| 728 | title: "Device hub", |
| 729 | to: "/settings/integrations", |
| 730 | targetId: "devices", |
| 731 | searchTerms: ["simulator emulator ios android install start"], |
| 732 | }, |
| 733 | { |
| 734 | id: "device-platform-support", |
| 735 | title: "Simulator support", |
| 736 | to: "/settings/integrations", |
| 737 | targetId: "devices", |
| 738 | searchTerms: ["xcode android studio sdk avd runtime"], |
| 739 | }, |
| 740 | { |
| 741 | id: "browser-profiles", |
| 742 | title: "Browser profiles", |
| 743 | to: "/settings/integrations", |
| 744 | targetId: "browser", |
| 745 | }, |
| 746 | { |
| 747 | id: "browser-default-profile", |
| 748 | title: "Default browser profile", |
| 749 | to: "/settings/integrations", |
| 750 | targetId: "browser-profiles", |
| 751 | }, |
| 752 | { |
| 753 | id: "browser-default-viewport", |
| 754 | title: "Default browser viewport", |
| 755 | to: "/settings/integrations", |
| 756 | searchTerms: ["preview size width height device desktop mobile rotate"], |
| 757 | }, |
| 758 | { |
| 759 | id: "browser-default-zoom", |
| 760 | title: "Default browser zoom", |
| 761 | to: "/settings/integrations", |
| 762 | searchTerms: ["preview page scale tabs percent"], |
| 763 | }, |
| 764 | { |
| 765 | id: "browser-default-appearance", |
| 766 | title: "Default browser appearance", |
| 767 | to: "/settings/integrations", |
| 768 | searchTerms: ["preview color scheme light dark system os"], |
| 769 | }, |
| 770 | { |
| 771 | id: "browser-recording-frame-rate", |
| 772 | title: "Browser recording frame rate", |
| 773 | to: "/settings/integrations", |
| 774 | }, |
| 775 | { |
| 776 | id: "browser-recording-key-presses", |
| 777 | title: "Show key presses in recordings", |
| 778 | to: "/settings/integrations", |
| 779 | searchTerms: ["browser preview keyboard shortcuts keystrokes overlay capture"], |
| 780 | }, |
| 781 | { |
| 782 | id: "browser-recording-mouse-presses", |
| 783 | title: "Show mouse presses in recordings", |
| 784 | to: "/settings/integrations", |
| 785 | searchTerms: ["browser preview clicks buttons drag overlay capture"], |
| 786 | }, |
| 787 | { |
| 788 | id: "browser-link-target", |
| 789 | title: "Open links in", |
| 790 | to: "/settings/integrations", |
| 791 | searchTerms: ["links default browser in-app browser external open"], |
| 792 | }, |
| 793 | { |
| 794 | id: "browser-auto-show-floating-preview", |
| 795 | title: "Auto-show floating preview", |
| 796 | to: "/settings/integrations", |
| 797 | searchTerms: ["agent opens browser device simulator pop into view hide"], |
| 798 | }, |
| 799 | { |
| 800 | id: "automatic-pull", |
| 801 | title: "Automatically pull", |
| 802 | to: "/settings/source-control", |
| 803 | scope: "project-defaults", |
| 804 | searchTerms: ["auto pull default branch current checkout fast forward upstream"], |
| 805 | }, |
| 806 | { |
| 807 | id: "remove-agent-credits-on-merge", |
| 808 | title: "Remove agent credits when merging", |
| 809 | to: "/settings/source-control", |
| 810 | scope: "project-defaults", |
| 811 | searchTerms: ["pull request github squash co-authored-by attribution claude codex generated"], |
| 812 | }, |
| 813 | { |
| 814 | id: "pull-request-merge-method", |
| 815 | title: "Default merge method", |
| 816 | to: "/settings/source-control", |
| 817 | scope: "project-defaults", |
| 818 | searchTerms: ["pull request merge squash rebase last selected"], |
| 819 | }, |
| 820 | { |
| 821 | id: "source-control", |
| 822 | title: "Source control", |
| 823 | to: "/settings/source-control", |
| 824 | scope: "environment-defaults", |
| 825 | searchTerms: [ |
| 826 | "version control git github gitlab forgejo gitea tea codeberg bitbucket azure devops hosting integrations credentials scan server environment", |
| 827 | ], |
| 828 | }, |
| 829 | { |
| 830 | id: "git-fetch-interval", |
| 831 | title: "Git fetch interval", |
| 832 | to: "/settings/source-control", |
| 833 | searchTerms: [ |
| 834 | "automatic remote branch refresh background credentials security keys seconds off", |
| 835 | ], |
| 836 | environmentOnly: true, |
| 837 | scope: "environment-defaults", |
| 838 | }, |
| 839 | { |
| 840 | id: "worktree-branch-naming", |
| 841 | title: "Worktree branch naming", |
| 842 | to: "/settings/source-control", |
| 843 | searchTerms: ["static semantic prefix custom prompt instructions feat fix refactor chore"], |
| 844 | environmentOnly: true, |
| 845 | scope: "project-defaults", |
| 846 | }, |
| 847 | { |
| 848 | id: "github-accounts", |
| 849 | title: "GitHub accounts and token", |
| 850 | to: "/settings/source-control", |
| 851 | searchTerms: [ |
| 852 | "github gh account login user host enterprise ghes switch multiple accounts disable sign in token personal access token pat api key credential", |
| 853 | ], |
| 854 | environmentOnly: true, |
| 855 | scope: "environment-defaults", |
| 856 | }, |
| 857 | ...SOURCE_CONTROL_HOST_SEARCH_ITEMS, |
| 858 | { |
| 859 | id: "source-control-writing-style", |
| 860 | title: "Source control writing style", |
| 861 | to: "/settings/source-control", |
| 862 | searchTerms: [ |
| 863 | "repository conventions conventional commits custom instructions change descriptions request titles", |
| 864 | ], |
| 865 | environmentOnly: true, |
| 866 | }, |
| 867 | { |
| 868 | id: "follow-change-request-templates", |
| 869 | title: "Follow change request templates", |
| 870 | to: "/settings/source-control", |
| 871 | searchTerms: ["repository pr pull request description structure"], |
| 872 | environmentOnly: true, |
| 873 | }, |
| 874 | { |
| 875 | id: "source-control-writer-model", |
| 876 | title: "Source control writer model", |
| 877 | to: "/settings/source-control", |
| 878 | searchTerms: [ |
| 879 | "override generated commit change request pr titles descriptions branch bookmark", |
| 880 | ], |
| 881 | environmentOnly: true, |
| 882 | scope: "project-defaults", |
| 883 | }, |
| 884 | { |
| 885 | id: "review-guides", |
| 886 | title: "Review guides (beta)", |
| 887 | to: "/settings/source-control", |
| 888 | searchTerms: ["pull request pr guided review guide tour beta experimental"], |
| 889 | environmentOnly: true, |
| 890 | }, |
| 891 | { |
| 892 | id: "review-guide-model", |
| 893 | title: "Review guide model", |
| 894 | to: "/settings/source-control", |
| 895 | // Hidden until review guides are on, so a search lands on the switch that shows it. |
| 896 | targetId: "review-guides", |
| 897 | searchTerms: ["pull request pr guided review guide brief maintainer"], |
| 898 | environmentOnly: true, |
| 899 | scope: "project-defaults", |
| 900 | }, |
| 901 | { |
| 902 | id: "review-guide-instructions", |
| 903 | title: "Review guide instructions", |
| 904 | to: "/settings/source-control", |
| 905 | targetId: "review-guides", |
| 906 | searchTerms: ["pull request pr guided review guide preferences taste prompt custom"], |
| 907 | environmentOnly: true, |
| 908 | scope: "project-defaults", |
| 909 | }, |
| 910 | { |
| 911 | id: "project-actions", |
| 912 | title: "Actions", |
| 913 | to: "/settings/projects", |
| 914 | searchTerms: ["commands scripts setup run dev server checkout worktree t3.json import"], |
| 915 | }, |
| 916 | { |
| 917 | id: "environment-icon", |
| 918 | title: "Environment icon", |
| 919 | to: "/settings/connections", |
| 920 | targetId: "connections-environment", |
| 921 | searchTerms: ["machine glyph sidebar mac mini studio laptop desktop server cloud vm"], |
| 922 | }, |
| 923 | { |
| 924 | id: "local-environment", |
| 925 | title: "Local environment", |
| 926 | to: "/settings/connections", |
| 927 | targetId: "connections-environment", |
| 928 | searchTerms: ["turn off on disable enable local server agents remote only restart"], |
| 929 | desktopOnly: true, |
| 930 | }, |
| 931 | { |
| 932 | id: "network-access", |
| 933 | title: "Network access", |
| 934 | to: "/settings/connections", |
| 935 | targetId: "connections-environment", |
| 936 | searchTerms: ["expose backend remote pairing local machine interfaces host restart"], |
| 937 | localBackendManagementOnly: true, |
| 938 | }, |
| 939 | { |
| 940 | id: "tailscale-https", |
| 941 | title: "Tailscale HTTPS", |
| 942 | to: "/settings/connections", |
| 943 | targetId: "connections-environment", |
| 944 | searchTerms: ["serve magicdns endpoint remote secure network"], |
| 945 | desktopOnly: true, |
| 946 | localBackendManagementOnly: true, |
| 947 | }, |
| 948 | { |
| 949 | id: "wsl-backend", |
| 950 | title: "WSL backend", |
| 951 | to: "/settings/connections", |
| 952 | searchTerms: [ |
| 953 | "windows subsystem linux distro second server projects stop windows backend restart", |
| 954 | ], |
| 955 | desktopOnly: true, |
| 956 | windowsOnly: true, |
| 957 | localBackendManagementOnly: true, |
| 958 | wslAvailableOnly: true, |
| 959 | }, |
| 960 | { |
| 961 | id: "t3-connect", |
| 962 | localEnvironmentOnly: true, |
| 963 | title: "T3 Connect", |
| 964 | to: "/settings/connections", |
| 965 | targetId: "connections-environment", |
| 966 | searchTerms: ["managed tunnel cloud other devices remote"], |
| 967 | desktopOnly: true, |
| 968 | cloudOnly: true, |
| 969 | }, |
| 970 | { |
| 971 | id: "hold-webhooks-while-offline", |
| 972 | localEnvironmentOnly: true, |
| 973 | title: "Hold webhooks while offline", |
| 974 | to: "/settings/connections", |
| 975 | targetId: "connections-environment", |
| 976 | searchTerms: ["webhook automations offline queue mailbox t3 connect"], |
| 977 | cloudOnly: true, |
| 978 | managedTunnelOnly: true, |
| 979 | }, |
| 980 | { |
| 981 | id: "publish-agent-activity", |
| 982 | localEnvironmentOnly: true, |
| 983 | title: "Publish agent activity", |
| 984 | to: "/settings/connections", |
| 985 | targetId: "connections-environment", |
| 986 | searchTerms: ["mobile push notifications live activities cloud tunnel"], |
| 987 | cloudOnly: true, |
| 988 | }, |
| 989 | { |
| 990 | id: "connections-environment", |
| 991 | title: "This machine", |
| 992 | to: "/settings/connections", |
| 993 | searchTerms: [ |
| 994 | "connections server backend local remote access administrative permissions scope pairing links qr code authorized clients sessions revoke endpoint", |
| 995 | ], |
| 996 | }, |
| 997 | { |
| 998 | id: "remote-environments", |
| 999 | title: "Environments", |
| 1000 | to: "/settings/connections", |
| 1001 | searchTerms: [ |
| 1002 | "add pair backend host code ssh config agent tunnel saved t3 connect self hosted self-hosted server hub", |
| 1003 | ], |
| 1004 | }, |
| 1005 | { |
| 1006 | id: "load-balancing", |
| 1007 | title: "Load balancing", |
| 1008 | to: "/settings/connections", |
| 1009 | searchTerms: [ |
| 1010 | "automatic machine environment resources cpu memory capacity preference weight shared projects", |
| 1011 | ], |
| 1012 | }, |
| 1013 | { |
| 1014 | id: "github-routing", |
| 1015 | title: "GitHub sharing", |
| 1016 | to: "/settings/connections", |
| 1017 | searchTerms: ["pull request trusted environments shared credentials permissions read actions"], |
| 1018 | }, |
| 1019 | { |
| 1020 | id: "archive", |
| 1021 | title: "Archived threads", |
| 1022 | to: "/settings/archived", |
| 1023 | searchTerms: ["restore reopen deleted history projects"], |
| 1024 | }, |
| 1025 | ] as const satisfies ReadonlyArray<SettingsSearchItem>; |
| 1026 | |
| 1027 | export type SettingsSearchItemId = (typeof SETTINGS_SEARCH_ITEMS)[number]["id"]; |
| 1028 | |
| 1029 | const SEARCH_ITEMS_BY_ID = new Map(SETTINGS_SEARCH_ITEMS.map((item) => [item.id, item] as const)); |
| 1030 | |
| 1031 | const SETTINGS_CATEGORY_SCOPES: Readonly<Record<SettingsPath, SettingsSearchScope | null>> = { |
| 1032 | "/settings/diagnostics": "environment", |
| 1033 | "/settings/projects": "project", |
| 1034 | "/settings/general": null, |
| 1035 | "/settings/appearance": null, |
| 1036 | "/settings/snap-shot": null, |
| 1037 | // Keybindings fan out to the selection; Providers shows the representative |
| 1038 | // environment at any selection. Neither needs a particular scope to render. |
| 1039 | "/settings/keybindings": null, |
| 1040 | "/settings/providers": null, |
| 1041 | "/settings/integrations": null, |
| 1042 | "/settings/source-control": "environment-defaults", |
| 1043 | "/settings/storage": "project-defaults", |
| 1044 | "/settings/connections": "connections", |
| 1045 | "/settings/scheduled-tasks": null, |
| 1046 | "/settings/archived": "project-defaults", |
| 1047 | }; |
| 1048 | |
| 1049 | /** Search keeps the selected target. A missing row can explain its owning scope instead. */ |
| 1050 | export function getSettingsSearchTargetScope(targetId: string) { |
| 1051 | const items: readonly SettingsSearchItem[] = SETTINGS_SEARCH_ITEMS; |
| 1052 | const item = |
| 1053 | items.find((candidate) => candidate.id === targetId) ?? |
| 1054 | items.find((candidate) => candidate.targetId === targetId); |
| 1055 | return item |
| 1056 | ? { |
| 1057 | title: item.title, |
| 1058 | scope: item.scope ?? SETTINGS_CATEGORY_SCOPES[item.to], |
| 1059 | ...(item.requiresThreadAutoSettlement ? { requiresThreadAutoSettlement: true } : {}), |
| 1060 | } |
| 1061 | : null; |
| 1062 | } |
| 1063 | |
| 1064 | interface AutoSettlementSearchEnvironment { |
| 1065 | readonly environmentId: EnvironmentId; |
| 1066 | readonly connection: { readonly phase: EnvironmentConnectionPhase }; |
| 1067 | readonly serverConfig: { |
| 1068 | readonly environment: { |
| 1069 | readonly capabilities: { readonly threadAutoSettlement?: boolean }; |
| 1070 | }; |
| 1071 | } | null; |
| 1072 | } |
| 1073 | |
| 1074 | /** Discovery needs one capable environment; the selected page needs every connected target to support it. */ |
| 1075 | export function getThreadAutoSettlementSearchAvailability( |
| 1076 | environments: readonly AutoSettlementSearchEnvironment[], |
| 1077 | scope?: Pick<ResolvedSettingsScope, "kind" | "environmentIds">, |
| 1078 | ) { |
| 1079 | const connected = environments.filter( |
| 1080 | (environment) => |
| 1081 | environment.connection.phase === "connected" && environment.serverConfig !== null, |
| 1082 | ); |
| 1083 | const eligibleEnvironmentIds = connected |
| 1084 | .filter( |
| 1085 | (environment) => |
| 1086 | environment.serverConfig?.environment.capabilities.threadAutoSettlement === true, |
| 1087 | ) |
| 1088 | .map((environment) => environment.environmentId); |
| 1089 | const selected = connected.filter((environment) => |
| 1090 | scope?.environmentIds.includes(environment.environmentId), |
| 1091 | ); |
| 1092 | return { |
| 1093 | eligibleEnvironmentIds, |
| 1094 | isTargetAvailable: |
| 1095 | scope !== undefined && |
| 1096 | scope.kind !== "unavailable" && |
| 1097 | selected.length > 0 && |
| 1098 | selected.every((environment) => eligibleEnvironmentIds.includes(environment.environmentId)), |
| 1099 | }; |
| 1100 | } |
| 1101 | |
| 1102 | export function isSettingsSearchScopeAvailable( |
| 1103 | requiredScope: SettingsSearchScope | null, |
| 1104 | scopeKind: ResolvedSettingsScope["kind"], |
| 1105 | ): boolean { |
| 1106 | switch (requiredScope) { |
| 1107 | case null: |
| 1108 | case "connections": |
| 1109 | return true; |
| 1110 | case "environment": |
| 1111 | case "checkout": |
| 1112 | return requiredScope === scopeKind; |
| 1113 | case "project": |
| 1114 | return scopeKind === "project" || scopeKind === "checkout"; |
| 1115 | case "environment-defaults": |
| 1116 | return scopeKind === "environment" || scopeKind === "all"; |
| 1117 | case "project-defaults": |
| 1118 | return ( |
| 1119 | scopeKind === "environment" || |
| 1120 | scopeKind === "all" || |
| 1121 | scopeKind === "project" || |
| 1122 | scopeKind === "checkout" |
| 1123 | ); |
| 1124 | } |
| 1125 | } |
| 1126 | |
| 1127 | function settingsScopeKindFromSearch(search: SettingsScopeSearch): ResolvedSettingsScope["kind"] { |
| 1128 | const target = validateSettingsScopeSearch({ ...search }); |
| 1129 | if (target.checkout && !target.project) return "unavailable"; |
| 1130 | if (target.project) return target.checkout ? "checkout" : "project"; |
| 1131 | return target.machine ? "environment" : "all"; |
| 1132 | } |
| 1133 | |
| 1134 | export function isSettingsOverviewVisible(search: SettingsScopeSearch): boolean { |
| 1135 | const kind = settingsScopeKindFromSearch(search); |
| 1136 | return kind === "project" || kind === "checkout"; |
| 1137 | } |
| 1138 | |
| 1139 | /** |
| 1140 | * `id` and `title` props for the element a search item anchors to. Panels |
| 1141 | * spread (or pick from) this instead of restating the strings, so the catalog |
| 1142 | * and the rendered settings cannot drift apart. |
| 1143 | */ |
| 1144 | export function searchableSetting(id: SettingsSearchItemId): { |
| 1145 | readonly id: string; |
| 1146 | readonly title: string; |
| 1147 | } { |
| 1148 | const { id: anchorId, title } = SEARCH_ITEMS_BY_ID.get(id)!; |
| 1149 | return { id: anchorId, title }; |
| 1150 | } |
| 1151 | |
| 1152 | export function filterAvailableSettingsSearchItems( |
| 1153 | availability: SettingsSearchAvailability, |
| 1154 | ): ReadonlyArray<SettingsSearchItem> { |
| 1155 | const items: ReadonlyArray<SettingsSearchItem> = SETTINGS_SEARCH_ITEMS; |
| 1156 | return items.filter( |
| 1157 | (item) => |
| 1158 | item.to !== "/settings/snap-shot" && |
| 1159 | (!item.cloudOnly || availability.hasCloudPublicConfig) && |
| 1160 | (!item.environmentOnly || availability.hasEnvironment) && |
| 1161 | (!item.providerSettingsOnly || availability.hasProviderSettingsEnvironment) && |
| 1162 | (!item.macProviderSettingsOnly || availability.hasMacProviderSettingsEnvironment) && |
| 1163 | (!item.localBackendManagementOnly || availability.canManageLocalBackend) && |
| 1164 | (!item.localEnvironmentOnly || !availability.localEnvironmentDisabled) && |
| 1165 | (!item.wslAvailableOnly || availability.isWslSettingsRowVisible) && |
| 1166 | (!item.requiresThreadAutoSettlement || availability.hasThreadAutoSettlement) && |
| 1167 | (!item.managedTunnelOnly || availability.managedTunnelActive === true), |
| 1168 | ); |
| 1169 | } |
| 1170 | |
| 1171 | export function searchSettings( |
| 1172 | query: string, |
| 1173 | items: ReadonlyArray<SettingsSearchItem> = SETTINGS_SEARCH_ITEMS, |
| 1174 | ): ReadonlyArray<SettingsSearchItem> { |
| 1175 | const normalizedQuery = normalizeSearchText(query); |
| 1176 | if (normalizedQuery.length === 0) return []; |
| 1177 | const queryTokens = normalizedQuery.split(" "); |
| 1178 | const platform = typeof navigator === "undefined" ? "" : navigator.platform; |
| 1179 | |
| 1180 | return items |
| 1181 | .flatMap((item, index) => { |
| 1182 | if (item.to === "/settings/snap-shot") return []; |
| 1183 | if (!isElectron && item.desktopOnly === true) return []; |
| 1184 | if (item.releaseChannelOnly && !isElectron && HOSTED_APP_CHANNEL === null) return []; |
| 1185 | if (item.macOnly && !isMacPlatform(platform)) return []; |
| 1186 | if (item.windowsOnly && !isWindowsPlatform(platform)) return []; |
| 1187 | |
| 1188 | const title = normalizeSearchText(item.title); |
| 1189 | const fields = [ |
| 1190 | title, |
| 1191 | normalizeSearchText(SETTINGS_SECTION_LABELS[item.to]), |
| 1192 | ...(item.searchTerms ?? []).map(normalizeSearchText), |
| 1193 | ]; |
| 1194 | if (!queryTokens.every((token) => fields.some((field) => field.includes(token)))) return []; |
| 1195 | |
| 1196 | const exactPhraseField = fields.findIndex((field) => field.includes(normalizedQuery)); |
| 1197 | const rank = |
| 1198 | title === normalizedQuery |
| 1199 | ? 5 |
| 1200 | : title.startsWith(normalizedQuery) |
| 1201 | ? 4 |
| 1202 | : title.includes(normalizedQuery) |
| 1203 | ? 3 |
| 1204 | : queryTokens.every((token) => title.includes(token)) |
| 1205 | ? 2 |
| 1206 | : exactPhraseField >= 0 |
| 1207 | ? 1 |
| 1208 | : 0; |
| 1209 | return [{ item, index, rank }]; |
| 1210 | }) |
| 1211 | .toSorted( |
| 1212 | (left, right) => |
| 1213 | Number(left.item.secondary ?? false) - Number(right.item.secondary ?? false) || |
| 1214 | right.rank - left.rank || |
| 1215 | left.index - right.index, |
| 1216 | ) |
| 1217 | .map(({ item }) => item); |
| 1218 | } |