From a37ff2fa6238310b5e930ba4bb209123b5fb89d2 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:39:39 -0800 Subject: [PATCH] Show matching settings as a results list in Settings search (#40852) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Show matching settings as a results list in the Settings sidebar Typing in the Settings search now replaces the nav with a ranked list of individual settings (pane title on the second line, matched text marked). The first result is selected and previewed: its pane opens unfiltered, scrolled to the setting with the target highlight. Arrow keys move and preview, Enter or a click picks and keeps the query, Esc or the clear button returns to the nav while leaving the shown pane open. The sidebar query no longer filters page content. The Back to app row is now 48px so its border lines up with the Settings header. * Highlight settings search targets with a full-bleed bar and find unanchored rows Matches in the results list are marked with semibold text (the jump palette's HighlightedText) instead of a grey box. The landed-on row gets a tint from card edge to card edge with a left bar that fades after ~3s, replacing the inset grey block that sat off-center in the card padding. Integration provider rows get anchors, and rows not wrapped in SearchableSetting are found by their visible label. * Fit the search highlight to each row's enclosing card Settings cards pad differently (groups, plain sections, shadcn cards, the Appearance accordion), so the highlight now measures the gap from the row to its nearest card's inner edges instead of assuming one gutter. A group title match highlights that group's card rather than the heading text. * Give the settings target highlight a single owner Components applied the highlight through SettingsTargetHighlightContext while the page effect applied it to whatever element the jump scrolled to; when those differed (Jira's provider row vs. its card shell) two highlights showed. The effect now owns it alone, and the context, its provider and the macOS Permissions prop are removed. Jira and Linear reuse their existing anchor ids. * Use a plain fading tint for the settings target highlight Drops the left bar; the tint still spans the enclosing card and fades after ~3s. * Mark settings result titles with the ranking fold The title highlight lowercased by hand and skipped titles whose length changed, so İstanbul matching "stan" (or a query of İ) ranked but was never marked. Fold per code point with the ranking's own fold and map offsets back to the title. * Make settings search list-only until a pick and reveal collapsed targets Typing now only changes the result rows: nothing is selected and the page does not move until Enter or a click (arrows move the selection only). This removes previews that discarded the Open in draft, re-ran repo hook checks, mounted a pane per keystroke and drifted after deep links. - Rank once on a deferred query, cap the list at 50 with a 'keep typing' hint, and skip rendering the normal nav while searching. - A settings jump dispatches a reveal event from its target; Appearance sections, Advanced disclosures and the network/mobile collapsibles (now kept mounted) open themselves instead of reading the search query. - Nested scrollers (Shortcuts) scroll the target into view; General sub-page results open their page; mirror recovery deep-links to the mirror field; a target that never mounts drops its pending jump; group-title matches use data-settings-group. * Fix follow-ups from the settings search repair review - Two more e2e specs pick a result with ArrowDown + Enter instead of relying on typing to navigate. - After a jump reveals a growing section, re-scroll once if the row is still cut off. - Hide 'No settings match' while the deferred ranking catches up. - The reveal hook is a ref callback so disclosures that mount later still open. - The Advanced disclosure fixture shows the real forced-open case (unsaved edits). * Give every settings search result a reachable row * Type the General sub-page anchors as a list instead of casting map entries * Keep the reveal callback ref out of render * Match the e2e shortcut labels to main's sentence-case names * Highlight the result's card, not its section title or a bare label --- src/renderer/src/assets/main.css | 32 ++ .../settings/accounts/accounts-search.ts | 13 + .../codex/accounts-pane-codex-section.tsx | 4 +- .../providers/AntigravityAccountsSection.tsx | 3 +- .../providers/CursorAccountsSection.tsx | 3 +- .../providers/GrokAccountsSection.tsx | 3 +- .../accounts/usage/MiniMaxAccountsSection.tsx | 3 +- .../AdvancedNetworkSettingsSection.tsx | 22 +- .../settings/agents/agents-search.ts | 2 + .../launch/AgentLaunchDefaultsGroup.tsx | 3 +- .../appearance/AppearanceInterfaceSection.tsx | 3 + .../settings/appearance/appearance-search.ts | 3 + .../settings/browser/use/BrowserUsePane.tsx | 12 +- .../browser/use/browser-use-search.ts | 5 + .../settings/cli/install/CliSection.tsx | 37 ++- .../settings/computer-use/ComputerUsePane.tsx | 5 +- .../computer-use/computer-use-search.ts | 4 + .../DeveloperPermissionsPane.tsx | 18 +- .../developer-permissions-search.ts | 11 + .../form-controls/SettingsFormControls.tsx | 5 +- .../general/GeneralPane.navigation.test.tsx | 36 +-- .../settings/general/general-search.ts | 1 + .../general/general-setting-anchors.ts | 9 + .../integrations/IntegrationsPane.tsx | 22 +- .../integrations/integrations-search.ts | 22 ++ .../components/settings/mobile/MobilePane.tsx | 6 +- .../emulator/EmulatorOpeningSettings.tsx | 2 + .../emulator/MobileEmulatorSettingsPane.tsx | 8 +- .../mobile/emulator/mobile-emulator-search.ts | 9 + .../settings/mobile/mobile-pane-search.ts | 9 + .../pairing/MobilePairingSetupSection.tsx | 16 +- .../settings/navigation/SettingsSidebar.tsx | 301 +++++++++++------- .../navigation/SettingsSidebarSearch.tsx | 206 ++++++++++++ .../settings-deep-link-target-watcher.ts | 8 +- .../settings-navigation-foundations.ts | 50 ++- .../navigation/settings-navigation-types.ts | 1 + .../settings-search-results.test.ts | 53 +++ .../navigation/settings-search-results.ts | 93 ++++++ .../use-settings-navigation-model.ts | 21 ++ .../use-settings-repo-scroll-effects.ts | 13 +- .../settings/page/settings-page-renderer.tsx | 80 +++-- ...ings-remote-security-section-renderers.tsx | 6 +- .../settings/page/settings-view-model.ts | 44 ++- .../use-settings-interaction-controller.ts | 44 +++ .../privacy/PrivacyDiagnosticsSection.tsx | 7 +- .../settings/privacy/PrivacyPane.tsx | 9 +- .../settings/privacy/privacy-search.ts | 10 + .../quick-commands/QuickCommandsPane.tsx | 3 +- .../quick-commands/quick-commands-search.ts | 3 + .../repository/repository-settings-targets.ts | 3 + .../RepositorySourceControlAiSection.tsx | 6 +- .../repository-git-author-search-entries.ts | 2 + .../settings/search/settings-search.ts | 17 +- .../section-layout/SearchableSetting.tsx | 10 +- .../settings/section-layout/SettingsGroup.tsx | 7 +- .../SettingsGroupDisclosure.fixture.tsx | 7 +- .../SettingsGroupDisclosure.tsx | 20 +- .../settings-target-highlight.ts | 91 +++++- .../AutoRenameBranchFromWorkSetting.tsx | 14 +- .../HostedReviewCreationDefaults.tsx | 2 + .../auto-rename-branch-search.ts | 3 + .../commit-message-ai-search.ts | 3 + .../src/components/settings/ssh/SshPane.tsx | 3 +- .../settings/ssh/ssh-hosts-header-slot.tsx | 15 +- .../src/components/settings/ssh/ssh-search.ts | 9 + .../platform/TerminalMacKeyboardSection.tsx | 2 + .../terminal-advanced-platform-search.ts | 3 + .../terminal/themes/TerminalThemeSections.tsx | 2 + .../terminal/themes/terminal-theme-search.ts | 5 + .../voice/VoiceDictationLanguageSetting.tsx | 2 + .../settings/voice/VoiceDictationSettings.tsx | 2 + .../voice/VoiceSpeechModelSection.tsx | 2 + .../settings/voice/voice-pane-search.ts | 10 + src/renderer/src/i18n/locales/de.json | 3 +- src/renderer/src/i18n/locales/en.json | 7 +- src/renderer/src/i18n/locales/es.json | 3 +- src/renderer/src/i18n/locales/fr.json | 3 +- src/renderer/src/i18n/locales/ja.json | 3 +- src/renderer/src/i18n/locales/ko.json | 3 +- src/renderer/src/i18n/locales/pt.json | 3 +- src/renderer/src/i18n/locales/ru.json | 3 +- src/renderer/src/i18n/locales/tr.json | 3 +- src/renderer/src/i18n/locales/zh-Hant.json | 3 +- src/renderer/src/i18n/locales/zh.json | 1 - .../src/ssh/node-dist-mirror-recovery.ts | 14 +- .../slices/settings/settings-search-state.ts | 28 +- tests/e2e/claude-code-theme-sync.spec.ts | 12 +- ...nding-terminal-conflict-diagnostic.spec.ts | 7 +- .../settings-search-shortcuts-pane.spec.ts | 28 +- 89 files changed, 1262 insertions(+), 385 deletions(-) create mode 100644 src/renderer/src/components/settings/navigation/SettingsSidebarSearch.tsx create mode 100644 src/renderer/src/components/settings/navigation/settings-search-results.test.ts create mode 100644 src/renderer/src/components/settings/navigation/settings-search-results.ts diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 883d9061562..39c73d8af2b 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -2172,6 +2172,38 @@ html.native-shell .app-layout { } } +/* Search and deep-link target: a tint out to the enclosing card's edges that fades. + An inset shadow carries the tint so a highlighted card keeps its own fill and corners. + The bleed vars are measured per target, since nested and sub-row targets sit further in. */ +.settings-target-highlight { + --settings-target-tint: color-mix(in srgb, var(--foreground) 8%, transparent); + box-shadow: + inset 0 0 0 100vmax var(--settings-target-tint), + calc(-1 * var(--settings-target-bleed-left, 0px)) 0 0 var(--settings-target-tint), + var(--settings-target-bleed-right, 0px) 0 0 var(--settings-target-tint); +} + +@media (prefers-reduced-motion: no-preference) { + .settings-target-highlight { + animation: settings-target-highlight-fade 3s ease-out forwards; + } +} + +@keyframes settings-target-highlight-fade { + 60% { + box-shadow: + inset 0 0 0 100vmax var(--settings-target-tint), + calc(-1 * var(--settings-target-bleed-left, 0px)) 0 0 var(--settings-target-tint), + var(--settings-target-bleed-right, 0px) 0 0 var(--settings-target-tint); + } + 100% { + box-shadow: + inset 0 0 0 100vmax transparent, + calc(-1 * var(--settings-target-bleed-left, 0px)) 0 0 transparent, + var(--settings-target-bleed-right, 0px) 0 0 transparent; + } +} + @keyframes settings-shell-enter { from { opacity: 0; diff --git a/src/renderer/src/components/settings/accounts/accounts-search.ts b/src/renderer/src/components/settings/accounts/accounts-search.ts index dbf6bdb196f..01f6909a2bd 100644 --- a/src/renderer/src/components/settings/accounts/accounts-search.ts +++ b/src/renderer/src/components/settings/accounts/accounts-search.ts @@ -4,6 +4,14 @@ import { translateSearchKeyword } from '../search/settings-search-keywords' import { createLocalizedCatalog } from '@/i18n/localized-catalog' import { getAccountsClaudeSearchEntries } from './accounts-claude-search' +export const ACCOUNTS_SECTION_IDS = { + codex: 'accounts-codex', + minimax: 'accounts-minimax', + grok: 'accounts-grok', + antigravity: 'accounts-antigravity', + cursor: 'accounts-cursor' +} as const + export const getAccountsLocationSearchEntries = createLocalizedCatalog(() => [ { title: translate('auto.components.settings.accounts.search.5e2101f61f', 'Account location'), @@ -55,6 +63,7 @@ export const getAccountsCodexSearchEntries = createLocalizedCatalog(() => [ ] }, { + targetSectionId: ACCOUNTS_SECTION_IDS.codex, title: translate('auto.components.settings.accounts.search.4518496456', 'Active Codex account'), description: translate( 'auto.components.settings.accounts.search.87a4a8584e', @@ -153,6 +162,7 @@ export const getAccountsOpencodeSearchEntries = createLocalizedCatalog(() => [ export const getAccountsMiniMaxSearchEntries = createLocalizedCatalog(() => [ { + targetSectionId: ACCOUNTS_SECTION_IDS.minimax, title: translate('auto.components.settings.accounts.search.9b6ce52c36', 'MiniMax usage'), description: translate( 'auto.components.settings.accounts.search.f8374c3151', @@ -177,6 +187,7 @@ export const getAccountsMiniMaxSearchEntries = createLocalizedCatalog(() => [ export const getAccountsGrokSearchEntries = createLocalizedCatalog(() => [ { + targetSectionId: ACCOUNTS_SECTION_IDS.grok, title: translate('auto.components.settings.accounts.search.f4a8c2e1b7', 'Grok (xAI) Usage'), description: translate( 'auto.components.settings.accounts.search.e3b7d1f9a2', @@ -198,6 +209,7 @@ export const getAccountsGrokSearchEntries = createLocalizedCatalog(() => [ export const getAccountsAntigravitySearchEntries = createLocalizedCatalog(() => [ { + targetSectionId: ACCOUNTS_SECTION_IDS.antigravity, title: translate('accounts.antigravity.fd47b5a5dd', 'Antigravity accounts'), description: translate( 'accounts.antigravity.searchDescription', @@ -216,6 +228,7 @@ export const getAccountsAntigravitySearchEntries = createLocalizedCatalog(() => export const getAccountsCursorSearchEntries = createLocalizedCatalog(() => [ { + targetSectionId: ACCOUNTS_SECTION_IDS.cursor, title: translate('auto.components.settings.accounts.search.cursor.39bbdebe13', 'Cursor usage'), description: translate( 'auto.components.settings.accounts.search.cursor.description', diff --git a/src/renderer/src/components/settings/accounts/codex/accounts-pane-codex-section.tsx b/src/renderer/src/components/settings/accounts/codex/accounts-pane-codex-section.tsx index 7da61add753..9d225a2e59d 100644 --- a/src/renderer/src/components/settings/accounts/codex/accounts-pane-codex-section.tsx +++ b/src/renderer/src/components/settings/accounts/codex/accounts-pane-codex-section.tsx @@ -8,7 +8,7 @@ import { Label } from '../../../ui/label' import { OpenAIIcon } from '../../../status-bar/icons' import { CodexLoginLinkNotice } from './CodexLoginLinkNotice' import { SearchableSetting } from '../../section-layout/SearchableSetting' -import { getAccountsCodexSearchEntries } from '../accounts-search' +import { ACCOUNTS_SECTION_IDS, getAccountsCodexSearchEntries } from '../accounts-search' import { getCodexSystemDefaultSubtitle } from '../scope/accounts-pane-runtime' import type { AccountsPaneSectionModel } from '../accounts-pane-types' import { renderCodexAccountRow } from './accounts-pane-codex-account-row' @@ -47,7 +47,7 @@ export function renderCodexAccountsSection( wslCapabilitiesLoading } = model return ( -
+
diff --git a/src/renderer/src/components/settings/accounts/providers/AntigravityAccountsSection.tsx b/src/renderer/src/components/settings/accounts/providers/AntigravityAccountsSection.tsx index 8e85a588be5..2b7a98de921 100644 --- a/src/renderer/src/components/settings/accounts/providers/AntigravityAccountsSection.tsx +++ b/src/renderer/src/components/settings/accounts/providers/AntigravityAccountsSection.tsx @@ -26,6 +26,7 @@ import { useAntigravityChatSignIn } from '../../../provider-accounts/use-antigra import { ChatUnavailableOnWindowsNote } from '../../../agent/launch-settings/AgentChatChoiceControls' import { structuredChatUnavailableOnPlatform } from '../../../../../../shared/agent-session/structured-chat-platform-support' import { SettingsGroup } from '../../section-layout/SettingsGroup' +import { ACCOUNTS_SECTION_IDS } from '../accounts-search' import { AntigravityTerminalAccountRows, type AntigravityAccountAction @@ -204,7 +205,7 @@ export function AntigravityAccountsSection({ ) : null return ( -
+
diff --git a/src/renderer/src/components/settings/accounts/providers/CursorAccountsSection.tsx b/src/renderer/src/components/settings/accounts/providers/CursorAccountsSection.tsx index 2795b2555cb..611afe84edb 100644 --- a/src/renderer/src/components/settings/accounts/providers/CursorAccountsSection.tsx +++ b/src/renderer/src/components/settings/accounts/providers/CursorAccountsSection.tsx @@ -11,6 +11,7 @@ import type { CursorAccountStatus } from '../../../../../../shared/rate-limit-ty import { SearchableSetting } from '../../section-layout/SearchableSetting' import { openAppHttpLinkFromAnchor } from '@/lib/browser/navigation/app-http-link-open' import { SettingsGroup } from '../../section-layout/SettingsGroup' +import { ACCOUNTS_SECTION_IDS } from '../accounts-search' const CURSOR_USAGE_DASHBOARD_URL = 'https://cursor.com/dashboard/spending' const SEARCH_KEYWORDS = ['cursor', 'usage', 'plan', 'spend', 'billing', 'rate limit'] @@ -105,7 +106,7 @@ export function CursorAccountsSection(): React.JSX.Element { : null return ( -
+
diff --git a/src/renderer/src/components/settings/accounts/providers/GrokAccountsSection.tsx b/src/renderer/src/components/settings/accounts/providers/GrokAccountsSection.tsx index c60572732bf..4f3875e5ca2 100644 --- a/src/renderer/src/components/settings/accounts/providers/GrokAccountsSection.tsx +++ b/src/renderer/src/components/settings/accounts/providers/GrokAccountsSection.tsx @@ -10,6 +10,7 @@ import type { GrokAccountStatus } from '../../../../../../shared/rate-limit-type import { SearchableSetting } from '../../section-layout/SearchableSetting' import { openAppHttpLinkFromAnchor } from '@/lib/browser/navigation/app-http-link-open' import { SettingsGroup } from '../../section-layout/SettingsGroup' +import { ACCOUNTS_SECTION_IDS } from '../accounts-search' const GROK_CLI_DOCS_URL = 'https://docs.x.ai/build/overview' export function GrokAccountsSection(): React.JSX.Element { @@ -65,7 +66,7 @@ export function GrokAccountsSection(): React.JSX.Element { : null return ( -
+
diff --git a/src/renderer/src/components/settings/accounts/usage/MiniMaxAccountsSection.tsx b/src/renderer/src/components/settings/accounts/usage/MiniMaxAccountsSection.tsx index 66135b5fdce..561c70fad8a 100644 --- a/src/renderer/src/components/settings/accounts/usage/MiniMaxAccountsSection.tsx +++ b/src/renderer/src/components/settings/accounts/usage/MiniMaxAccountsSection.tsx @@ -17,6 +17,7 @@ import { } from './usage-credential-host' import type { UsageHostSettingsModel } from './use-usage-host-settings' import { SettingsGroup } from '../../section-layout/SettingsGroup' +import { ACCOUNTS_SECTION_IDS } from '../accounts-search' export function MiniMaxAccountsSection({ host, @@ -49,7 +50,7 @@ export function MiniMaxAccountsSection({ void usageSettings.update({ minimaxEndpoint: value }) } return ( -
+
diff --git a/src/renderer/src/components/settings/advanced/network/AdvancedNetworkSettingsSection.tsx b/src/renderer/src/components/settings/advanced/network/AdvancedNetworkSettingsSection.tsx index 3141bc949e0..7897dce49fb 100644 --- a/src/renderer/src/components/settings/advanced/network/AdvancedNetworkSettingsSection.tsx +++ b/src/renderer/src/components/settings/advanced/network/AdvancedNetworkSettingsSection.tsx @@ -18,6 +18,8 @@ import { Textarea } from '../../../ui/textarea' import { getAdvancedNetworkSearchEntries } from './advanced-network-search' import { NodeDistMirrorSettingField } from './NodeDistMirrorSettingField' import { SearchableSetting } from '../../section-layout/SearchableSetting' +import { useSettingsTargetReveal } from '../../section-layout/settings-target-highlight' +import { NODE_DIST_MIRROR_SETTINGS_TARGET_ID } from '../../navigation/settings-navigation-types' import { matchesSettingsSearch, normalizeSettingsSearchQuery } from '../../search/settings-search' import { translate } from '@/i18n/i18n' import { useTranslate, type Translate } from '@/i18n/use-translate' @@ -152,6 +154,7 @@ export function AdvancedNetworkSettingsSection({ const t = useTranslate() const searchQuery = useAppStore((s) => s.settingsSearchQuery) const [proxyConfigOpen, setProxyConfigOpen] = useState(false) + const proxyRevealRef = useSettingsTargetReveal(() => setProxyConfigOpen(true)) const proxyConfigForcedOpen = shouldOpenNetworkProxyConfig(searchQuery) || hasConfiguredNetworkProxy(settings) const proxyConfigExpanded = proxyConfigOpen || proxyConfigForcedOpen @@ -256,7 +259,11 @@ export function AdvancedNetworkSettingsSection({
- + - + {/* Mounted while closed so a settings jump can find and reveal the proxy fields. */} +
- updateSettings({ nodeDistMirrorUrl })} - /> +
+ updateSettings({ nodeDistMirrorUrl })} + /> +
diff --git a/src/renderer/src/components/settings/agents/agents-search.ts b/src/renderer/src/components/settings/agents/agents-search.ts index 0acfc7eb207..39c19793892 100644 --- a/src/renderer/src/components/settings/agents/agents-search.ts +++ b/src/renderer/src/components/settings/agents/agents-search.ts @@ -101,6 +101,7 @@ const AGENT_CHAT_CHOICE_SEARCH_ENTRY_ID = 'agent-chat-choice' export const AGENTS_INSTALLED_SECTION_ID = 'agents-installed' /** The top Chat or Terminal tiles that set this computer's default. */ export const AGENT_DEFAULT_SURFACE_SECTION_ID = 'agents-default-surface' +export const AGENT_PERMISSIONS_SECTION_ID = 'agents-permissions' const AGENT_WORKSPACE_TRUST_SEARCH_ENTRY_ID = 'agent-workspace-trust' const AGENT_RUNTIME_SEARCH_ENTRY_ID = 'agent-runtime' const CODEX_TERMINAL_SERVER_ISOLATION_SEARCH_ENTRY_ID = 'codex-terminal-server-isolation' @@ -212,6 +213,7 @@ const getAllAgentsPaneSearchEntries = createLocalizedCatalog(() => [ keywords: getAgentAwakeSearchKeywords() }, { + targetSectionId: AGENT_PERMISSIONS_SECTION_ID, title: translate('auto.components.settings.agents.search.788559d398', 'Agent permissions'), description: translate( 'settings.agents.permissionsSearchDescription', diff --git a/src/renderer/src/components/settings/agents/launch/AgentLaunchDefaultsGroup.tsx b/src/renderer/src/components/settings/agents/launch/AgentLaunchDefaultsGroup.tsx index f87d0a357aa..a6c67b3a2f8 100644 --- a/src/renderer/src/components/settings/agents/launch/AgentLaunchDefaultsGroup.tsx +++ b/src/renderer/src/components/settings/agents/launch/AgentLaunchDefaultsGroup.tsx @@ -1,5 +1,6 @@ import { useTranslate } from '@/i18n/use-translate' import { SettingsGroup } from '../../section-layout/SettingsGroup' +import { AGENT_PERMISSIONS_SECTION_ID } from '../agents-search' export function AgentLaunchDefaultsGroup({ description, @@ -14,7 +15,7 @@ export function AgentLaunchDefaultsGroup({ title={t('settings.agents.newAgentsTitle', 'New agents')} description={description} > -
+
{permissions}
diff --git a/src/renderer/src/components/settings/appearance/AppearanceInterfaceSection.tsx b/src/renderer/src/components/settings/appearance/AppearanceInterfaceSection.tsx index b58d5a5e154..eed1e8fd0c7 100644 --- a/src/renderer/src/components/settings/appearance/AppearanceInterfaceSection.tsx +++ b/src/renderer/src/components/settings/appearance/AppearanceInterfaceSection.tsx @@ -17,6 +17,7 @@ import { FontAutocomplete } from './fonts/FontAutocomplete' import { SettingsRow, SettingsSwitchRow } from '../form-controls/SettingsFormControls' import { DEFAULT_APP_FONT_FAMILY } from '../../../../../shared/constants' import { + APPEARANCE_EDITOR_THEME_SETTING_ID, getCodeEditorThemeEntries, getLanguageEntries, getThemeEntries, @@ -200,6 +201,8 @@ export function AppearanceInterfaceSection({ {editorThemeSlots.map((slot) => ( [ { + targetSectionId: APPEARANCE_EDITOR_THEME_SETTING_ID, title: translate('settings.appearance.editorTheme.title', 'Editor theme'), description: translate( 'settings.appearance.editorTheme.description', diff --git a/src/renderer/src/components/settings/browser/use/BrowserUsePane.tsx b/src/renderer/src/components/settings/browser/use/BrowserUsePane.tsx index c9afe1943f1..320547ff864 100644 --- a/src/renderer/src/components/settings/browser/use/BrowserUsePane.tsx +++ b/src/renderer/src/components/settings/browser/use/BrowserUsePane.tsx @@ -12,7 +12,10 @@ import '../../../../../../shared/constants' import { BROWSER_FAMILY_LABELS } from '../../../../../../shared/browser/browser-family-labels' import { SearchableSetting } from '../../section-layout/SearchableSetting' import { matchesSettingsSearch } from '../../search/settings-search' -import { getBrowserUsePaneSearchEntries } from './browser-use-search' +import { + BROWSER_USE_SETTINGS_TARGET_ID, + getBrowserUsePaneSearchEntries +} from './browser-use-search' import { BrowserUseExamples } from './BrowserUseExamples' import { BrowserUseComputerUseNotice } from './BrowserUseComputerUseNotice' import { BrowserUseEnableSwitch } from './BrowserUseEnableSwitch' @@ -88,7 +91,10 @@ export function BrowserUseSetup({ if (!browserUseEnabled) { return ( -
+

{t('auto.components.settings.BrowserUsePane.507fdae102', 'Agent browser use')} @@ -109,7 +115,7 @@ export function BrowserUseSetup({ } return ( -

+

diff --git a/src/renderer/src/components/settings/browser/use/browser-use-search.ts b/src/renderer/src/components/settings/browser/use/browser-use-search.ts index 1fbecec44a0..875cae9b2e3 100644 --- a/src/renderer/src/components/settings/browser/use/browser-use-search.ts +++ b/src/renderer/src/components/settings/browser/use/browser-use-search.ts @@ -2,8 +2,12 @@ import { translate } from '@/i18n/i18n' import { translateSearchKeyword } from '../../search/settings-search-keywords' import { createLocalizedCatalog } from '@/i18n/localized-catalog' +// The steps render only after Agent browser use is on, so both results land on its block. +export const BROWSER_USE_SETTINGS_TARGET_ID = 'browser-use' + export const getBrowserUsePaneSearchEntries = createLocalizedCatalog(() => [ { + targetSectionId: BROWSER_USE_SETTINGS_TARGET_ID, title: translate( 'auto.components.settings.browser.use.search.e72d500ca5', 'Install browser use skill' @@ -38,6 +42,7 @@ export const getBrowserUsePaneSearchEntries = createLocalizedCatalog(() => [ ] }, { + targetSectionId: BROWSER_USE_SETTINGS_TARGET_ID, title: translate( 'auto.components.settings.browser.use.search.9e2bfa1405', 'Import browser cookies' diff --git a/src/renderer/src/components/settings/cli/install/CliSection.tsx b/src/renderer/src/components/settings/cli/install/CliSection.tsx index c332c64cfe4..0df47b98d3a 100644 --- a/src/renderer/src/components/settings/cli/install/CliSection.tsx +++ b/src/renderer/src/components/settings/cli/install/CliSection.tsx @@ -22,6 +22,7 @@ import { } from '../../../../lib/skills/runtime/skill-runtime-command' import { WslCliRegistration } from './WslCliRegistration' import { SettingsGroup } from '../../section-layout/SettingsGroup' +import { GENERAL_SETTING_ANCHORS } from '../../general/general-setting-anchors' import { useCliRegistrationActions } from './use-cli-registration-actions' import { useLocalCliSkillFreshnessName } from '../../../../hooks/skills/freshness/use-local-cli-skill-freshness-name' import { translate } from '@/i18n/i18n' @@ -303,23 +304,25 @@ export function CliSection({

{!isBrowserManaged ? ( - +
+ +
) : null} diff --git a/src/renderer/src/components/settings/computer-use/ComputerUsePane.tsx b/src/renderer/src/components/settings/computer-use/ComputerUsePane.tsx index f71244a5ffe..af9871efc93 100644 --- a/src/renderer/src/components/settings/computer-use/ComputerUsePane.tsx +++ b/src/renderer/src/components/settings/computer-use/ComputerUsePane.tsx @@ -11,6 +11,7 @@ import { Button } from '../../ui/button' import { Badge } from '../../ui/badge' import { ComputerUseSkillSetupPanel } from './ComputerUseSkillSetupPanel' import { SettingsGroup } from '../section-layout/SettingsGroup' +import { COMPUTER_USE_SETTINGS_TARGET_ID } from './computer-use-search' import { translate } from '@/i18n/i18n' import { useTranslate, type Translate } from '@/i18n/use-translate' @@ -292,7 +293,7 @@ export function ComputerUsePane(): React.JSX.Element { return (
{isMac ? ( - +
@@ -384,7 +385,7 @@ export function ComputerUsePane(): React.JSX.Element { ) : null} - + {/* The inline panel adds pt-1.5 of its own; this evens the top inset with the bottom. */}
diff --git a/src/renderer/src/components/settings/computer-use/computer-use-search.ts b/src/renderer/src/components/settings/computer-use/computer-use-search.ts index 20b2d24b443..cd223e2a9ce 100644 --- a/src/renderer/src/components/settings/computer-use/computer-use-search.ts +++ b/src/renderer/src/components/settings/computer-use/computer-use-search.ts @@ -2,8 +2,12 @@ import { translate } from '@/i18n/i18n' import { translateSearchKeyword } from '../search/settings-search-keywords' import { createLocalizedCatalog } from '@/i18n/localized-catalog' +// The pane overview lands on its first card: macOS permissions, else the skill setup. +export const COMPUTER_USE_SETTINGS_TARGET_ID = 'computer-use-overview' + export const getComputerUsePaneSearchEntries = createLocalizedCatalog(() => [ { + targetSectionId: COMPUTER_USE_SETTINGS_TARGET_ID, title: translate('auto.components.settings.computer.use.search.c3c3d70b5e', 'Computer use'), description: translate( 'auto.components.settings.computer.use.search.9210db582b', diff --git a/src/renderer/src/components/settings/developer-permissions/DeveloperPermissionsPane.tsx b/src/renderer/src/components/settings/developer-permissions/DeveloperPermissionsPane.tsx index 8c12f0677bb..b4a7eedaec6 100644 --- a/src/renderer/src/components/settings/developer-permissions/DeveloperPermissionsPane.tsx +++ b/src/renderer/src/components/settings/developer-permissions/DeveloperPermissionsPane.tsx @@ -30,10 +30,10 @@ import { showDeveloperPermissionRequestNotice } from './developer-permission-req import { TerminalTccAttributionNotice } from './TerminalTccAttributionNotice' import { SETTINGS_GROUP_ROW_BLEED_CLASS, SettingsGroup } from '../section-layout/SettingsGroup' import { cn } from '@/lib/class-names' - -type DeveloperPermissionsPaneProps = { - highlightedSettingId?: string | null -} +import { + DEVELOPER_PERMISSIONS_OVERVIEW_ID, + getDeveloperPermissionSettingId +} from './developer-permissions-search' type PermissionDefinition = { id: DeveloperPermissionId @@ -223,9 +223,7 @@ const PERMISSIONS: PermissionDefinition[] = [ } ] -export function DeveloperPermissionsPane({ - highlightedSettingId = null -}: DeveloperPermissionsPaneProps): React.JSX.Element { +export function DeveloperPermissionsPane(): React.JSX.Element { const t = useTranslate() const [states, setStates] = useState([]) const [loading, setLoading] = useState(true) @@ -318,6 +316,7 @@ export function DeveloperPermissionsPane({
@@ -342,16 +341,15 @@ export function DeveloperPermissionsPane({ const state = stateById.get(permission.id) const status = state?.status const pending = pendingId === permission.id - const settingId = `developer-permissions-${permission.id}` + const settingId = getDeveloperPermissionSettingId(permission.id) return (
diff --git a/src/renderer/src/components/settings/developer-permissions/developer-permissions-search.ts b/src/renderer/src/components/settings/developer-permissions/developer-permissions-search.ts index 25ad8d28707..56786177a4e 100644 --- a/src/renderer/src/components/settings/developer-permissions/developer-permissions-search.ts +++ b/src/renderer/src/components/settings/developer-permissions/developer-permissions-search.ts @@ -1,9 +1,17 @@ import { translate } from '@/i18n/i18n' import { translateSearchKeyword } from '../search/settings-search-keywords' import { createLocalizedCatalog } from '@/i18n/localized-catalog' +import type { DeveloperPermissionId } from '../../../../../shared/developer-permissions-types' + +export const DEVELOPER_PERMISSIONS_OVERVIEW_ID = 'developer-permissions-overview' + +export function getDeveloperPermissionSettingId(id: DeveloperPermissionId): string { + return `developer-permissions-${id}` +} export const getDeveloperPermissionsPaneSearchEntries = createLocalizedCatalog(() => [ { + targetSectionId: DEVELOPER_PERMISSIONS_OVERVIEW_ID, title: translate( 'auto.components.settings.developer.permissions.search.ad085fa291', 'Developer permissions' @@ -36,6 +44,7 @@ export const getDeveloperPermissionsPaneSearchEntries = createLocalizedCatalog(( ] }, { + targetSectionId: getDeveloperPermissionSettingId('microphone'), title: translate( 'auto.components.settings.developer.permissions.search.16490ec4cd', 'Microphone and camera' @@ -80,6 +89,7 @@ export const getDeveloperPermissionsPaneSearchEntries = createLocalizedCatalog(( ] }, { + targetSectionId: getDeveloperPermissionSettingId('screen'), title: translate( 'auto.components.settings.developer.permissions.search.70ffdc218d', 'Screen recording and accessibility' @@ -144,6 +154,7 @@ export const getDeveloperPermissionsPaneSearchEntries = createLocalizedCatalog(( ] }, { + targetSectionId: getDeveloperPermissionSettingId('local-network'), title: translate( 'auto.components.settings.developer.permissions.search.3363889768', 'LAN, USB, and Bluetooth' diff --git a/src/renderer/src/components/settings/form-controls/SettingsFormControls.tsx b/src/renderer/src/components/settings/form-controls/SettingsFormControls.tsx index b88f93d1bb8..995a0a52a82 100644 --- a/src/renderer/src/components/settings/form-controls/SettingsFormControls.tsx +++ b/src/renderer/src/components/settings/form-controls/SettingsFormControls.tsx @@ -17,6 +17,7 @@ type SettingsRowProps = { className?: string labelId?: string alignTop?: boolean + id?: string } export function SettingsRow({ @@ -25,10 +26,12 @@ export function SettingsRow({ control, className, labelId, - alignTop + alignTop, + id }: SettingsRowProps): React.JSX.Element { return (
- state.settingsSearchInputQuery)} - onChange={(event) => setQuery(event.target.value)} - /> [0] & { initialQuery?: string } = {}) { - useAppStore.setState({ - settingsSearchQuery: initialQuery, - settingsSearchInputQuery: initialQuery - }) +function renderNavigationHarness(props: Parameters[0] = {}) { return render() } -it('opens an old subsection link from another child with a conflicting query, including repeated selection', async () => { +it('opens an old subsection link from another child, including repeated selection', async () => { const scroll = vi.spyOn(HTMLElement.prototype, 'scrollIntoView') - const { container } = renderNavigationHarness({ initialQuery: 'font' }) + const { container } = renderNavigationHarness() fireEvent.click(screen.getByRole('button', { name: 'Open app settings link' })) const add = await screen.findByRole('button', { name: 'Add app' }) const target = container.querySelector('#general-open-in-apps') @@ -131,21 +119,3 @@ it('opens an old subsection link from another child with a conflicting query, in fireEvent.click(screen.getByRole('button', { name: 'Open app settings link' })) await waitFor(() => expect(scroll.mock.contexts).toContain(target)) }) - -it('keeps an incomplete Open in draft when search temporarily replaces its form', async () => { - const onWrite = vi.fn() - renderNavigationHarness({ initialPage: 'open-in', initialQuery: '', onWrite }) - fireEvent.keyDown(screen.getByRole('button', { name: 'Add app' }), { key: 'ArrowDown' }) - fireEvent.click(await screen.findByRole('menuitem', { name: /Custom/ })) - fireEvent.change(screen.getByPlaceholderText('App name'), { target: { value: 'Draft editor' } }) - fireEvent.blur(screen.getByPlaceholderText('App name')) - fireEvent.change(screen.getByRole('textbox', { name: 'Search settings' }), { - target: { value: 'open in' } - }) - await screen.findByRole('button', { name: /Open in apps/i }) - fireEvent.change(screen.getByRole('textbox', { name: 'Search settings' }), { - target: { value: '' } - }) - expect(await screen.findByDisplayValue('Draft editor')).toBeTruthy() - expect(onWrite).not.toHaveBeenCalled() -}) diff --git a/src/renderer/src/components/settings/general/general-search.ts b/src/renderer/src/components/settings/general/general-search.ts index 6824faaaf5e..d2162087bb9 100644 --- a/src/renderer/src/components/settings/general/general-search.ts +++ b/src/renderer/src/components/settings/general/general-search.ts @@ -167,6 +167,7 @@ export const getGeneralCliSearchEntries = createLocalizedCatalog(() => [ targetSectionId: 'cli' }, { + targetSectionId: GENERAL_SETTING_ANCHORS.cliSkill, title: translate('settings.general.cli.skillTitle', 'Orca skill for agents'), description: translate( 'auto.components.settings.general.search.244e3fb4c8', diff --git a/src/renderer/src/components/settings/general/general-setting-anchors.ts b/src/renderer/src/components/settings/general/general-setting-anchors.ts index aa8acd95649..6f8fb32e979 100644 --- a/src/renderer/src/components/settings/general/general-setting-anchors.ts +++ b/src/renderer/src/components/settings/general/general-setting-anchors.ts @@ -6,6 +6,7 @@ export const GENERAL_SETTING_ANCHORS = { nesting: 'general-workspace-nesting', worktrees: 'general-global-worktree-visibility', openIn: 'general-open-in-apps', + cliSkill: 'general-cli-skill', autoSave: 'general-auto-save', autoSaveDelay: 'general-editor-auto-save-delay', formatOnSave: 'general-editor-format-on-save', @@ -28,6 +29,14 @@ export const GENERAL_SETTING_ANCHORS = { reviewNotes: 'general-editor-review-notes' } as const +// A search result for a General sub-page opens that page at this row. +export const GENERAL_SUB_PAGE_ENTRY_ANCHORS: readonly (readonly [GeneralSubPage, string])[] = [ + ['folder', GENERAL_SETTING_ANCHORS.folder], + ['worktrees', GENERAL_SETTING_ANCHORS.worktrees], + ['open-in', GENERAL_SETTING_ANCHORS.openIn], + ['editor', GENERAL_SETTING_ANCHORS.autoSaveDelay] +] + export const GENERAL_SETTING_OWNERS: Readonly> = { [GENERAL_SETTING_ANCHORS.folder]: 'folder', [GENERAL_SETTING_ANCHORS.nesting]: 'folder', diff --git a/src/renderer/src/components/settings/integrations/IntegrationsPane.tsx b/src/renderer/src/components/settings/integrations/IntegrationsPane.tsx index 821ebd8249b..f107475bc32 100644 --- a/src/renderer/src/components/settings/integrations/IntegrationsPane.tsx +++ b/src/renderer/src/components/settings/integrations/IntegrationsPane.tsx @@ -12,12 +12,20 @@ import { LinearIntegrationCard } from '../../integrations/task-trackers/linear-i import { IntegrationCardPresentationProvider } from '../../integrations/presentation/integration-card-presentation' import { useIntegrationProviderStatusRefresh } from '../../../hooks/use-integration-provider-status-refresh' import { SettingsGroup } from '../section-layout/SettingsGroup' +import { INTEGRATION_SETTING_TARGET_IDS } from './integrations-search' import { useTranslate } from '@/i18n/use-translate' // Flat presentation turns each provider into one row: -mx-4 cancels its inset, and inner rules are dropped so only rows divide. -function ProviderRow({ children }: { children: React.ReactNode }): React.JSX.Element { +function ProviderRow({ + id, + children +}: { + /** Search anchor; task-provider cards carry their own. */ + id?: string + children: React.ReactNode +}): React.JSX.Element { return ( -
+
{children}
) @@ -37,19 +45,19 @@ export function IntegrationsPane(): React.JSX.Element { 'Connect the source hosts Orca can use for pull requests, merge requests, checks, and review status.' )} > - + - + - + - + - + diff --git a/src/renderer/src/components/settings/integrations/integrations-search.ts b/src/renderer/src/components/settings/integrations/integrations-search.ts index 0ce31126680..b828997fd68 100644 --- a/src/renderer/src/components/settings/integrations/integrations-search.ts +++ b/src/renderer/src/components/settings/integrations/integrations-search.ts @@ -1,9 +1,25 @@ import { translate } from '@/i18n/i18n' import { translateSearchKeyword } from '../search/settings-search-keywords' import { createLocalizedCatalog } from '@/i18n/localized-catalog' +import { + JIRA_INTEGRATION_SECTION_ID, + LINEAR_INTEGRATION_SECTION_ID +} from '../../integrations/task-trackers/task-provider-integration-section-ids' + +// Provider rows carry these ids so search results can scroll to and highlight them. +export const INTEGRATION_SETTING_TARGET_IDS = { + github: 'integrations-github', + gitlab: 'integrations-gitlab', + bitbucket: 'integrations-bitbucket', + azuredevops: 'integrations-azure-devops', + gitea: 'integrations-gitea', + jira: JIRA_INTEGRATION_SECTION_ID, + linear: LINEAR_INTEGRATION_SECTION_ID +} as const export const getIntegrationsPaneSearchEntries = createLocalizedCatalog(() => [ { + targetSectionId: INTEGRATION_SETTING_TARGET_IDS.github, title: translate( 'auto.components.settings.integrations.search.a17a94d0b0', 'GitHub integration' @@ -25,6 +41,7 @@ export const getIntegrationsPaneSearchEntries = createLocalizedCatalog(() => [ ] }, { + targetSectionId: INTEGRATION_SETTING_TARGET_IDS.gitlab, title: translate( 'auto.components.settings.integrations.search.cd74b2f587', 'GitLab integration' @@ -51,6 +68,7 @@ export const getIntegrationsPaneSearchEntries = createLocalizedCatalog(() => [ ] }, { + targetSectionId: INTEGRATION_SETTING_TARGET_IDS.bitbucket, title: translate( 'auto.components.settings.integrations.search.9e238582e8', 'Bitbucket integration' @@ -95,6 +113,7 @@ export const getIntegrationsPaneSearchEntries = createLocalizedCatalog(() => [ ] }, { + targetSectionId: INTEGRATION_SETTING_TARGET_IDS.azuredevops, title: translate( 'auto.components.settings.integrations.search.b3efe7ab71', 'Azure DevOps integration' @@ -128,6 +147,7 @@ export const getIntegrationsPaneSearchEntries = createLocalizedCatalog(() => [ ] }, { + targetSectionId: INTEGRATION_SETTING_TARGET_IDS.gitea, title: translate( 'auto.components.settings.integrations.search.86a08aa60c', 'Gitea integration' @@ -157,6 +177,7 @@ export const getIntegrationsPaneSearchEntries = createLocalizedCatalog(() => [ ] }, { + targetSectionId: INTEGRATION_SETTING_TARGET_IDS.jira, title: translate('auto.components.settings.integrations.search.d8906d60a9', 'Jira integration'), description: translate( 'auto.components.settings.integrations.search.76f6af7c57', @@ -191,6 +212,7 @@ export const getIntegrationsPaneSearchEntries = createLocalizedCatalog(() => [ ] }, { + targetSectionId: INTEGRATION_SETTING_TARGET_IDS.linear, title: translate( 'auto.components.settings.integrations.search.d905b3ecee', 'Linear integration' diff --git a/src/renderer/src/components/settings/mobile/MobilePane.tsx b/src/renderer/src/components/settings/mobile/MobilePane.tsx index 466f7ea4dde..df14e8be444 100644 --- a/src/renderer/src/components/settings/mobile/MobilePane.tsx +++ b/src/renderer/src/components/settings/mobile/MobilePane.tsx @@ -27,7 +27,7 @@ import type { MobileRelayMintFailure } from '../../../../../shared/remote-runtim import { useMobilePairingConnectionMode } from '../../mobile/use-mobile-pairing-connection-mode' import { useMobilePairingAddressPreference } from '../../mobile/use-mobile-pairing-address-preference' import { usePairingOfferExpiry } from '../../mobile/use-pairing-offer-expiry' -import { shouldOpenMobilePairingAddress } from './mobile-pane-search' +import { MOBILE_SETTING_ANCHORS, shouldOpenMobilePairingAddress } from './mobile-pane-search' export function MobilePane(): React.JSX.Element { const t = useTranslate() @@ -349,7 +349,7 @@ export function MobilePane(): React.JSX.Element {
-
+
-
+
updateSettings({ mobileEmulatorEnabled: !enabled })} /> -
+