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. */}
+
@@ -350,10 +358,12 @@ export function AdvancedNetworkSettingsSection({
-
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)}
- />
requestTarget('general-open-in-apps')}>Open app settings link
[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 })}
/>
-
+
@@ -233,6 +236,7 @@ export function MobileEmulatorSettingsPane({
{enabled ? (
[
{
title: translate(
@@ -68,6 +74,7 @@ export const getMobileEmulatorSearchEntries = createLocalizedCatalog(() => [
]
},
{
+ targetSectionId: MOBILE_EMULATOR_SETTING_ANCHORS.opening,
title: translate(
'auto.components.settings.mobile.emulator.search.b8c9f903eb',
'Default emulator device'
@@ -100,6 +107,7 @@ export const getMobileEmulatorSearchEntries = createLocalizedCatalog(() => [
]
},
{
+ targetSectionId: MOBILE_EMULATOR_SETTING_ANCHORS.availability,
title: translate(
'auto.components.settings.mobile.emulator.search.de556a0911',
'Emulator availability'
@@ -132,6 +140,7 @@ export const getMobileEmulatorSearchEntries = createLocalizedCatalog(() => [
]
},
{
+ targetSectionId: MOBILE_EMULATOR_SETTING_ANCHORS.agentControl,
title: translate(
'auto.components.settings.mobile.emulator.search.0d6e7dba38',
'Agent CLI control'
diff --git a/src/renderer/src/components/settings/mobile/mobile-pane-search.ts b/src/renderer/src/components/settings/mobile/mobile-pane-search.ts
index 93e1399190d..a31f0504a2d 100644
--- a/src/renderer/src/components/settings/mobile/mobile-pane-search.ts
+++ b/src/renderer/src/components/settings/mobile/mobile-pane-search.ts
@@ -4,8 +4,15 @@ import { createLocalizedCatalog } from '@/i18n/localized-catalog'
import { matchesSettingsSearch, normalizeSettingsSearchQuery } from '../search/settings-search'
import { getMachineNameSearchEntries } from '../machine-identity/machine-name-search'
+export const MOBILE_SETTING_ANCHORS = {
+ networkInterface: 'mobile-network-interface',
+ pairing: 'mobile-pairing',
+ devices: 'mobile-paired-devices'
+} as const
+
const getNetworkInterfaceSearchEntries = createLocalizedCatalog(() => [
{
+ targetSectionId: MOBILE_SETTING_ANCHORS.networkInterface,
title: translate('auto.components.settings.mobile.pane.search.a068d70f7b', 'Network interface'),
description: translate(
'auto.components.settings.mobile.pane.search.3190ef67a4',
@@ -54,6 +61,7 @@ export function shouldOpenMobilePairingAddress(searchQuery: string): boolean {
export const getMobilePaneSearchEntries = createLocalizedCatalog(() => [
{
+ targetSectionId: MOBILE_SETTING_ANCHORS.pairing,
title: translate('auto.components.settings.mobile.pane.search.1dcb1d1a11', 'Mobile pairing'),
description: translate(
'auto.components.settings.mobile.pane.search.7fb728fb2b',
@@ -69,6 +77,7 @@ export const getMobilePaneSearchEntries = createLocalizedCatalog(() => [
]
},
{
+ targetSectionId: MOBILE_SETTING_ANCHORS.devices,
title: translate('auto.components.settings.mobile.pane.search.cb018bcc07', 'Connected devices'),
description: translate(
'auto.components.settings.mobile.pane.search.13419718b3',
diff --git a/src/renderer/src/components/settings/mobile/pairing/MobilePairingSetupSection.tsx b/src/renderer/src/components/settings/mobile/pairing/MobilePairingSetupSection.tsx
index 4aad3d604cc..a08702a6730 100644
--- a/src/renderer/src/components/settings/mobile/pairing/MobilePairingSetupSection.tsx
+++ b/src/renderer/src/components/settings/mobile/pairing/MobilePairingSetupSection.tsx
@@ -2,6 +2,8 @@ import { useState, type ReactNode } from 'react'
import { ChevronDown, Loader2, QrCode, RefreshCw } from 'lucide-react'
import { Button } from '../../../ui/button'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../../ui/collapsible'
+import { useSettingsTargetReveal } from '../../section-layout/settings-target-highlight'
+import { MOBILE_SETTING_ANCHORS } from '../mobile-pane-search'
import { Tooltip, TooltipContent, TooltipTrigger } from '../../../ui/tooltip'
import { cn } from '@/lib/class-names'
import { useTranslate } from '@/i18n/use-translate'
@@ -53,6 +55,9 @@ export function MobilePairingSetupSection({
const t = useTranslate()
const usingRelay = connectionMode === 'automatic'
const [addressDisclosureOpen, setAddressDisclosureOpen] = useState(false)
+ const addressRevealRef = useSettingsTargetReveal(() =>
+ setAddressDisclosureOpen(true)
+ )
// Render search/custom-address pickers outright; a collapse trigger cannot close pinned-open state.
const addressDisclosurePinned = addressDisclosureForcedOpen || selectedAddressIsCustom
// Relay can use main’s default LAN endpoint; LAN-only requires an explicit reachable address.
@@ -64,7 +69,7 @@ export function MobilePairingSetupSection({
)
const addressControls = (
-
+
) : usingRelay ? (
// Keep Relay’s optional direct path available in a disclosure without dominating setup.
-
+
-
+ {/* Mounted while closed so a settings jump can find and reveal the address picker. */}
+
{addressControls}
diff --git a/src/renderer/src/components/settings/navigation/SettingsSidebar.tsx b/src/renderer/src/components/settings/navigation/SettingsSidebar.tsx
index bd4cc3f2ee2..889b0451adf 100644
--- a/src/renderer/src/components/settings/navigation/SettingsSidebar.tsx
+++ b/src/renderer/src/components/settings/navigation/SettingsSidebar.tsx
@@ -1,6 +1,6 @@
-import { type RefObject, useMemo } from 'react'
-
-import { ArrowLeft, Search, Server } from 'lucide-react'
+import type { KeyboardEvent, RefObject } from 'react'
+import { useDeferredValue, useId, useMemo, useState } from 'react'
+import { ArrowLeft, Server } from 'lucide-react'
import type { RepoIcon } from '../../../../../shared/repo-icon'
import type {
SettingsNavIcon,
@@ -8,13 +8,10 @@ import type {
} from '@/components/settings/navigation/settings-navigation-types'
import type { GitHubRepositoryIdentity } from '../../../../../shared/github/repositories/repository-identity'
import type { GlobalSettings } from '../../../../../shared/global-settings-types'
-import { useShortcutKeyComboDetails } from '@/hooks/useShortcutLabel'
-import { ShortcutKeyCombo } from '../../ShortcutKeyCombo'
import { cn } from '@/lib/class-names'
import { RepoIconGlyph } from '../../repo/repo-icon'
import { RepoForkIndicator } from '../../repo/repo-fork-indicator'
import { Button } from '../../ui/button'
-import { Input } from '../../ui/input'
import { SetupGuideProgressRing } from '../../setup-guide/SetupGuideProgressRing'
import {
useSettingsSetupGuideProgress,
@@ -24,6 +21,12 @@ import {
import { useTranslate } from '@/i18n/use-translate'
import { resolveLeftSidebarStyleVariables } from '@/app-shell/appearance/left-sidebar-appearance'
import { useAppStore } from '@/store/index'
+import {
+ rankSettingsSearchResults,
+ SETTINGS_SEARCH_RESULT_LIMIT,
+ type SettingsSearchResult
+} from './settings-search-results'
+import { SettingsSearchField, SettingsSearchResultsList } from './SettingsSidebarSearch'
type NavSection = {
id: string
@@ -51,50 +54,12 @@ type SettingsSidebarProps = {
settings: GlobalSettings | null
generalGroups: NavGroup[]
repoSections: RepoNavSection[]
- hasRepos: boolean
+ searchCandidates: readonly SettingsSearchResult[]
searchInputRef?: RefObject
searchAutoFocus?: boolean
onBack: () => void
onSelectSection: (sectionId: string) => void
-}
-
-function SettingsSearchField({
- searchInputRef,
- searchAutoFocus = false
-}: Pick): React.JSX.Element {
- const t = useTranslate()
- const searchQuery = useAppStore((state) => state.settingsSearchInputQuery)
- const onSearchChange = useAppStore((state) => state.setSettingsSearchQuery)
- const searchShortcutCombos = useShortcutKeyComboDetails('settings.search')
-
- return (
-
-
-
- onSearchChange(event.target.value)}
- placeholder={t('auto.components.settings.SettingsSidebar.dbceaa8840', 'Search settings')}
- className="bg-background/60 pl-9 pr-14 text-[13px]"
- />
- {searchQuery === '' ? (
-
- {searchShortcutCombos.map((combo) => (
-
- ))}
-
- ) : null}
-
-
- )
+ onOpenSearchResult: (result: SettingsSearchResult) => void
}
type VisibleInstallStatus = Extract<
@@ -161,13 +126,79 @@ export function SettingsSidebar({
settings,
generalGroups,
repoSections,
- hasRepos,
+ searchCandidates,
searchInputRef,
searchAutoFocus = false,
onBack,
- onSelectSection
+ onSelectSection,
+ onOpenSearchResult
}: SettingsSidebarProps): React.JSX.Element {
const t = useTranslate()
+ const searchQuery = useAppStore((state) => state.settingsSearchInputQuery)
+ const setSearchQuery = useAppStore((state) => state.setSettingsSearchQuery)
+ const listboxId = useId()
+ // Ranking follows the deferred query so a large catalog never blocks typing.
+ const deferredSearchQuery = useDeferredValue(searchQuery)
+ const rankedResults = useMemo(
+ () => rankSettingsSearchResults(deferredSearchQuery, searchCandidates),
+ [searchCandidates, deferredSearchQuery]
+ )
+ const searchResults = rankedResults.slice(0, SETTINGS_SEARCH_RESULT_LIMIT)
+ const hiddenResultCount = rankedResults.length - searchResults.length
+ const isSearching = searchQuery.trim() !== ''
+ // Nothing is selected until the user moves or picks; typing only changes the rows.
+ const [selectedResultKey, setSelectedResultKey] = useState(null)
+ const selectedResultIndex = searchResults.findIndex((result) => result.key === selectedResultKey)
+ const selectedResult = selectedResultIndex === -1 ? undefined : searchResults[selectedResultIndex]
+ const repoSectionById = useMemo(
+ () => new Map(repoSections.map((section) => [section.id, section] as const)),
+ [repoSections]
+ )
+ const groupTitleById = useMemo(
+ () => new Map(generalGroups.map((group) => [group.id, group.title] as const)),
+ [generalGroups]
+ )
+ const getOptionId = (index: number): string => `${listboxId}-option-${index}`
+
+ const pickResult = (result: SettingsSearchResult): void => {
+ setSelectedResultKey(result.key)
+ onOpenSearchResult(result)
+ }
+ const changeSearchQuery = (query: string): void => {
+ setSearchQuery(query)
+ setSelectedResultKey(null)
+ }
+ const handleSearchKeyDown = (event: KeyboardEvent): void => {
+ if (event.nativeEvent.isComposing) {
+ return
+ }
+ if (event.key === 'Escape' && searchQuery !== '') {
+ // Keep the page-level Escape from closing Settings while clearing search.
+ event.preventDefault()
+ changeSearchQuery('')
+ return
+ }
+ if (!isSearching || searchResults.length === 0) {
+ return
+ }
+ const count = searchResults.length
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
+ event.preventDefault()
+ const nextIndex =
+ selectedResultIndex === -1
+ ? event.key === 'ArrowDown'
+ ? 0
+ : count - 1
+ : (selectedResultIndex + (event.key === 'ArrowDown' ? 1 : -1) + count) % count
+ setSelectedResultKey(searchResults[nextIndex].key)
+ document.getElementById(getOptionId(nextIndex))?.scrollIntoView({ block: 'nearest' })
+ return
+ }
+ if (event.key === 'Enter' && selectedResult) {
+ event.preventDefault()
+ pickResult(selectedResult)
+ }
+ }
const setupGuideProgress = useSettingsSetupGuideProgress(true)
const leftSidebarStyle = useMemo(() => resolveLeftSidebarStyleVariables(settings), [settings])
const setupActive = activeSectionId === 'setup-guide'
@@ -200,7 +231,8 @@ export function SettingsSidebar({
className="flex w-[280px] shrink-0 flex-col border-r border-worktree-sidebar-border bg-worktree-sidebar"
style={leftSidebarStyle}
>
-
+ {/* Same 48px height as SettingsHeader so both bottom borders meet in one line. */}
+
-
+
0}
+ activeOptionId={selectedResult ? getOptionId(selectedResultIndex) : undefined}
+ onQueryChange={changeSearchQuery}
+ onKeyDown={handleSearchKeyDown}
+ />
- {showSetupGuideTopRow ? (
+ {isSearching ? (
+
+ ) : null}
+
+ {!isSearching && showSetupGuideTopRow ? (
) : null}
-
-
- {generalGroups.map((group) => (
-
+ {/* Not rendered while searching so typing never re-renders every project row. */}
+ {isSearching ? null : (
+
+
+ {generalGroups.map((group) => (
+
+
+ {group.title}
+
+
+ {group.sections
+ .filter((section) => section.id !== 'setup-guide')
+ .map((section) => {
+ const Icon = section.icon
+ const isActive = activeSectionId === section.id
+
+ return (
+ onSelectSection(section.id)}
+ className={navItemClassName(isActive)}
+ >
+
+ {section.title}
+ {isVisibleInstallStatus(section.installStatus) ? (
+
+ {installStatusLabel(section.installStatus)}
+
+ ) : section.badge ? (
+
+ {section.badge}
+
+ ) : null}
+
+ )
+ })}
+
+
+ ))}
+
+
- {group.title}
+ {t('auto.components.settings.SettingsSidebar.5c9669ff9c', 'Projects')}
-
- {group.sections
- .filter((section) => section.id !== 'setup-guide')
- .map((section) => {
- const Icon = section.icon
+
+ {repoSections.length > 0 ? (
+
+ {repoSections.map((section) => {
const isActive = activeSectionId === section.id
return (
@@ -246,76 +341,36 @@ export function SettingsSidebar({
onClick={() => onSelectSection(section.id)}
className={navItemClassName(isActive)}
>
-
+
{section.title}
- {isVisibleInstallStatus(section.installStatus) ? (
-
- {installStatusLabel(section.installStatus)}
+
+ {section.isRemote && (
+
+
+ {t('auto.components.settings.SettingsSidebar.e0900f83e7', 'SSH')}
- ) : section.badge ? (
-
- {section.badge}
-
- ) : null}
+ )}
)
})}
-
+
+ ) : (
+
+ {t(
+ 'auto.components.settings.SettingsSidebar.df38d612b7',
+ 'No projects added yet.'
+ )}
+
+ )}
- ))}
-
-
-
- {t('auto.components.settings.SettingsSidebar.5c9669ff9c', 'Projects')}
-
-
- {repoSections.length > 0 ? (
-
- {repoSections.map((section) => {
- const isActive = activeSectionId === section.id
-
- return (
- onSelectSection(section.id)}
- className={navItemClassName(isActive)}
- >
-
- {section.title}
-
- {section.isRemote && (
-
-
- {t('auto.components.settings.SettingsSidebar.e0900f83e7', 'SSH')}
-
- )}
-
- )
- })}
-
- ) : (
-
- {hasRepos
- ? t(
- 'auto.components.settings.SettingsSidebar.3e483e256b',
- 'No matching project settings.'
- )
- : t(
- 'auto.components.settings.SettingsSidebar.df38d612b7',
- 'No projects added yet.'
- )}
-
- )}
-
+ )}
)
}
diff --git a/src/renderer/src/components/settings/navigation/SettingsSidebarSearch.tsx b/src/renderer/src/components/settings/navigation/SettingsSidebarSearch.tsx
new file mode 100644
index 00000000000..c29d63543a6
--- /dev/null
+++ b/src/renderer/src/components/settings/navigation/SettingsSidebarSearch.tsx
@@ -0,0 +1,206 @@
+import type { KeyboardEvent, RefObject } from 'react'
+import { Search, X } from 'lucide-react'
+import type { RepoIcon } from '../../../../../shared/repo-icon'
+import { useShortcutKeyComboDetails } from '@/hooks/useShortcutLabel'
+import { ShortcutKeyCombo } from '../../ShortcutKeyCombo'
+import { cn } from '@/lib/class-names'
+import { RepoIconGlyph } from '../../repo/repo-icon'
+import { Button } from '../../ui/button'
+import { Input } from '../../ui/input'
+import { useTranslate } from '@/i18n/use-translate'
+import { HighlightedText } from '../../worktree-jump/worktree-jump-palette-primitives'
+import {
+ getSettingsSearchTitleMatchRanges,
+ type SettingsSearchResult
+} from './settings-search-results'
+
+type SettingsSearchFieldProps = {
+ searchInputRef?: RefObject
+ searchAutoFocus?: boolean
+ query: string
+ listboxId: string
+ expanded: boolean
+ activeOptionId: string | undefined
+ onQueryChange: (query: string) => void
+ onKeyDown: (event: KeyboardEvent) => void
+}
+
+export function SettingsSearchField({
+ searchInputRef,
+ searchAutoFocus = false,
+ query,
+ listboxId,
+ expanded,
+ activeOptionId,
+ onQueryChange,
+ onKeyDown
+}: SettingsSearchFieldProps): React.JSX.Element {
+ const t = useTranslate()
+ const searchShortcutCombos = useShortcutKeyComboDetails('settings.search')
+
+ return (
+
+
+
+ onQueryChange(event.target.value)}
+ onKeyDown={onKeyDown}
+ placeholder={t('auto.components.settings.SettingsSidebar.dbceaa8840', 'Search settings')}
+ aria-label={t('auto.components.settings.SettingsSidebar.dbceaa8840', 'Search settings')}
+ role="combobox"
+ aria-autocomplete="list"
+ aria-expanded={expanded}
+ aria-controls={expanded ? listboxId : undefined}
+ aria-activedescendant={activeOptionId}
+ className="bg-background/60 pl-9 pr-14 text-[13px]"
+ />
+ {query === '' ? (
+
+ {searchShortcutCombos.map((combo) => (
+
+ ))}
+
+ ) : (
+ {
+ onQueryChange('')
+ searchInputRef?.current?.focus()
+ }}
+ aria-label={t('settings.search.clear', 'Clear search')}
+ className="absolute right-1.5 top-1/2 -translate-y-1/2"
+ >
+
+
+ )}
+
+
+ )
+}
+
+type SettingsSearchResultsListProps = {
+ results: readonly SettingsSearchResult[]
+ selectedIndex: number
+ query: string
+ listboxId: string
+ getOptionId: (index: number) => string
+ repoSectionById: ReadonlyMap
+ groupTitleById: ReadonlyMap
+ hiddenResultCount: number
+ pending: boolean
+ onSelect: (result: SettingsSearchResult) => void
+}
+
+export function SettingsSearchResultsList({
+ results: searchResults,
+ selectedIndex: selectedResultIndex,
+ query: searchQuery,
+ listboxId,
+ getOptionId,
+ repoSectionById,
+ groupTitleById,
+ hiddenResultCount,
+ pending,
+ onSelect
+}: SettingsSearchResultsListProps): React.JSX.Element {
+ const t = useTranslate()
+ return (
+
+ {searchResults.length > 0 ? (
+
+ {searchResults.map((result, index) => {
+ const isSelected = index === selectedResultIndex
+ const repoSection = repoSectionById.get(result.section.id)
+ const Icon = result.section.icon
+ return (
+ event.preventDefault()}
+ onClick={() => onSelect(result)}
+ className={cn(
+ 'flex min-h-10 w-full items-start gap-2 rounded-lg px-3 py-1 text-left outline-none transition-colors duration-150',
+ isSelected
+ ? 'bg-worktree-sidebar-accent ring-1 ring-worktree-sidebar-ring/25'
+ : 'hover:bg-worktree-sidebar-accent/60'
+ )}
+ >
+ {repoSection ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {result.isPane
+ ? repoSection
+ ? t('auto.components.settings.SettingsSidebar.5c9669ff9c', 'Projects')
+ : (groupTitleById.get(result.section.group) ?? result.section.title)
+ : result.section.title}
+
+
+
+ )
+ })}
+
+ ) : null}
+ {searchResults.length > 0 && hiddenResultCount > 0 ? (
+
+ {t('settings.search.moreResults', '{{value0}} more · keep typing to narrow', {
+ value0: hiddenResultCount
+ })}
+
+ ) : null}
+ {searchResults.length === 0 && !pending ? (
+
+ {t('settings.search.noResults', 'No settings match “{{value0}}”', {
+ value0: searchQuery.trim()
+ })}
+
+ ) : null}
+
+ )
+}
diff --git a/src/renderer/src/components/settings/navigation/settings-deep-link-target-watcher.ts b/src/renderer/src/components/settings/navigation/settings-deep-link-target-watcher.ts
index 17299d46d6d..4622aa05593 100644
--- a/src/renderer/src/components/settings/navigation/settings-deep-link-target-watcher.ts
+++ b/src/renderer/src/components/settings/navigation/settings-deep-link-target-watcher.ts
@@ -11,6 +11,8 @@ export function watchForSettingsDeepLinkTarget(args: {
root: HTMLElement | null
isTargetPresent: () => boolean
onTargetPresent: () => void
+ /** The target never mounted; the caller drops its pending jump so it cannot fire later. */
+ onTimeout?: () => void
}): SettingsDeepLinkTargetWatch {
const root = args.root ?? (typeof document === 'undefined' ? null : document.body)
if (!root || typeof MutationObserver === 'undefined') {
@@ -42,7 +44,11 @@ export function watchForSettingsDeepLinkTarget(args: {
args.onTargetPresent()
})
observer.observe(root, { subtree: true, childList: true })
- timeout = window.setTimeout(finish, TARGET_WAIT_TIMEOUT_MS)
+ timeout = window.setTimeout(() => {
+ timeout = null
+ finish()
+ args.onTimeout?.()
+ }, TARGET_WAIT_TIMEOUT_MS)
return { cancel: finish }
}
diff --git a/src/renderer/src/components/settings/navigation/settings-navigation-foundations.ts b/src/renderer/src/components/settings/navigation/settings-navigation-foundations.ts
index b32b4855f4b..fc2eb2c73a9 100644
--- a/src/renderer/src/components/settings/navigation/settings-navigation-foundations.ts
+++ b/src/renderer/src/components/settings/navigation/settings-navigation-foundations.ts
@@ -9,6 +9,7 @@ import type {
SettingsNavTarget
} from '@/components/settings/navigation/settings-navigation-types'
import type { SettingsDeepLinkTargetWatch } from './settings-deep-link-target-watcher'
+import { parseSettingsTitleTargetId } from '../section-layout/settings-target-highlight'
export const SETTINGS_NAV_GROUPS = [
{
@@ -116,20 +117,65 @@ export function getSettingsScrollTarget(
sectionId: string,
container?: HTMLElement | null
): HTMLElement | null {
+ const title = parseSettingsTitleTargetId(sectionId)
+ if (title !== null) {
+ return (
+ (container ?? document).querySelector(
+ `[data-settings-search-title="${CSS.escape(title)}"]`
+ ) ?? (container ? findSettingsRowByVisibleTitle(title, container) : null)
+ )
+ }
return (
container?.querySelector(`[data-settings-section="${CSS.escape(sectionId)}"]`) ??
document.getElementById(sectionId)
)
}
+// Rows not wrapped in SearchableSetting are found by the label they render.
+function findSettingsRowByVisibleTitle(title: string, root: HTMLElement): HTMLElement | null {
+ const wanted = title.trim().toLowerCase()
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
+ const element = node.parentElement
+ // The pane's own h2 heading is not a setting row.
+ if (node.textContent?.trim().toLowerCase() !== wanted || !element || element.closest('h2')) {
+ continue
+ }
+ // A group title stands for its whole card, not the heading text.
+ const groupCard = element
+ .closest('[data-settings-group-heading]')
+ ?.closest('[data-settings-group]')
+ ?.querySelector('[data-settings-group-card]')
+ // Free-form card content has no row; the card stands in rather than the bare label.
+ return (
+ groupCard ??
+ element.closest('[data-settings-row]') ??
+ element.closest('[data-settings-group-card]') ??
+ element
+ )
+ }
+ return null
+}
+
+function hasNestedScroller(target: HTMLElement, container: HTMLElement): boolean {
+ for (let node = target.parentElement; node && node !== container; node = node.parentElement) {
+ const overflowY = getComputedStyle(node).overflowY
+ if ((overflowY === 'auto' || overflowY === 'scroll') && node.scrollHeight > node.clientHeight) {
+ return true
+ }
+ }
+ return false
+}
+
export function scrollSubsectionIntoView(targetId: string, container?: HTMLElement | null): void {
// Whole-pane swaps need inner scrolling only when the pane exceeds the viewport.
const target = getSettingsScrollTarget(targetId, container)
if (!target) {
return
}
- if (!container) {
- target.scrollIntoView({ block: 'start' })
+ // A target inside its own scroll area (the Shortcuts list) needs every scroller moved, not just the page.
+ if (!container || hasNestedScroller(target, container)) {
+ target.scrollIntoView({ block: container ? 'center' : 'start' })
return
}
const containerRect = container.getBoundingClientRect()
diff --git a/src/renderer/src/components/settings/navigation/settings-navigation-types.ts b/src/renderer/src/components/settings/navigation/settings-navigation-types.ts
index 00fde539cc3..c90336ad724 100644
--- a/src/renderer/src/components/settings/navigation/settings-navigation-types.ts
+++ b/src/renderer/src/components/settings/navigation/settings-navigation-types.ts
@@ -74,6 +74,7 @@ export const BROWSER_USER_AGENT_SETTINGS_TARGET_ID = 'browser-user-agent'
export const CODEX_TERMINAL_SERVER_ISOLATION_SETTINGS_TARGET_ID =
'agents-codex-terminal-server-isolation'
export const GLOBAL_WORKTREE_VISIBILITY_SETTINGS_TARGET_ID = 'general-global-worktree-visibility'
+export const NODE_DIST_MIRROR_SETTINGS_TARGET_ID = 'advanced-node-dist-mirror'
export type SettingsNavigationTarget = {
pane: SettingsNavTarget
diff --git a/src/renderer/src/components/settings/navigation/settings-search-results.test.ts b/src/renderer/src/components/settings/navigation/settings-search-results.test.ts
new file mode 100644
index 00000000000..9ae65679bad
--- /dev/null
+++ b/src/renderer/src/components/settings/navigation/settings-search-results.test.ts
@@ -0,0 +1,53 @@
+import { Settings } from 'lucide-react'
+import { expect, it } from 'vitest'
+import type { SettingsNavSection } from './settings-navigation-types'
+import {
+ buildSettingsSearchResults,
+ getSettingsSearchTitleMatchRanges,
+ rankSettingsSearchResults
+} from './settings-search-results'
+
+function section(
+ id: string,
+ title: string,
+ searchEntries: SettingsNavSection['searchEntries']
+): SettingsNavSection {
+ return { id, title, description: '', icon: Settings, group: 'interface', searchEntries }
+}
+
+it('ranks individual settings across panes and targets each one', () => {
+ const results = buildSettingsSearchResults(
+ [
+ section('general', 'General', [
+ { title: 'Auto update', keywords: ['release'] },
+ { title: 'Auto update' },
+ { title: 'Theme', keywords: ['update'] }
+ ]),
+ section('updates', 'Updates', [{ title: 'Channel', targetSectionId: 'updates-channel' }]),
+ section('repo-1', 'orca', [{ title: 'Update hooks' }])
+ ],
+ (_section, entry) => (entry.title === 'Theme' ? 'general-theme' : undefined)
+ )
+
+ expect(
+ rankSettingsSearchResults('update', results).map((result) => [
+ result.section.id,
+ result.entry.title,
+ result.targetId
+ ])
+ ).toEqual([
+ ['updates', 'Updates', 'updates'],
+ ['repo-1', 'Update hooks', 'settings-title:Update hooks'],
+ ['general', 'Auto update', 'settings-title:Auto update'],
+ ['general', 'Theme', 'general-theme']
+ ])
+ expect(rankSettingsSearchResults('channel', results)[0]?.targetId).toBe('updates-channel')
+ expect(rankSettingsSearchResults(' ', results)).toEqual([])
+})
+
+it('marks the same text the ranking fold matched, including dotted capital I', () => {
+ expect(getSettingsSearchTitleMatchRanges('İstanbul', 'stan')).toEqual([{ start: 1, end: 5 }])
+ expect(getSettingsSearchTitleMatchRanges('İstanbul', 'İst')).toEqual([{ start: 0, end: 3 }])
+ expect(getSettingsSearchTitleMatchRanges('Line height', 'İ')).toEqual([{ start: 1, end: 2 }])
+ expect(getSettingsSearchTitleMatchRanges('Theme', 'update')).toEqual([])
+})
diff --git a/src/renderer/src/components/settings/navigation/settings-search-results.ts b/src/renderer/src/components/settings/navigation/settings-search-results.ts
new file mode 100644
index 00000000000..c0a0840467e
--- /dev/null
+++ b/src/renderer/src/components/settings/navigation/settings-search-results.ts
@@ -0,0 +1,93 @@
+import type { MatchRange } from '../../../../../shared/palette-match/normalized-text'
+import type { SettingsNavSection } from './settings-navigation-types'
+import {
+ foldSettingsSearchText,
+ getSettingsSectionSearchEntries,
+ normalizeSettingsSearchQuery,
+ rankSettingsSearchItems,
+ type SettingsSearchEntry
+} from '../search/settings-search'
+import { getSettingsTitleTargetId } from '../section-layout/settings-target-highlight'
+
+// Enough to scan; past this the query is too broad to be useful.
+export const SETTINGS_SEARCH_RESULT_LIMIT = 50
+
+export type SettingsSearchResult = {
+ key: string
+ section: SettingsNavSection
+ entry: SettingsSearchEntry
+ isPane: boolean
+ /** Scroll target inside the pane; the pane id itself means "show from the top". */
+ targetId: string
+}
+
+export function buildSettingsSearchResults(
+ sections: readonly SettingsNavSection[],
+ resolveAnchorId: (section: SettingsNavSection, entry: SettingsSearchEntry) => string | undefined
+): SettingsSearchResult[] {
+ const results: SettingsSearchResult[] = []
+ for (const section of sections) {
+ const seenTitles = new Set()
+ getSettingsSectionSearchEntries(section).forEach((entry, index) => {
+ if (seenTitles.has(entry.title)) {
+ return
+ }
+ seenTitles.add(entry.title)
+ const isPane = index === 0
+ results.push({
+ key: `${section.id}\u0000${entry.title}`,
+ section,
+ entry,
+ isPane,
+ targetId: isPane
+ ? section.id
+ : (entry.targetSectionId ??
+ resolveAnchorId(section, entry) ??
+ getSettingsTitleTargetId(entry.title))
+ })
+ })
+ }
+ return results
+}
+
+export function rankSettingsSearchResults(
+ query: string,
+ results: readonly SettingsSearchResult[]
+): SettingsSearchResult[] {
+ if (normalizeSettingsSearchQuery(query) === '') {
+ return []
+ }
+ return rankSettingsSearchItems(
+ query,
+ results,
+ (result) => result.entry,
+ (result) => ({ firstEntryIsPane: result.isPane })
+ ).map(({ item }) => item)
+}
+
+/** Range of a title's first match under the ranking fold, mapped back to the original title. */
+export function getSettingsSearchTitleMatchRanges(title: string, query: string): MatchRange[] {
+ const needle = normalizeSettingsSearchQuery(query)
+ if (needle === '') {
+ return []
+ }
+ // Fold per code point so each folded unit maps back to the original text it came from.
+ let folded = ''
+ const originalStarts: number[] = []
+ const originalEnds: number[] = []
+ let offset = 0
+ for (const char of title) {
+ const chunk = foldSettingsSearchText(char)
+ for (let i = 0; i < chunk.length; i += 1) {
+ originalStarts.push(offset)
+ originalEnds.push(offset + char.length)
+ }
+ folded += chunk
+ offset += char.length
+ }
+ const start = folded.indexOf(needle)
+ if (start === -1) {
+ return []
+ }
+ return [{ start: originalStarts[start], end: originalEnds[start + needle.length - 1] }]
+}
diff --git a/src/renderer/src/components/settings/navigation/use-settings-navigation-model.ts b/src/renderer/src/components/settings/navigation/use-settings-navigation-model.ts
index 4c9e240c260..f196bd3eba1 100644
--- a/src/renderer/src/components/settings/navigation/use-settings-navigation-model.ts
+++ b/src/renderer/src/components/settings/navigation/use-settings-navigation-model.ts
@@ -18,6 +18,12 @@ import { DESKTOP_TERMINAL_SCROLLBACK_ROW_PRESETS as SCROLLBACK_PRESETS_ROWS } fr
import type { SettingsStoreModel } from '../page/use-settings-store-model'
import type { SettingsInteractionController } from '../page/use-settings-interaction-controller'
import { hasReadyVoiceModel } from './settings-navigation-foundations'
+import { buildSettingsSearchResults } from './settings-search-results'
+import { getGeneralWorkspaceSearchDescriptors } from '../general/general-search'
+import { getGeneralEditorSearchDescriptors } from '../editor/general-editor-search'
+import { GENERAL_SUB_PAGE_ENTRY_ANCHORS } from '../general/general-setting-anchors'
+import { getGeneralSubPageTitle } from '../general/general-sub-pages'
+import { translate } from '@/i18n/i18n'
export function useSettingsNavigationModel(
model: SettingsStoreModel,
@@ -129,6 +135,20 @@ export function useSettingsNavigationModel(
() => new Map(navSections.map((section) => [section.id, section] as const)),
[navSections]
)
+ const searchCandidates = useMemo(() => {
+ // General sub-page rows carry anchors in descriptors, not in their search entries.
+ const generalAnchorByTitle = new Map([
+ ...[...getGeneralWorkspaceSearchDescriptors(), ...getGeneralEditorSearchDescriptors()].map(
+ (descriptor) => [descriptor.entry.title, descriptor.sectionId] as const
+ ),
+ ...GENERAL_SUB_PAGE_ENTRY_ANCHORS.map(
+ ([page, anchor]) => [getGeneralSubPageTitle(page, translate), anchor] as const
+ )
+ ])
+ return buildSettingsSearchResults(navSections, (section, entry) =>
+ section.id === 'general' ? generalAnchorByTitle.get(entry.title) : undefined
+ )
+ }, [navSections])
const getSectionSearchEntries = (sectionId: string) => {
const section = navSectionById.get(sectionId)
return section ? getSettingsSectionSearchEntries(section) : []
@@ -204,6 +224,7 @@ export function useSettingsNavigationModel(
displayedGitUsername,
navSections,
getSectionSearchEntries,
+ searchCandidates,
visibleNavSections,
visibleSectionIds,
projectByRepoId,
diff --git a/src/renderer/src/components/settings/navigation/use-settings-repo-scroll-effects.ts b/src/renderer/src/components/settings/navigation/use-settings-repo-scroll-effects.ts
index 5843d2b33b0..1e63953efa0 100644
--- a/src/renderer/src/components/settings/navigation/use-settings-repo-scroll-effects.ts
+++ b/src/renderer/src/components/settings/navigation/use-settings-repo-scroll-effects.ts
@@ -9,6 +9,7 @@ import type { SettingsInteractionController } from '../page/use-settings-interac
import type { SettingsNavigationModel } from './use-settings-navigation-model'
import type { SettingsTerminalModel } from '../page/use-settings-terminal-model'
import { watchForSettingsDeepLinkTarget } from './settings-deep-link-target-watcher'
+import { requestSettingsTargetReveal } from '../section-layout/settings-target-highlight'
import {
cancelPendingSettingsDeepLinkTargetWatch,
cancelPendingSettingsSubsectionScrollFrame,
@@ -195,13 +196,19 @@ export function useSettingsRepoScrollEffects(
root: container,
isTargetPresent: () =>
getSettingsScrollTarget(scrollTargetId, contentScrollRef.current) !== null,
- onTargetPresent: () => setPendingNavRequestTick((tick) => tick + 1)
+ onTargetPresent: () => setPendingNavRequestTick((tick) => tick + 1),
+ onTimeout: () => {
+ pendingNavSectionRef.current = null
+ pendingScrollTargetRef.current = null
+ }
})
return
}
- if (pendingNavSectionId === 'general') {
- setHighlightedSettingsTarget({ id: scrollTargetId })
+ const revealTarget = getSettingsScrollTarget(scrollTargetId, container)
+ if (revealTarget) {
+ requestSettingsTargetReveal(revealTarget)
}
+ setHighlightedSettingsTarget({ id: scrollTargetId })
const scrollToSubsection = (): void => {
scrollSubsectionIntoView(scrollTargetId, contentScrollRef.current)
}
diff --git a/src/renderer/src/components/settings/page/settings-page-renderer.tsx b/src/renderer/src/components/settings/page/settings-page-renderer.tsx
index e4b5bb9b9ff..376049e9d6a 100644
--- a/src/renderer/src/components/settings/page/settings-page-renderer.tsx
+++ b/src/renderer/src/components/settings/page/settings-page-renderer.tsx
@@ -1,4 +1,3 @@
-import { SettingsTargetHighlightProvider } from '../section-layout/settings-target-highlight'
import { SettingsHeader } from '../navigation/SettingsHeader'
import { getGeneralSubPageTitle } from '../general/general-sub-pages'
import { ActiveSettingsSectionProvider } from '../section-layout/SettingsSection'
@@ -96,12 +95,13 @@ export function renderSettingsPage(
activeSectionId={model.activeSectionId}
generalGroups={view.generalNavGroups}
repoSections={view.repoNavSections}
- hasRepos={model.repos.length > 0}
+ searchCandidates={navigation.searchCandidates}
searchInputRef={interactions.searchInputRef}
// Deep-linked panes/modals own focus; plain entry focuses search.
searchAutoFocus={model.settingsNavigationTarget == null}
onBack={interactions.closeSettingsPageWithPromptGuard}
onSelectSection={actions.scrollToSection}
+ onOpenSearchResult={(result) => void actions.openSettingsSearchResult(result)}
/>
@@ -161,46 +161,42 @@ export function renderSettingsPage(
) : (
-
- {renderAgentsSettingsSection(context, t)}
- {renderAccountsSettingsSection(context, t)}
- {renderOrchestrationSettingsSection(context, t)}
- {renderLinearSettingsSection(context, t)}
- {renderDesktopCapabilitySettingsSections(context, t)}
- {renderOrcaAccountSettingsSection(context, t)}
- {renderSetupGuideSettingsSection(context, t)}
- {renderGeneralSettingsSection(context, t)}
- {renderIntegrationsSettingsSection(context, t)}
- {renderMobileSettingsSection(context, t)}
- {renderAutomationsSettingsSection(context, t)}
- {renderArtifactsSettingsSection(context, t)}
- {renderShareSkillsSettingsSection(context, t)}
- {renderSessionHistorySettingsSection(context, t)}
- {renderGitSettingsSection(context, t)}
- {renderTasksSettingsSection(context, t)}
- {renderTerminalSettingsSection(context, t)}
- {renderQuickCommandsSettingsSection(context, t)}
- {renderBrowserSettingsSection(context, t)}
- {renderMobileEmulatorSettingsSection(context, t)}
- {renderFloatingWorkspaceSettingsSection(context, t)}
- {renderAppearanceSettingsSection(context, t)}
- {renderChatSettingsSection(context)}
- {renderInputSettingsSection(context, t)}
- {renderNotificationsSettingsSection(context, t)}
- {renderShortcutsSettingsSection(context, t)}
- {renderStatsSettingsSection(context, t)}
- {renderServersSettingsSection(context, t)}
- {renderSshSettingsSection(context, t)}
- {renderDeveloperPermissionsSettingsSection(context, t)}
- {renderPrivacySettingsSection(context, t)}
- {renderAdvancedSettingsSection(context, t)}
- {renderDevSettingsSection(context, t)}
- {renderExperimentalSettingsSection(context, t)}
- {renderPluginsSettingsSection(context)}
- {renderProjectSettingsSections(context, t)}
-
+ {renderAgentsSettingsSection(context, t)}
+ {renderAccountsSettingsSection(context, t)}
+ {renderOrchestrationSettingsSection(context, t)}
+ {renderLinearSettingsSection(context, t)}
+ {renderDesktopCapabilitySettingsSections(context, t)}
+ {renderOrcaAccountSettingsSection(context, t)}
+ {renderSetupGuideSettingsSection(context, t)}
+ {renderGeneralSettingsSection(context, t)}
+ {renderIntegrationsSettingsSection(context, t)}
+ {renderMobileSettingsSection(context, t)}
+ {renderAutomationsSettingsSection(context, t)}
+ {renderArtifactsSettingsSection(context, t)}
+ {renderShareSkillsSettingsSection(context, t)}
+ {renderSessionHistorySettingsSection(context, t)}
+ {renderGitSettingsSection(context, t)}
+ {renderTasksSettingsSection(context, t)}
+ {renderTerminalSettingsSection(context, t)}
+ {renderQuickCommandsSettingsSection(context, t)}
+ {renderBrowserSettingsSection(context, t)}
+ {renderMobileEmulatorSettingsSection(context, t)}
+ {renderFloatingWorkspaceSettingsSection(context, t)}
+ {renderAppearanceSettingsSection(context, t)}
+ {renderChatSettingsSection(context)}
+ {renderInputSettingsSection(context, t)}
+ {renderNotificationsSettingsSection(context, t)}
+ {renderShortcutsSettingsSection(context, t)}
+ {renderStatsSettingsSection(context, t)}
+ {renderServersSettingsSection(context, t)}
+ {renderSshSettingsSection(context, t)}
+ {renderDeveloperPermissionsSettingsSection(context, t)}
+ {renderPrivacySettingsSection(context, t)}
+ {renderAdvancedSettingsSection(context, t)}
+ {renderDevSettingsSection(context, t)}
+ {renderExperimentalSettingsSection(context, t)}
+ {renderPluginsSettingsSection(context)}
+ {renderProjectSettingsSections(context, t)}
)}
diff --git a/src/renderer/src/components/settings/page/settings-remote-security-section-renderers.tsx b/src/renderer/src/components/settings/page/settings-remote-security-section-renderers.tsx
index 1263ebcd922..fa900439621 100644
--- a/src/renderer/src/components/settings/page/settings-remote-security-section-renderers.tsx
+++ b/src/renderer/src/components/settings/page/settings-remote-security-section-renderers.tsx
@@ -92,11 +92,7 @@ export function renderDeveloperPermissionsSettingsSection(
)}
searchEntries={navigation.getSectionSearchEntries('developer-permissions')}
>
- {view.isSectionMounted('developer-permissions') ? (
-
- ) : null}
+ {view.isSectionMounted('developer-permissions') ?
: null}
) : null
}
diff --git a/src/renderer/src/components/settings/page/settings-view-model.ts b/src/renderer/src/components/settings/page/settings-view-model.ts
index 23747a7ffb0..9aadac5c369 100644
--- a/src/renderer/src/components/settings/page/settings-view-model.ts
+++ b/src/renderer/src/components/settings/page/settings-view-model.ts
@@ -5,7 +5,11 @@ import type { Translate } from '@/i18n/use-translate'
import type { SettingsStoreModel } from './use-settings-store-model'
import type { SettingsInteractionController } from './use-settings-interaction-controller'
import type { SettingsNavigationModel } from '../navigation/use-settings-navigation-model'
-import { getSettingsNavGroupDefinitionsForSearch } from '../navigation/settings-navigation-foundations'
+import {
+ cancelPendingSettingsDeepLinkTargetWatch,
+ getSettingsNavGroupDefinitionsForSearch
+} from '../navigation/settings-navigation-foundations'
+import type { SettingsSearchResult } from '../navigation/settings-search-results'
export function useSettingsNavigationActions(
model: Pick<
@@ -36,7 +40,8 @@ export function useSettingsNavigationActions(
confirmDiscardSourceControlAiPromptChanges,
contentScrollRef,
pendingNavSectionRef,
- pendingScrollTargetRef
+ pendingScrollTargetRef,
+ pendingScrollTargetWatchRef
} = interactions
const scrollToSection = useCallback(
async (sectionId: string): Promise
=> {
@@ -51,6 +56,10 @@ export function useSettingsNavigationActions(
// Clear stale filtering when selecting a result’s pane.
setSettingsSearchQuery('')
}
+ // A plain pane pick supersedes any search or deep-link jump still waiting for its row.
+ pendingNavSectionRef.current = null
+ pendingScrollTargetRef.current = null
+ cancelPendingSettingsDeepLinkTargetWatch(pendingScrollTargetWatchRef)
setHighlightedSettingsTarget(null)
setGeneralSubPage(null)
releaseNativeAccounts()
@@ -61,6 +70,9 @@ export function useSettingsNavigationActions(
releaseNativeAccounts,
confirmDiscardSourceControlAiPromptChanges,
contentScrollRef,
+ pendingNavSectionRef,
+ pendingScrollTargetRef,
+ pendingScrollTargetWatchRef,
setActiveSectionId,
setGeneralSubPage,
setHighlightedSettingsTarget,
@@ -69,6 +81,32 @@ export function useSettingsNavigationActions(
]
)
+ const openSettingsSearchResult = useCallback(
+ async (result: SettingsSearchResult): Promise => {
+ const sectionId = result.section.id
+ if (sectionId !== activeSectionId) {
+ if (!(await confirmDiscardSourceControlAiPromptChanges())) {
+ return
+ }
+ releaseNativeAccounts()
+ }
+ setHighlightedSettingsTarget(null)
+ pendingNavSectionRef.current = sectionId
+ pendingScrollTargetRef.current = result.targetId
+ // Pending refs cannot schedule the jump effect; trigger a render.
+ setPendingNavRequestTick((tick) => tick + 1)
+ },
+ [
+ activeSectionId,
+ confirmDiscardSourceControlAiPromptChanges,
+ pendingNavSectionRef,
+ pendingScrollTargetRef,
+ releaseNativeAccounts,
+ setHighlightedSettingsTarget,
+ setPendingNavRequestTick
+ ]
+ )
+
const openComputerUseFromBrowser = useCallback(async () => {
if (!(await confirmDiscardSourceControlAiPromptChanges())) {
return
@@ -90,7 +128,7 @@ export function useSettingsNavigationActions(
settingsSearchQuery
])
- return { scrollToSection, openComputerUseFromBrowser }
+ return { scrollToSection, openSettingsSearchResult, openComputerUseFromBrowser }
}
export type SettingsNavigationActions = ReturnType
diff --git a/src/renderer/src/components/settings/page/use-settings-interaction-controller.ts b/src/renderer/src/components/settings/page/use-settings-interaction-controller.ts
index 972fe5e6d4e..1b7fbf6fd18 100644
--- a/src/renderer/src/components/settings/page/use-settings-interaction-controller.ts
+++ b/src/renderer/src/components/settings/page/use-settings-interaction-controller.ts
@@ -9,10 +9,18 @@ import {
} from '../appearance/fonts/installed-font-list'
import type { SettingsStoreModel } from './use-settings-store-model'
import type { SettingsDeepLinkTargetWatch } from '../navigation/settings-deep-link-target-watcher'
+import {
+ applySettingsTargetHighlightBleed,
+ clearSettingsTargetHighlightBleed,
+ resolveSettingsHighlightElement,
+ SETTINGS_TARGET_HIGHLIGHT_CLASS_NAME
+} from '../section-layout/settings-target-highlight'
import {
cancelPendingSettingsDeepLinkTargetWatch,
cancelPendingSettingsSubsectionScrollFrame,
+ getSettingsScrollTarget,
readSourceControlAiSettings,
+ scrollSubsectionIntoView,
SETTINGS_TARGET_HIGHLIGHT_MS
} from '../navigation/settings-navigation-foundations'
@@ -35,6 +43,14 @@ export type SettingsInteractionModel = Pick<
| 'updateSettingsOrThrow'
>
+const SETTINGS_REVEAL_SETTLE_MS = 250
+
+function isFullyVisibleIn(target: HTMLElement, container: HTMLElement): boolean {
+ const targetRect = target.getBoundingClientRect()
+ const containerRect = container.getBoundingClientRect()
+ return targetRect.top >= containerRect.top && targetRect.bottom <= containerRect.bottom
+}
+
export function useSettingsInteractionController(model: SettingsInteractionModel) {
const {
closeSettingsPage,
@@ -144,6 +160,34 @@ export function useSettingsInteractionController(model: SettingsInteractionModel
return () => window.clearTimeout(timeout)
}, [highlightedSettingsTarget, setHighlightedSettingsTarget])
+ useEffect(() => {
+ const targetId = highlightedSettingsTarget?.id
+ if (!targetId) {
+ return
+ }
+ const target = getSettingsScrollTarget(targetId, contentScrollRef.current)
+ if (!target) {
+ return
+ }
+ // The one owner of the highlight, on the element the jump scrolled to (or its card).
+ const highlighted = resolveSettingsHighlightElement(target)
+ highlighted.classList.add(SETTINGS_TARGET_HIGHLIGHT_CLASS_NAME)
+ applySettingsTargetHighlightBleed(highlighted)
+ // A section the jump just revealed grows over ~200ms; re-scroll once if the row is still cut off.
+ const settle = window.setTimeout(() => {
+ const container = contentScrollRef.current
+ if (container && !isFullyVisibleIn(target, container)) {
+ scrollSubsectionIntoView(targetId, container)
+ }
+ applySettingsTargetHighlightBleed(highlighted)
+ }, SETTINGS_REVEAL_SETTLE_MS)
+ return () => {
+ window.clearTimeout(settle)
+ clearSettingsTargetHighlightBleed(highlighted)
+ highlighted.classList.remove(SETTINGS_TARGET_HIGHLIGHT_CLASS_NAME)
+ }
+ }, [highlightedSettingsTarget])
+
const requestFontSuggestions = useCallback((): void => {
if (installedFontsLoadedRef.current || installedFontsLoadPromiseRef.current) {
return
diff --git a/src/renderer/src/components/settings/privacy/PrivacyDiagnosticsSection.tsx b/src/renderer/src/components/settings/privacy/PrivacyDiagnosticsSection.tsx
index fb3b9cc196d..9aaba9f0165 100644
--- a/src/renderer/src/components/settings/privacy/PrivacyDiagnosticsSection.tsx
+++ b/src/renderer/src/components/settings/privacy/PrivacyDiagnosticsSection.tsx
@@ -13,6 +13,7 @@ import {
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { diagnosticsErrorText } from './diagnostics-bundle-error-copy'
+import { PRIVACY_SETTING_ANCHORS } from './privacy-search'
export function PrivacyDiagnosticsSection(): React.JSX.Element {
const t = useTranslate()
@@ -352,7 +353,11 @@ function PrivacyDiagnosticsRow({
children: React.ReactNode
}): React.JSX.Element {
return (
-
+
{icon}
diff --git a/src/renderer/src/components/settings/privacy/PrivacyPane.tsx b/src/renderer/src/components/settings/privacy/PrivacyPane.tsx
index accd597c577..211f6b9219c 100644
--- a/src/renderer/src/components/settings/privacy/PrivacyPane.tsx
+++ b/src/renderer/src/components/settings/privacy/PrivacyPane.tsx
@@ -12,6 +12,7 @@ import {
} from '../../../diagnostics/telemetry/telemetry'
import { useAppStore } from '../../../store/index'
import { PrivacyDiagnosticsSection } from './PrivacyDiagnosticsSection'
+import { PRIVACY_SETTING_ANCHORS } from './privacy-search'
import { useTranslate } from '@/i18n/use-translate'
import { openAppHttpLink } from '@/lib/browser/navigation/app-http-link-open'
import { SettingsGroup } from '../section-layout/SettingsGroup'
@@ -92,8 +93,12 @@ export function PrivacyPane({ settings }: PrivacyPaneProps): React.JSX.Element {
}
return (
-
-
+
+
diff --git a/src/renderer/src/components/settings/privacy/privacy-search.ts b/src/renderer/src/components/settings/privacy/privacy-search.ts
index 34e07a91ca1..cc2d86847ae 100644
--- a/src/renderer/src/components/settings/privacy/privacy-search.ts
+++ b/src/renderer/src/components/settings/privacy/privacy-search.ts
@@ -2,8 +2,15 @@ import { translate } from '@/i18n/i18n'
import { translateSearchKeyword } from '../search/settings-search-keywords'
import { createLocalizedCatalog } from '@/i18n/localized-catalog'
+export const PRIVACY_SETTING_ANCHORS = {
+ overview: 'privacy-overview',
+ usageData: 'privacy-usage-data',
+ diagnostics: 'privacy-diagnostics'
+} as const
+
export const getPrivacyPaneSearchEntries = createLocalizedCatalog(() => [
{
+ targetSectionId: PRIVACY_SETTING_ANCHORS.overview,
title: translate('auto.components.settings.privacy.search.a966af0840', 'Privacy & telemetry'),
description: translate(
'auto.components.settings.privacy.search.aa3b794c17',
@@ -40,6 +47,7 @@ export const getPrivacyPaneSearchEntries = createLocalizedCatalog(() => [
]
},
{
+ targetSectionId: PRIVACY_SETTING_ANCHORS.diagnostics,
title: translate('auto.components.settings.privacy.search.6d258d2ed6', 'Diagnostics'),
description: translate(
'auto.components.settings.privacy.search.8b08f32366',
@@ -54,6 +62,8 @@ export const getPrivacyPaneSearchEntries = createLocalizedCatalog(() => [
]
},
{
+ // The variables disable the usage-data switch; their notice renders under it only while one is set.
+ targetSectionId: PRIVACY_SETTING_ANCHORS.usageData,
title: translate(
'auto.components.settings.privacy.search.e058a3c98d',
'Telemetry environment variables'
diff --git a/src/renderer/src/components/settings/quick-commands/QuickCommandsPane.tsx b/src/renderer/src/components/settings/quick-commands/QuickCommandsPane.tsx
index d7697fef8ca..ca0161265ef 100644
--- a/src/renderer/src/components/settings/quick-commands/QuickCommandsPane.tsx
+++ b/src/renderer/src/components/settings/quick-commands/QuickCommandsPane.tsx
@@ -15,6 +15,7 @@ import { getSettingOwnershipSummary } from '../page/setting-ownership'
import { useTranslate } from '@/i18n/use-translate'
import { QuickCommandsList } from './QuickCommandsList'
import { QuickCommandsToolbar } from './QuickCommandsToolbar'
+import { QUICK_COMMANDS_SETTINGS_TARGET_ID } from './quick-commands-search'
import { GLOBAL_SCOPE_KEY } from './QuickCommandsScopeFilter'
import {
getRepoExecutionHostId,
@@ -276,7 +277,7 @@ export function QuickCommandsPane({
})
return (
-
+
{shouldShowTerminalQuickCommandHostOwnership(hostOptions) ? (
{ownership.description}
) : null}
diff --git a/src/renderer/src/components/settings/quick-commands/quick-commands-search.ts b/src/renderer/src/components/settings/quick-commands/quick-commands-search.ts
index fa93883f37f..0a9d0605286 100644
--- a/src/renderer/src/components/settings/quick-commands/quick-commands-search.ts
+++ b/src/renderer/src/components/settings/quick-commands/quick-commands-search.ts
@@ -2,8 +2,11 @@ import { translate } from '@/i18n/i18n'
import { translateSearchKeyword } from '../search/settings-search-keywords'
import { createLocalizedCatalog } from '@/i18n/localized-catalog'
+export const QUICK_COMMANDS_SETTINGS_TARGET_ID = 'quick-commands-list'
+
export const getQuickCommandsPaneSearchEntries = createLocalizedCatalog(() => [
{
+ targetSectionId: QUICK_COMMANDS_SETTINGS_TARGET_ID,
title: translate('auto.components.settings.quick.commands.search.915776ccfa', 'Quick commands'),
description: translate(
'auto.components.settings.quick.commands.search.d691c4e8d8',
diff --git a/src/renderer/src/components/settings/repository/repository-settings-targets.ts b/src/renderer/src/components/settings/repository/repository-settings-targets.ts
index f5dad33c9f3..fbe30364d8a 100644
--- a/src/renderer/src/components/settings/repository/repository-settings-targets.ts
+++ b/src/renderer/src/components/settings/repository/repository-settings-targets.ts
@@ -8,6 +8,9 @@ export function getRepositoryIconSectionId(repoId: string): string {
return `repo-${repoId}-icon`
}
+// Search entries describe the representative repo; the pane may show another host's repo, so this id is not repo-scoped.
+export const REPOSITORY_SOURCE_CONTROL_AI_CARD_ID = 'repo-source-control-ai-card'
+
export function getRepositorySourceControlAiSectionId(repoId: string): string {
return `repo-${repoId}-source-control-ai`
}
diff --git a/src/renderer/src/components/settings/repository/source-control/RepositorySourceControlAiSection.tsx b/src/renderer/src/components/settings/repository/source-control/RepositorySourceControlAiSection.tsx
index 89500158818..2f7799d6c4e 100644
--- a/src/renderer/src/components/settings/repository/source-control/RepositorySourceControlAiSection.tsx
+++ b/src/renderer/src/components/settings/repository/source-control/RepositorySourceControlAiSection.tsx
@@ -6,7 +6,10 @@ import { normalizeSourceControlAiSettings } from '../../../../../../shared/sourc
import type { SourceControlAiRepoUpdate } from '../../../../../../shared/source-control/ai/settings/source-control-ai-recipe-save'
import type { SourceControlAiSettingsPatch } from '../../../../../../shared/source-control/ai/settings/source-control-ai-settings-types'
import { useAppStore } from '../../../../store/index'
-import { getRepositorySourceControlAiSectionId } from '../repository-settings-targets'
+import {
+ getRepositorySourceControlAiSectionId,
+ REPOSITORY_SOURCE_CONTROL_AI_CARD_ID
+} from '../repository-settings-targets'
import { RepositorySourceControlAiActionRows } from './RepositorySourceControlAiActionRows'
import { RepositorySourceControlAiCustomCommand } from './RepositorySourceControlAiCustomCommand'
import { RepositorySourceControlAiEnablement } from './RepositorySourceControlAiEnablement'
@@ -83,6 +86,7 @@ export function RepositorySourceControlAiSection({
data-settings-section={getRepositorySourceControlAiSectionId(repo.id)}
>
{
// The first entry is the pane title; rank it above setting titles.
- const titleScore = index === 0 ? PANE_TITLE_SCORE : ENTRY_TITLE_SCORE
+ const titleScore = index === 0 && firstEntryIsPane ? PANE_TITLE_SCORE : ENTRY_TITLE_SCORE
return Math.max(
score,
scoreSettingsSearchText(normalizedQuery, entry.title, titleScore),
@@ -138,7 +144,8 @@ export function getSettingsSectionSearchEntries(section: {
export function rankSettingsSearchItems(
query: string,
items: readonly T[],
- getEntries: (item: T) => SettingsSearchEntry | SettingsSearchEntry[]
+ getEntries: (item: T) => SettingsSearchEntry | SettingsSearchEntry[],
+ getScoreOptions?: (item: T) => SettingsSearchScoreOptions
): RankedSettingsSearchItem[] {
if (isSettingsSearchQueryTooLarge(query)) {
return []
@@ -151,7 +158,7 @@ export function rankSettingsSearchItems(
.map((item, index): SettingsSearchRankCandidate => ({
item,
index,
- score: scoreSettingsSearch(query, getEntries(item))
+ score: scoreSettingsSearch(query, getEntries(item), getScoreOptions?.(item))
}))
.filter((candidate) => candidate.score > SETTINGS_SEARCH_NO_MATCH_SCORE)
.sort((a, b) => b.score - a.score || a.index - b.index)
diff --git a/src/renderer/src/components/settings/section-layout/SearchableSetting.tsx b/src/renderer/src/components/settings/section-layout/SearchableSetting.tsx
index 63fadf5ccb1..3a3c3a6f499 100644
--- a/src/renderer/src/components/settings/section-layout/SearchableSetting.tsx
+++ b/src/renderer/src/components/settings/section-layout/SearchableSetting.tsx
@@ -1,5 +1,3 @@
-import { useContext } from 'react'
-import { SettingsTargetHighlightContext } from './settings-target-highlight'
import { useAppStateValue } from '@/store/retained-views/app-state-view'
import type React from 'react'
import { cn } from '@/lib/class-names'
@@ -21,7 +19,6 @@ export function SearchableSetting({
className,
id
}: SearchableSettingProps): React.JSX.Element | null {
- const highlighted = useContext(SettingsTargetHighlightContext) === id
const query = useAppStateValue((state) => state.settingsSearchQuery)
if (!forceVisible && !matchesSettingsSearch(query, { title, description, keywords })) {
return null
@@ -30,11 +27,8 @@ export function SearchableSetting({
return (
{children}
diff --git a/src/renderer/src/components/settings/section-layout/SettingsGroup.tsx b/src/renderer/src/components/settings/section-layout/SettingsGroup.tsx
index 363b22d0dc1..824d54279dc 100644
--- a/src/renderer/src/components/settings/section-layout/SettingsGroup.tsx
+++ b/src/renderer/src/components/settings/section-layout/SettingsGroup.tsx
@@ -11,13 +11,14 @@ export const SETTINGS_GROUP_ROW_BLEED_CLASS = '-mx-5 px-5'
export const SETTINGS_GROUP_NESTED_INSET_CLASS = 'pl-5'
type SettingsGroupProps = {
- id?: string
title?: React.ReactNode
description?: React.ReactNode
action?: React.ReactNode
children?: React.ReactNode
className?: string
bodyClassName?: string
+ /** Anchors the card, the same element a search for the group title lands on. */
+ id?: string
onHeaderClick?: (event: React.MouseEvent
) => void
}
@@ -34,6 +35,8 @@ export function SettingsGroup({
}: SettingsGroupProps): React.JSX.Element {
const card = (
[data-settings-group-body]:empty]:hidden',
@@ -51,7 +54,7 @@ export function SettingsGroup({
)
return (
-
+
{title ? (
(
-
+
diff --git a/src/renderer/src/components/settings/section-layout/SettingsGroupDisclosure.tsx b/src/renderer/src/components/settings/section-layout/SettingsGroupDisclosure.tsx
index 00269e9ca5b..9a51102871f 100644
--- a/src/renderer/src/components/settings/section-layout/SettingsGroupDisclosure.tsx
+++ b/src/renderer/src/components/settings/section-layout/SettingsGroupDisclosure.tsx
@@ -2,8 +2,7 @@ import type React from 'react'
import { useState } from 'react'
import { ChevronRight } from 'lucide-react'
import { cn } from '@/lib/class-names'
-import { useAppStore } from '../../../store/index'
-import { normalizeSettingsSearchQuery } from '../search/settings-search'
+import { useSettingsTargetReveal } from '../section-layout/settings-target-highlight'
import { useTranslate } from '@/i18n/use-translate'
import { SETTINGS_GROUP_NESTED_INSET_CLASS } from './SettingsGroup'
@@ -20,20 +19,18 @@ export function SettingsGroupDisclosure({
children
}: SettingsGroupDisclosureProps): React.JSX.Element {
const t = useTranslate()
- const searchQuery = useAppStore((state) => state.settingsSearchQuery)
- const isSearching = normalizeSettingsSearchQuery(searchQuery).length > 0
const [open, setOpen] = useState(false)
- const forced = isSearching || forceOpen
- const expanded = open || forced
+ const expanded = open || forceOpen
+ const revealRef = useSettingsTargetReveal(() => setOpen(true))
return (
-
+
setOpen((prev) => !prev)}
- // Search matches and unsaved edits must remain reachable.
- disabled={forced}
+ // Unsaved edits must remain reachable.
+ disabled={forceOpen}
className="flex min-h-11 w-full items-center gap-2 text-sm font-semibold text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/50 disabled:cursor-default"
>
{label ?? t('auto.components.settings.AppearanceAdvancedDisclosure.advanced', 'Advanced')}
- {expanded ?
{children}
: null}
+ {/* Stays mounted while collapsed so a settings jump can find and reveal its rows. */}
+
+ {children}
+
)
}
diff --git a/src/renderer/src/components/settings/section-layout/settings-target-highlight.ts b/src/renderer/src/components/settings/section-layout/settings-target-highlight.ts
index 7d82c34d388..ecb73978fc9 100644
--- a/src/renderer/src/components/settings/section-layout/settings-target-highlight.ts
+++ b/src/renderer/src/components/settings/section-layout/settings-target-highlight.ts
@@ -1,4 +1,89 @@
-import { createContext } from 'react'
+import { useCallback, useLayoutEffect, useRef, type RefCallback } from 'react'
-export const SettingsTargetHighlightContext = createContext
(null)
-export const SettingsTargetHighlightProvider = SettingsTargetHighlightContext.Provider
+export const SETTINGS_TARGET_HIGHLIGHT_CLASS_NAME = 'settings-target-highlight'
+
+const SETTINGS_TITLE_TARGET_PREFIX = 'settings-title:'
+
+// Search entries without an anchor id target their rendered row by title.
+export function getSettingsTitleTargetId(title: string): string {
+ return `${SETTINGS_TITLE_TARGET_PREFIX}${title}`
+}
+
+export function parseSettingsTitleTargetId(targetId: string): string | null {
+ return targetId.startsWith(SETTINGS_TITLE_TARGET_PREFIX)
+ ? targetId.slice(SETTINGS_TITLE_TARGET_PREFIX.length)
+ : null
+}
+
+const SETTINGS_GROUP_CARD_SELECTOR = '[data-settings-group-card]'
+
+// Rows sit at different insets (nested rows, sub-fields), so measure the gap from the row to
+// its card's inner edges instead of assuming one gutter.
+function findEnclosingSettingsCard(element: HTMLElement): HTMLElement | null {
+ const groupCard = element.parentElement?.closest(SETTINGS_GROUP_CARD_SELECTOR)
+ if (groupCard) {
+ return groupCard
+ }
+ for (let node = element.parentElement; node; node = node.parentElement) {
+ const style = getComputedStyle(node)
+ if (
+ Number.parseFloat(style.borderLeftWidth) > 0 &&
+ Number.parseFloat(style.borderTopLeftRadius) >= 8
+ ) {
+ return node
+ }
+ }
+ return null
+}
+
+// An anchor on a whole section (title plus card) tints only its card, so the tint keeps the card's corners.
+export function resolveSettingsHighlightElement(target: HTMLElement): HTMLElement {
+ if (target.matches(SETTINGS_GROUP_CARD_SELECTOR) || findEnclosingSettingsCard(target)) {
+ return target
+ }
+ return target.querySelector(SETTINGS_GROUP_CARD_SELECTOR) ?? target
+}
+
+export function applySettingsTargetHighlightBleed(target: HTMLElement): void {
+ const card = findEnclosingSettingsCard(target)
+ if (!card) {
+ return
+ }
+ const cardRect = card.getBoundingClientRect()
+ const cardStyle = getComputedStyle(card)
+ const targetRect = target.getBoundingClientRect()
+ const left = targetRect.left - cardRect.left - Number.parseFloat(cardStyle.borderLeftWidth)
+ const right = cardRect.right - Number.parseFloat(cardStyle.borderRightWidth) - targetRect.right
+ target.style.setProperty('--settings-target-bleed-left', `${Math.max(0, left)}px`)
+ target.style.setProperty('--settings-target-bleed-right', `${Math.max(0, right)}px`)
+}
+
+export function clearSettingsTargetHighlightBleed(target: HTMLElement): void {
+ target.style.removeProperty('--settings-target-bleed-left')
+ target.style.removeProperty('--settings-target-bleed-right')
+}
+
+const SETTINGS_TARGET_REVEAL_EVENT = 'orca-settings-target-reveal'
+
+// Collapsed sections keep their rows mounted but hidden; the jump asks each one around the target to open.
+export function requestSettingsTargetReveal(target: HTMLElement): void {
+ target.dispatchEvent(new Event(SETTINGS_TARGET_REVEAL_EVENT, { bubbles: true }))
+}
+
+// A ref callback, so a disclosure that mounts later (or remounts) still gets the listener.
+export function useSettingsTargetReveal(
+ onReveal: () => void
+): RefCallback {
+ const onRevealRef = useRef(onReveal)
+ useLayoutEffect(() => {
+ onRevealRef.current = onReveal
+ })
+ return useCallback((node: T | null) => {
+ if (!node) {
+ return
+ }
+ const listener = (): void => onRevealRef.current()
+ node.addEventListener(SETTINGS_TARGET_REVEAL_EVENT, listener)
+ return () => node.removeEventListener(SETTINGS_TARGET_REVEAL_EVENT, listener)
+ }, [])
+}
diff --git a/src/renderer/src/components/settings/source-control/AutoRenameBranchFromWorkSetting.tsx b/src/renderer/src/components/settings/source-control/AutoRenameBranchFromWorkSetting.tsx
index a2408dbbf07..ceabbe37c75 100644
--- a/src/renderer/src/components/settings/source-control/AutoRenameBranchFromWorkSetting.tsx
+++ b/src/renderer/src/components/settings/source-control/AutoRenameBranchFromWorkSetting.tsx
@@ -19,7 +19,11 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../ui/co
import { Label } from '../../ui/label'
import { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover'
import { Switch } from '../../ui/switch'
-import { getAutoRenameBranchAdvancedSearchEntries } from './auto-rename-branch-search'
+import {
+ AUTO_RENAME_BRANCH_TEMPLATE_SETTING_ID,
+ getAutoRenameBranchAdvancedSearchEntries
+} from './auto-rename-branch-search'
+import { useSettingsTargetReveal } from '../section-layout/settings-target-highlight'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { matchesSettingsSearch, normalizeSettingsSearchQuery } from '../search/settings-search'
import { Trans } from 'react-i18next'
@@ -65,6 +69,7 @@ export function AutoRenameBranchFromWorkSetting({
const searchQuery = settingsSearchQuery ?? storeSearchQuery
const config = readSourceControlSettings(settings)
const [optionsOpen, setOptionsOpen] = useState(false)
+ const advancedRevealRef = useSettingsTargetReveal(() => setOptionsOpen(true))
const advancedSearchOpen = shouldOpenAutoRenameBranchAdvanced(searchQuery)
const advancedOpen = optionsOpen || advancedSearchOpen
const persistedBranchNameTemplate = resolveSourceControlActionCommandTemplate(
@@ -179,7 +184,7 @@ export function AutoRenameBranchFromWorkSetting({
/>
-
+
-
+ {/* Mounted while closed so a settings jump can find and reveal the template. */}
+
-
+
{t(
diff --git a/src/renderer/src/components/settings/source-control/HostedReviewCreationDefaults.tsx b/src/renderer/src/components/settings/source-control/HostedReviewCreationDefaults.tsx
index 27e66b67c24..2685580ed01 100644
--- a/src/renderer/src/components/settings/source-control/HostedReviewCreationDefaults.tsx
+++ b/src/renderer/src/components/settings/source-control/HostedReviewCreationDefaults.tsx
@@ -1,6 +1,7 @@
import type { SourceControlAiSettings } from '../../../../../shared/source-control/ai/settings/source-control-ai-settings-types'
import { Label } from '../../ui/label'
import { SearchableSetting } from '../section-layout/SearchableSetting'
+import { PR_CREATION_DEFAULTS_SETTING_ID } from './commit-message-ai-search'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
@@ -78,6 +79,7 @@ export function HostedReviewCreationDefaults({
return (
[
{
+ targetSectionId: AUTO_RENAME_BRANCH_TEMPLATE_SETTING_ID,
title: translate(
'auto.components.settings.auto.rename.branch.search.722551c5b3',
'Branch name command template'
diff --git a/src/renderer/src/components/settings/source-control/commit-message-ai-search.ts b/src/renderer/src/components/settings/source-control/commit-message-ai-search.ts
index 677ba8f56c2..1225726d4d8 100644
--- a/src/renderer/src/components/settings/source-control/commit-message-ai-search.ts
+++ b/src/renderer/src/components/settings/source-control/commit-message-ai-search.ts
@@ -2,6 +2,8 @@ import { translate } from '@/i18n/i18n'
import { translateSearchKeyword } from '../search/settings-search-keywords'
import { createLocalizedCatalog } from '@/i18n/localized-catalog'
+export const PR_CREATION_DEFAULTS_SETTING_ID = 'source-control-pr-creation-defaults'
+
export const getCommitMessageAiPaneSearchEntries = createLocalizedCatalog(() => [
{
title: translate(
@@ -124,6 +126,7 @@ export const getCommitMessageAiPaneSearchEntries = createLocalizedCatalog(() =>
]
},
{
+ targetSectionId: PR_CREATION_DEFAULTS_SETTING_ID,
title: translate(
'auto.components.settings.commit.message.ai.search.eefd33788c',
'PR creation defaults'
diff --git a/src/renderer/src/components/settings/ssh/SshPane.tsx b/src/renderer/src/components/settings/ssh/SshPane.tsx
index 45ed6b8a5c6..1db63d5cd53 100644
--- a/src/renderer/src/components/settings/ssh/SshPane.tsx
+++ b/src/renderer/src/components/settings/ssh/SshPane.tsx
@@ -9,6 +9,7 @@ import { SshTargetsEmptyState } from './targets/SshTargetsEmptyState'
import { SshTargetDestructiveActions } from './targets/SshTargetDestructiveActions'
import { SshHostsHeaderActions, SshHostsListToolbar } from './SshPaneToolbar'
import { SshHostsHeaderPortal } from './ssh-hosts-header-slot'
+import { SSH_SETTING_ANCHORS } from './ssh-search'
import { useSshTargetTests } from './targets/use-ssh-target-tests'
import { SshRestoreRemovedHostsDialog } from './targets/SshRestoreRemovedHostsDialog'
import { useSshConfigSync } from './targets/use-ssh-config-sync'
@@ -262,7 +263,7 @@ export function SshPane({ addTargetIntentSignal }: SshPaneProps): React.JSX.Elem
}
return (
-
+
: null
+ return context ? (
+
+ ) : null
}
/** Renders inline when no section header provides a slot (tests, other hosts of the pane). */
@@ -36,7 +43,11 @@ export function SshHostsHeaderPortal({
}): React.JSX.Element | null {
const context = useContext(SshHostsHeaderSlotContext)
if (!context) {
- return {children}
+ return (
+
+ )
}
return context.slot ? createPortal(children, context.slot) : null
}
diff --git a/src/renderer/src/components/settings/ssh/ssh-search.ts b/src/renderer/src/components/settings/ssh/ssh-search.ts
index f6e2c911d88..1193ff7761b 100644
--- a/src/renderer/src/components/settings/ssh/ssh-search.ts
+++ b/src/renderer/src/components/settings/ssh/ssh-search.ts
@@ -2,8 +2,14 @@ import { translate } from '@/i18n/i18n'
import { translateSearchKeyword } from '../search/settings-search-keywords'
import { createLocalizedCatalog } from '@/i18n/localized-catalog'
+export const SSH_SETTING_ANCHORS = {
+ hosts: 'ssh-hosts',
+ headerActions: 'ssh-host-actions'
+} as const
+
export const getSshPaneSearchEntries = createLocalizedCatalog(() => [
{
+ targetSectionId: SSH_SETTING_ANCHORS.hosts,
title: translate('auto.components.settings.ssh.search.53ae83cb55', 'SSH connections'),
description: translate('auto.components.settings.ssh.search.74c6d90d78', 'Manage SSH hosts.'),
keywords: [
@@ -15,6 +21,7 @@ export const getSshPaneSearchEntries = createLocalizedCatalog(() => [
]
},
{
+ targetSectionId: SSH_SETTING_ANCHORS.headerActions,
title: translate('auto.components.settings.ssh.search.004ef87a84', 'Add SSH target'),
description: translate('auto.components.settings.ssh.search.62826efbe9', 'Add a new SSH host.'),
keywords: [
@@ -27,6 +34,7 @@ export const getSshPaneSearchEntries = createLocalizedCatalog(() => [
]
},
{
+ targetSectionId: SSH_SETTING_ANCHORS.headerActions,
title: translate('auto.components.settings.ssh.search.893c8717f2', 'Import from SSH config'),
description: translate(
'auto.components.settings.ssh.search.7f251a45a8',
@@ -40,6 +48,7 @@ export const getSshPaneSearchEntries = createLocalizedCatalog(() => [
]
},
{
+ targetSectionId: SSH_SETTING_ANCHORS.hosts,
title: translate('auto.components.settings.ssh.search.50416e3809', 'Test connection'),
description: translate(
'auto.components.settings.ssh.search.96ca5d9a0b',
diff --git a/src/renderer/src/components/settings/terminal/platform/TerminalMacKeyboardSection.tsx b/src/renderer/src/components/settings/terminal/platform/TerminalMacKeyboardSection.tsx
index 1dcc224bdd8..ccb811fb460 100644
--- a/src/renderer/src/components/settings/terminal/platform/TerminalMacKeyboardSection.tsx
+++ b/src/renderer/src/components/settings/terminal/platform/TerminalMacKeyboardSection.tsx
@@ -2,6 +2,7 @@ import type { GlobalSettings } from '../../../../../../shared/global-settings-ty
import { useDetectedOptionAsAlt } from '@/lib/keyboard-layout/use-effective-mac-option-as-alt'
import { useTranslate } from '@/i18n/use-translate'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
+import { TERMINAL_MAC_YEN_SETTING_ID } from './terminal-advanced-platform-search'
import {
SettingsRow,
SettingsSegmentedControl,
@@ -123,6 +124,7 @@ export function TerminalMacKeyboardSection({
[
}
])
+export const TERMINAL_MAC_YEN_SETTING_ID = 'terminal-mac-jis-yen-to-backslash'
+
export const getTerminalMacYenSearchEntries = createLocalizedCatalog(() => [
{
+ targetSectionId: TERMINAL_MAC_YEN_SETTING_ID,
title: translate(
'auto.components.settings.terminal.search.694b8764ac',
'JIS Yen (¥) to Backslash (\\)'
diff --git a/src/renderer/src/components/settings/terminal/themes/TerminalThemeSections.tsx b/src/renderer/src/components/settings/terminal/themes/TerminalThemeSections.tsx
index 4077ec9de3c..9c0d56fa555 100644
--- a/src/renderer/src/components/settings/terminal/themes/TerminalThemeSections.tsx
+++ b/src/renderer/src/components/settings/terminal/themes/TerminalThemeSections.tsx
@@ -24,6 +24,7 @@ import {
import { ThemePicker } from './TerminalThemePicker'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
import { SettingsGroupSubsection } from '../../section-layout/SettingsGroup'
+import { TERMINAL_THEME_MODE_SETTING_ID } from './terminal-theme-search'
import { TerminalSettingsPreview } from '../appearance/TerminalSettingsPreview'
import { WarpThemeImportButton } from './WarpThemeImportButton'
import { YamlThemeImportButton } from './YamlThemeImportButton'
@@ -189,6 +190,7 @@ export function TerminalThemeCatalogSection({
}
>
[
{
title: translate('auto.components.settings.terminal.search.ee9665265e', 'Dark theme'),
@@ -56,6 +59,7 @@ export const getTerminalThemeTargetSearchEntries = createLocalizedCatalog(() =>
export const getTerminalLightThemeSearchEntries = createLocalizedCatalog(() => [
{
+ targetSectionId: TERMINAL_THEME_MODE_SETTING_ID,
title: translate(
'auto.components.settings.terminal.search.match_dark_mode_title',
'Match dark mode'
@@ -101,6 +105,7 @@ export const getTerminalLightThemeSearchEntries = createLocalizedCatalog(() => [
]
},
{
+ targetSectionId: TERMINAL_THEME_MODE_SETTING_ID,
title: translate('auto.components.settings.terminal.search.18a9f63aff', 'Light divider color'),
description: translate(
'auto.components.settings.terminal.search.77d9f9cd55',
diff --git a/src/renderer/src/components/settings/voice/VoiceDictationLanguageSetting.tsx b/src/renderer/src/components/settings/voice/VoiceDictationLanguageSetting.tsx
index 6cfdab20790..5cffd808cc2 100644
--- a/src/renderer/src/components/settings/voice/VoiceDictationLanguageSetting.tsx
+++ b/src/renderer/src/components/settings/voice/VoiceDictationLanguageSetting.tsx
@@ -9,6 +9,7 @@ import { useIntlLocale, useTranslate, type Translate } from '@/i18n/use-translat
import { languageDisplayName, uiCollator } from '@/i18n/ui-intl-formatters'
import { SettingsRow } from '../form-controls/SettingsFormControls'
import { SettingsSearchableSelect } from '../form-controls/SettingsSearchableSelect'
+import { VOICE_SETTING_ANCHORS } from './voice-pane-search'
// Intl formatter caches key on option identity, so this must be a module constant.
const NAME_COLLATOR: Intl.CollatorOptions = { sensitivity: 'base' }
@@ -81,6 +82,7 @@ export function VoiceDictationLanguageSetting({
return (
({
+ targetSectionId: VOICE_SETTING_ANCHORS.speechModel,
title: translate(
'auto.components.settings.voice.pane.search.ffaa3efe7c',
'OpenAI transcription'
@@ -68,6 +76,7 @@ export const getVoicePaneSearchEntries = createLocalizedCatalog(() => [
]
},
{
+ targetSectionId: VOICE_SETTING_ANCHORS.mode,
title: translate('auto.components.settings.voice.pane.search.d0c230c061', 'Dictation mode'),
description: translate(
'auto.components.settings.voice.pane.search.748b33e531',
@@ -114,6 +123,7 @@ export const getVoicePaneSearchEntries = createLocalizedCatalog(() => [
]
},
{
+ targetSectionId: VOICE_SETTING_ANCHORS.language,
title: translate('auto.components.settings.voice.pane.search.9d83747eae', 'Dictation language'),
description: translate(
'auto.components.settings.voice.pane.search.dictationLanguageDescription',
diff --git a/src/renderer/src/i18n/locales/de.json b/src/renderer/src/i18n/locales/de.json
index 261b776a760..0c83b1a6630 100644
--- a/src/renderer/src/i18n/locales/de.json
+++ b/src/renderer/src/i18n/locales/de.json
@@ -9029,8 +9029,7 @@
"60f8a673a7": "Zur App zurückkehren",
"6503182299": "Checkliste für den Einstieg",
"82db1b7de4": "Einrichtungscheckliste, {{value0}} von {{value1}} erledigt. Einrichtungsanleitung anzeigen.",
- "df38d612b7": "Noch keine Projekte hinzugefügt.",
- "3e483e256b": "Keine passenden Projekteinstellungen."
+ "df38d612b7": "Noch keine Projekte hinzugefügt."
},
"ShortcutFilterRail": {
"28b63545bf": "Status",
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index 81f1cdadd6d..3b06d46ca0a 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -1414,6 +1414,12 @@
"label": "Settings breadcrumb",
"close": "Close settings"
},
+ "search": {
+ "clear": "Clear search",
+ "results": "Settings search results",
+ "noResults": "No settings match “{{value0}}”",
+ "moreResults": "{{value0}} more · keep typing to narrow"
+ },
"voice": {
"cleanup": {
"recipeDescription": "Clean up dictated text before it lands."
@@ -9712,7 +9718,6 @@
"6503182299": "Onboarding checklist",
"82db1b7de4": "Onboarding checklist, {{value0}} of {{value1}} done. Show setup guide.",
"df38d612b7": "No projects added yet.",
- "3e483e256b": "No matching project settings.",
"progressComplete": "{{value0}}/{{value1}} complete"
},
"ShortcutFilterRail": {
diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json
index cb07a2b0a83..ee0c7657740 100644
--- a/src/renderer/src/i18n/locales/es.json
+++ b/src/renderer/src/i18n/locales/es.json
@@ -8600,8 +8600,7 @@
"60f8a673a7": "Volver a la app",
"6503182299": "Checklist de onboarding",
"82db1b7de4": "Checklist de onboarding, {{value0}} de {{value1}} completados. Mostrar guía de configuración.",
- "df38d612b7": "Aún no se han agregado proyectos.",
- "3e483e256b": "No hay ajustes de proyecto coincidentes."
+ "df38d612b7": "Aún no se han agregado proyectos."
},
"ShortcutFilterRail": {
"28b63545bf": "Estado",
diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json
index 97a5ef03aac..f399f11898a 100644
--- a/src/renderer/src/i18n/locales/fr.json
+++ b/src/renderer/src/i18n/locales/fr.json
@@ -8913,8 +8913,7 @@
"60f8a673a7": "Retour à l'application",
"6503182299": "Checklist d'intégration",
"82db1b7de4": "Checklist d'intégration, {{value0}} sur {{value1}} terminés. Afficher le guide de configuration.",
- "df38d612b7": "Aucun projet ajouté pour le moment.",
- "3e483e256b": "Aucun paramètre de projet correspondant."
+ "df38d612b7": "Aucun projet ajouté pour le moment."
},
"ShortcutFilterRail": {
"28b63545bf": "Statut",
diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json
index 496260dc2a9..7f5f01a0252 100644
--- a/src/renderer/src/i18n/locales/ja.json
+++ b/src/renderer/src/i18n/locales/ja.json
@@ -8347,8 +8347,7 @@
"60f8a673a7": "アプリに戻る",
"6503182299": "オンボーディングチェックリスト",
"82db1b7de4": "オンボーディングチェックリスト、{{value0}}/{{value1}} 完了。セットアップガイドを表示。",
- "df38d612b7": "まだ追加されたプロジェクトはありません。",
- "3e483e256b": "一致するプロジェクト設定がありません。"
+ "df38d612b7": "まだ追加されたプロジェクトはありません。"
},
"ShortcutFilterRail": {
"28b63545bf": "状態",
diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json
index 8a245a647fc..d6b619537b2 100644
--- a/src/renderer/src/i18n/locales/ko.json
+++ b/src/renderer/src/i18n/locales/ko.json
@@ -8307,8 +8307,7 @@
"60f8a673a7": "앱으로 돌아가기",
"6503182299": "온보딩 체크리스트",
"82db1b7de4": "온보딩 체크리스트, {{value0}}/{{value1}} 완료. 설정 가이드 보기.",
- "df38d612b7": "아직 추가된 프로젝트가 없습니다.",
- "3e483e256b": "일치하는 프로젝트 설정이 없습니다."
+ "df38d612b7": "아직 추가된 프로젝트가 없습니다."
},
"ShortcutFilterRail": {
"28b63545bf": "상태",
diff --git a/src/renderer/src/i18n/locales/pt.json b/src/renderer/src/i18n/locales/pt.json
index fa1b2152ae7..e59c8c9b986 100644
--- a/src/renderer/src/i18n/locales/pt.json
+++ b/src/renderer/src/i18n/locales/pt.json
@@ -8581,8 +8581,7 @@
"60f8a673a7": "Voltar ao app",
"6503182299": "Checklist de introdução",
"82db1b7de4": "Checklist de introdução, {{value0}} de {{value1}} concluído(s). Mostrar guia de configuração.",
- "df38d612b7": "Nenhum projeto adicionado ainda.",
- "3e483e256b": "Nenhuma configuração de projeto correspondente."
+ "df38d612b7": "Nenhum projeto adicionado ainda."
},
"computer": {
"use": {
diff --git a/src/renderer/src/i18n/locales/ru.json b/src/renderer/src/i18n/locales/ru.json
index 1968d2bcb98..3645581871e 100644
--- a/src/renderer/src/i18n/locales/ru.json
+++ b/src/renderer/src/i18n/locales/ru.json
@@ -9374,8 +9374,7 @@
"60f8a673a7": "Вернуться в приложение",
"6503182299": "Знакомство с Orca",
"82db1b7de4": "Список начальной настройки: выполнено {{value0}} из {{value1}}. Показать руководство.",
- "df38d612b7": "Проекты ещё не добавлены.",
- "3e483e256b": "Подходящие настройки проектов не найдены."
+ "df38d612b7": "Проекты ещё не добавлены."
},
"ShortcutFilterRail": {
"28b63545bf": "Статус",
diff --git a/src/renderer/src/i18n/locales/tr.json b/src/renderer/src/i18n/locales/tr.json
index ab9382136ad..2442c221309 100644
--- a/src/renderer/src/i18n/locales/tr.json
+++ b/src/renderer/src/i18n/locales/tr.json
@@ -9006,8 +9006,7 @@
"60f8a673a7": "Uygulamaya dön",
"6503182299": "Başlangıç kontrol listesi",
"82db1b7de4": "Başlangıç kontrol listesi, {{value1}} adımdan {{value0}} tanesi tamamlandı. Kurulum kılavuzunu göster.",
- "df38d612b7": "Henüz proje eklenmedi.",
- "3e483e256b": "Eşleşen proje ayarı yok."
+ "df38d612b7": "Henüz proje eklenmedi."
},
"ShortcutFilterRail": {
"28b63545bf": "Durum",
diff --git a/src/renderer/src/i18n/locales/zh-Hant.json b/src/renderer/src/i18n/locales/zh-Hant.json
index 54a38e3e028..fceccf2497b 100644
--- a/src/renderer/src/i18n/locales/zh-Hant.json
+++ b/src/renderer/src/i18n/locales/zh-Hant.json
@@ -8850,8 +8850,7 @@
"60f8a673a7": "返回應用程式",
"6503182299": "入門清單",
"82db1b7de4": "入門清單,已完成 {{value0}}/{{value1}}。顯示設定指南。",
- "df38d612b7": "尚未新增專案。",
- "3e483e256b": "沒有符合的專案設定。"
+ "df38d612b7": "尚未新增專案。"
},
"ShortcutFilterRail": {
"28b63545bf": "狀態",
diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json
index 8f692763ce8..d8a00d1e5ad 100644
--- a/src/renderer/src/i18n/locales/zh.json
+++ b/src/renderer/src/i18n/locales/zh.json
@@ -8527,7 +8527,6 @@
"6503182299": "入门清单",
"82db1b7de4": "入门清单,已完成 {{value0}}/{{value1}},显示设置指南",
"df38d612b7": "尚未添加项目",
- "3e483e256b": "没有匹配的项目设置",
"progressComplete": "已完成 {{value0}}/{{value1}}"
},
"ShortcutFilterRail": {
diff --git a/src/renderer/src/ssh/node-dist-mirror-recovery.ts b/src/renderer/src/ssh/node-dist-mirror-recovery.ts
index bceed52c150..3ff343dd0aa 100644
--- a/src/renderer/src/ssh/node-dist-mirror-recovery.ts
+++ b/src/renderer/src/ssh/node-dist-mirror-recovery.ts
@@ -7,6 +7,7 @@ import type { SshManagedServerStatus } from '../../../shared/ssh-types'
import { translate } from '@/i18n/i18n'
import { isPairedWebClientWindow } from '@/app-shell/chrome/desktop-window-chrome'
import { useAppStore } from '@/store/index'
+import { NODE_DIST_MIRROR_SETTINGS_TARGET_ID } from '@/components/settings/navigation/settings-navigation-types'
import {
isSshConnectInFlight,
subscribeSshConnectInFlight,
@@ -56,14 +57,15 @@ export function readNodeDistMirrorDialogTarget(data: Record): s
return typeof data.targetId === 'string' && data.targetId ? data.targetId : null
}
-// Search opens collapsed fields too when only the environment variable supplied a mirror.
+// The jump reveals the collapsed proxy fields even when only the environment supplied a mirror.
function openNodeDistMirrorSettings(): void {
- const { openSettingsPage, openSettingsTarget, setSettingsSearchQuery } = useAppStore.getState()
+ const { openSettingsPage, openSettingsTarget } = useAppStore.getState()
openSettingsPage()
- openSettingsTarget({ pane: 'advanced', repoId: null })
- setSettingsSearchQuery(
- translate('auto.components.settings.general.search.nodeDistMirror', 'mirror')
- )
+ openSettingsTarget({
+ pane: 'advanced',
+ repoId: null,
+ sectionId: NODE_DIST_MIRROR_SETTINGS_TARGET_ID
+ })
}
export type SaveNodeDistMirrorResult =
diff --git a/src/renderer/src/store/slices/settings/settings-search-state.ts b/src/renderer/src/store/slices/settings/settings-search-state.ts
index c691e1199a8..6cdafacfcd9 100644
--- a/src/renderer/src/store/slices/settings/settings-search-state.ts
+++ b/src/renderer/src/store/slices/settings/settings-search-state.ts
@@ -1,7 +1,6 @@
-export const SETTINGS_SEARCH_DEBOUNCE_MS = 150
-
export type SettingsSearchState = {
settingsSearchInputQuery: string
+ /** Content filter; the sidebar search no longer drives it, so it stays empty in the app. */
settingsSearchQuery: string
setSettingsSearchQuery: (q: string) => void
}
@@ -9,31 +8,10 @@ export type SettingsSearchState = {
export function createSettingsSearchState(
set: (state: Partial) => void
): SettingsSearchState {
- let settingsSearchDebounceTimer: ReturnType | null = null
-
- const clearSettingsSearchDebounce = (): void => {
- if (settingsSearchDebounceTimer) {
- clearTimeout(settingsSearchDebounceTimer)
- settingsSearchDebounceTimer = null
- }
- }
-
return {
settingsSearchInputQuery: '',
settingsSearchQuery: '',
- setSettingsSearchQuery: (q) => {
- clearSettingsSearchDebounce()
- if (q.trim() === '') {
- set({ settingsSearchInputQuery: q, settingsSearchQuery: q })
- return
- }
- set({ settingsSearchInputQuery: q })
- // Why: applying settings search mounts and filters many heavy sections,
- // so keep typing responsive while waiting for the query to settle.
- settingsSearchDebounceTimer = setTimeout(() => {
- settingsSearchDebounceTimer = null
- set({ settingsSearchQuery: q })
- }, SETTINGS_SEARCH_DEBOUNCE_MS)
- }
+ // Why input only: the sidebar lists matching settings and the page stays unfiltered.
+ setSettingsSearchQuery: (q) => set({ settingsSearchInputQuery: q })
}
}
diff --git a/tests/e2e/claude-code-theme-sync.spec.ts b/tests/e2e/claude-code-theme-sync.spec.ts
index f893842f615..f9af24e2110 100644
--- a/tests/e2e/claude-code-theme-sync.spec.ts
+++ b/tests/e2e/claude-code-theme-sync.spec.ts
@@ -10,6 +10,14 @@ import {
waitForActiveTerminalManager
} from './helpers/terminal'
+// Search lists results; the page moves on an explicit pick.
+async function openSettingsSearchResult(page: Page, query: string): Promise {
+ const searchInput = page.getByPlaceholder('Search settings')
+ await searchInput.fill(query)
+ await searchInput.press('ArrowDown')
+ await searchInput.press('Enter')
+}
+
const enabled = process.env.ORCA_REAL_CLAUDE_THEME_TEST === '1'
// Claude Code's own light theme paints its prompt border this gray.
const CLAUDE_LIGHT_BORDER = 0x8a8a8a
@@ -236,7 +244,7 @@ test('an existing user accepts the offer, then Claude Code follows the Orca them
// Turning it off through the real switch restores the user's theme, live too.
await orcaPage.evaluate(() => window.__store!.getState().openSettingsPage())
- await orcaPage.getByPlaceholder('Search settings').fill('Match Claude Code')
+ await openSettingsSearchResult(orcaPage, 'Match Claude Code')
const toggle = orcaPage.getByRole('switch', { name: 'Match Claude Code to Terminal Theme' })
await expect(toggle).toBeChecked()
await windowShot('settings-on-light')
@@ -251,7 +259,7 @@ test('an existing user accepts the offer, then Claude Code follows the Orca them
// The same switch in dark mode, off then on.
await updateSettings({ theme: 'dark' })
await orcaPage.evaluate(() => window.__store!.getState().openSettingsPage())
- await orcaPage.getByPlaceholder('Search settings').fill('Match Claude Code')
+ await openSettingsSearchResult(orcaPage, 'Match Claude Code')
await expect(toggle).not.toBeChecked()
await windowShot('settings-off-dark')
await toggle.click()
diff --git a/tests/e2e/keybinding-terminal-conflict-diagnostic.spec.ts b/tests/e2e/keybinding-terminal-conflict-diagnostic.spec.ts
index fe962a72b37..89f484e403d 100644
--- a/tests/e2e/keybinding-terminal-conflict-diagnostic.spec.ts
+++ b/tests/e2e/keybinding-terminal-conflict-diagnostic.spec.ts
@@ -35,13 +35,16 @@ test.describe('Terminal shortcut conflicting with a chord main intercepts', () =
const searchInput = orcaPage.getByPlaceholder('Search settings')
await expect(searchInput).toBeVisible()
await searchInput.fill('shortcuts')
+ // Search lists results; the page moves on an explicit pick.
+ await searchInput.press('ArrowDown')
+ await searchInput.press('Enter')
await expect(orcaPage.getByRole('heading', { name: 'Shortcuts', exact: true })).toBeVisible()
await orcaPage.getByRole('button', { name: /Conflicts/ }).click()
await expect(orcaPage.getByText('Split terminal right', { exact: true })).toBeVisible()
- await expect(orcaPage.getByText('Worktree History Forward', { exact: true })).toBeVisible()
+ await expect(orcaPage.getByText('Worktree history forward', { exact: true })).toBeVisible()
await expect(
- orcaPage.getByText(/conflicts with .*Worktree History Forward/).first()
+ orcaPage.getByText(/conflicts with .*Worktree history forward/).first()
).toBeVisible()
})
})
diff --git a/tests/e2e/settings-search-shortcuts-pane.spec.ts b/tests/e2e/settings-search-shortcuts-pane.spec.ts
index 5eb774ba332..7b684a2aeb7 100644
--- a/tests/e2e/settings-search-shortcuts-pane.spec.ts
+++ b/tests/e2e/settings-search-shortcuts-pane.spec.ts
@@ -2,7 +2,9 @@ import { test, expect } from './helpers/orca-app'
import { waitForSessionReady } from './helpers/store'
test.describe('Settings sidebar search on the Shortcuts pane', () => {
- test('pane-title-only query keeps rows visible and local search usable', async ({ orcaPage }) => {
+ test('picking a result opens the Shortcuts pane unfiltered with local search usable', async ({
+ orcaPage
+ }) => {
await waitForSessionReady(orcaPage)
await orcaPage.evaluate(async () => {
@@ -20,21 +22,21 @@ test.describe('Settings sidebar search on the Shortcuts pane', () => {
await expect(searchInput).toBeFocused()
await searchInput.fill('shortcuts')
+ // Typing only lists results; the page moves on an explicit pick.
+ await expect(orcaPage.getByRole('option').first()).toBeVisible()
+ await expect(
+ orcaPage.getByRole('heading', { name: 'Shortcuts', exact: true })
+ ).not.toBeVisible()
+ await searchInput.press('ArrowDown')
+ await searchInput.press('Enter')
await expect(orcaPage.getByRole('heading', { name: 'Shortcuts', exact: true })).toBeVisible()
- // Regression: a pane-title-only query used to blank the whole list (0/112).
- await expect(orcaPage.getByText('Go to File', { exact: true })).toBeVisible()
- await expect(orcaPage.getByText('No shortcuts match those filters.')).not.toBeVisible()
-
- // Keep pane search working with a global query; the old empty base list broke their intersection.
+ // The sidebar query no longer filters the pane, so every row and the pane's own search stay usable.
+ await expect(orcaPage.getByText('Go to file', { exact: true })).toBeVisible()
+ await expect(searchInput).toHaveValue('shortcuts')
const localSearch = orcaPage.getByPlaceholder('Search command or keys')
await localSearch.fill('go to')
- await expect(orcaPage.getByText('Go to File', { exact: true })).toBeVisible()
- await expect(orcaPage.getByText('Force Reload', { exact: true })).not.toBeVisible()
-
- await localSearch.clear()
- await searchInput.fill('worktree')
- await expect(orcaPage.getByText('Create worktree', { exact: true })).toBeVisible()
- await expect(orcaPage.getByText('Go to File', { exact: true })).not.toBeVisible()
+ await expect(orcaPage.getByText('Go to file', { exact: true })).toBeVisible()
+ await expect(orcaPage.getByText('Force reload', { exact: true })).not.toBeVisible()
})
})