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()
})
})