1import { HOSTED_APP_CHANNEL } from "~/branding";
2import { isElectron } from "~/env";
3import { isMacPlatform, isWindowsPlatform, normalizeSearchText } from "~/lib/utils";
4import { STATIC_KEYBINDING_COMMANDS, type KeybindingCommand } from "@t3tools/contracts";
5import type { EnvironmentId } from "@t3tools/contracts";
6import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection";
7import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients";
8import { DEFAULT_KEYBINDINGS } from "@t3tools/shared/keybindings";
9import { commandLabel } from "./KeybindingsSettings.logic";
10import {
11 validateSettingsScopeSearch,
12 type ResolvedSettingsScope,
13 type SettingsScopeSearch,
14} from "./settingsScope";
15
16export 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 */
36export type SettingsSearchScope =
37 | "environment"
38 | "environment-defaults"
39 | "project-defaults"
40 | "project"
41 | "checkout"
42 | "connections";
43
44export 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
79export 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 */
95export 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. */
112export 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 */
122const 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. */
139export 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. */
144const 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 */
166export 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
1027export type SettingsSearchItemId = (typeof SETTINGS_SEARCH_ITEMS)[number]["id"];
1028
1029const SEARCH_ITEMS_BY_ID = new Map(SETTINGS_SEARCH_ITEMS.map((item) => [item.id, item] as const));
1030
1031const 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. */
1050export 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
1064interface 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. */
1075export 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
1102export 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
1127function 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
1134export 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 */
1144export 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
1152export 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
1171export 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}