-
-
When to Run Setup
-
- Choose the default behavior when a setup command is available.
-
-
+
+
+
+
When to Run Setup
+
+ Choose the default behavior when a setup command is available.
+
+
-
- {SETUP_RUN_POLICY_OPTIONS.map(({ policy, label, description }) => {
- const selected = selectedSetupRunPolicy === policy
+
+ {SETUP_RUN_POLICY_OPTIONS.map(({ policy, label, description }) => {
+ const selected = selectedSetupRunPolicy === policy
- return (
-
onUpdateSetupRunPolicy(policy)}
- className={`rounded-xl border px-3 py-2.5 text-center transition-colors ${
- selected
- ? 'border-foreground/15 bg-accent text-accent-foreground'
- : 'border-border/60 bg-background text-foreground hover:border-border hover:bg-muted/40'
- }`}
- >
-
- {label}
-
- onUpdateSetupRunPolicy(policy)}
+ className={`rounded-xl border px-3 py-2.5 text-center transition-colors ${
+ selected
+ ? 'border-foreground/15 bg-accent text-accent-foreground'
+ : 'border-border/60 bg-background text-foreground hover:border-border hover:bg-muted/40'
}`}
>
- {description}
-
-
- )
- })}
+
+ {label}
+
+
+ {description}
+
+
+ )
+ })}
+
-
+
)
}
diff --git a/src/renderer/src/components/settings/RepositoryPane.tsx b/src/renderer/src/components/settings/RepositoryPane.tsx
index f34d78a44..4e491a4e7 100644
--- a/src/renderer/src/components/settings/RepositoryPane.tsx
+++ b/src/renderer/src/components/settings/RepositoryPane.tsx
@@ -9,6 +9,9 @@ import { Trash2 } from 'lucide-react'
import { DEFAULT_REPO_HOOK_SETTINGS } from './SettingsConstants'
import { BaseRefPicker } from './BaseRefPicker'
import { RepositoryHooksSection } from './RepositoryHooksSection'
+import { SearchableSetting } from './SearchableSetting'
+import { matchesSettingsSearch, type SettingsSearchEntry } from './settings-search'
+import { useAppStore } from '../../store'
type RepositoryPaneProps = {
repo: Repo
@@ -18,6 +21,46 @@ type RepositoryPaneProps = {
removeRepo: (repoId: string) => void
}
+export function getRepositoryPaneSearchEntries(repo: Repo): SettingsSearchEntry[] {
+ return [
+ {
+ title: 'Display Name',
+ description: 'Repo-specific display details for the sidebar and tabs.',
+ keywords: [repo.displayName, repo.path, 'repository name']
+ },
+ {
+ title: 'Badge Color',
+ description: 'Repo color used in the sidebar and tabs.',
+ keywords: [repo.displayName, 'color', 'badge']
+ },
+ {
+ title: 'Default Worktree Base',
+ description: 'Default base branch or ref when creating worktrees.',
+ keywords: [repo.displayName, 'base ref', 'branch']
+ },
+ {
+ title: 'Remove Repo',
+ description: 'Remove this repository from Orca.',
+ keywords: [repo.displayName, 'delete', 'repository']
+ },
+ {
+ title: 'orca.yaml hooks',
+ description: 'Shared setup and archive hook commands for this repository.',
+ keywords: [repo.displayName, 'hooks', 'setup', 'archive', 'yaml']
+ },
+ {
+ title: 'Legacy Repo-Local Hooks',
+ description: 'Older setup and archive hook scripts stored in local repo settings.',
+ keywords: [repo.displayName, 'legacy', 'fallback', 'hooks']
+ },
+ {
+ title: 'When to Run Setup',
+ description: 'Choose the default behavior when a setup command is available.',
+ keywords: [repo.displayName, 'setup run policy', 'ask', 'run by default', 'skip by default']
+ }
+ ]
+}
+
export function RepositoryPane({
repo,
yamlHooks,
@@ -25,6 +68,7 @@ export function RepositoryPane({
updateRepo,
removeRepo
}: RepositoryPaneProps): React.JSX.Element {
+ const searchQuery = useAppStore((state) => state.settingsSearchQuery)
const [confirmingRemove, setConfirmingRemove] = useState
(null)
const [copiedTemplate, setCopiedTemplate] = useState(false)
@@ -83,30 +127,45 @@ export function RepositoryPane({
})
}
- return (
-
-
+ const allEntries = getRepositoryPaneSearchEntries(repo)
+ const identityEntries = allEntries.slice(0, 4)
+ const hooksEntries = allEntries.slice(4)
+
+ const visibleSections = [
+ matchesSettingsSearch(searchQuery, identityEntries) ? (
+
-
Identity
+
Identity
Repo-specific display details for the sidebar and tabs.
-
handleRemoveRepo(repo.id)}
- onBlur={() => setConfirmingRemove(null)}
- className="gap-2"
+
-
- {confirmingRemove === repo.id ? 'Confirm Remove' : 'Remove Repo'}
-
+
handleRemoveRepo(repo.id)}
+ onBlur={() => setConfirmingRemove(null)}
+ className="gap-2"
+ >
+
+ {confirmingRemove === repo.id ? 'Confirm Remove' : 'Remove Repo'}
+
+
-
+
Display Name
-
+
-
+
Badge Color
{REPO_COLORS.map((color) => (
@@ -136,9 +200,14 @@ export function RepositoryPane({
/>
))}
-
+
-
+
Default Worktree Base
updateRepo(repo.id, { worktreeBaseRef: ref })}
onUsePrimary={() => updateRepo(repo.id, { worktreeBaseRef: undefined })}
/>
-
+
-
-
-
+ ) : null,
+ matchesSettingsSearch(searchQuery, hooksEntries) ? (
+ ) : null
+ ].filter(Boolean)
+
+ return (
+
+ {visibleSections.map((section, index) => (
+
+ {index > 0 ? : null}
+ {section}
+
+ ))}
)
}
diff --git a/src/renderer/src/components/settings/SearchableSetting.tsx b/src/renderer/src/components/settings/SearchableSetting.tsx
new file mode 100644
index 000000000..61c4b17bc
--- /dev/null
+++ b/src/renderer/src/components/settings/SearchableSetting.tsx
@@ -0,0 +1,23 @@
+import type React from 'react'
+import { useAppStore } from '../../store'
+import { matchesSettingsSearch, type SettingsSearchEntry } from './settings-search'
+
+type SearchableSettingProps = SettingsSearchEntry & {
+ children: React.ReactNode
+ className?: string
+}
+
+export function SearchableSetting({
+ title,
+ description,
+ keywords,
+ children,
+ className
+}: SearchableSettingProps): React.JSX.Element | null {
+ const query = useAppStore((state) => state.settingsSearchQuery)
+ if (!matchesSettingsSearch(query, { title, description, keywords })) {
+ return null
+ }
+
+ return {children}
+}
diff --git a/src/renderer/src/components/settings/Settings.tsx b/src/renderer/src/components/settings/Settings.tsx
index 48624d065..1577f0dc5 100644
--- a/src/renderer/src/components/settings/Settings.tsx
+++ b/src/renderer/src/components/settings/Settings.tsx
@@ -1,16 +1,38 @@
-import { useEffect, useState, useCallback, useRef } from 'react'
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { Keyboard, Palette, SlidersHorizontal, SquareTerminal } from 'lucide-react'
import type { OrcaHooks } from '../../../../shared/types'
import { useAppStore } from '../../store'
-import { ScrollArea } from '../ui/scroll-area'
-import { Button } from '../ui/button'
-import { ArrowLeft, Palette, SlidersHorizontal, SquareTerminal, Keyboard } from 'lucide-react'
import { getSystemPrefersDark } from '@/lib/terminal-theme'
import { SCROLLBACK_PRESETS_MB, getFallbackTerminalFonts } from './SettingsConstants'
-import { GeneralPane } from './GeneralPane'
-import { AppearancePane } from './AppearancePane'
-import { ShortcutsPane } from './ShortcutsPane'
-import { TerminalPane } from './TerminalPane'
-import { RepositoryPane } from './RepositoryPane'
+import { GeneralPane, GENERAL_PANE_SEARCH_ENTRIES } from './GeneralPane'
+import { AppearancePane, APPEARANCE_PANE_SEARCH_ENTRIES } from './AppearancePane'
+import { ShortcutsPane, SHORTCUTS_PANE_SEARCH_ENTRIES } from './ShortcutsPane'
+import { TerminalPane, TERMINAL_PANE_SEARCH_ENTRIES } from './TerminalPane'
+import { RepositoryPane, getRepositoryPaneSearchEntries } from './RepositoryPane'
+import { SettingsSidebar } from './SettingsSidebar'
+import { SettingsSection } from './SettingsSection'
+import { matchesSettingsSearch, type SettingsSearchEntry } from './settings-search'
+
+type SettingsNavTarget = 'general' | 'appearance' | 'terminal' | 'shortcuts' | 'repo'
+
+type SettingsNavSection = {
+ id: string
+ title: string
+ description: string
+ icon: typeof SlidersHorizontal
+ searchEntries: SettingsSearchEntry[]
+}
+
+function getSettingsSectionId(pane: SettingsNavTarget, repoId: string | null): string {
+ if (pane === 'repo' && repoId) {
+ return `repo-${repoId}`
+ }
+ return pane
+}
+
+function getFallbackVisibleSection(sections: SettingsNavSection[]): SettingsNavSection | undefined {
+ return sections.at(0)
+}
function Settings(): React.JSX.Element {
const settings = useAppStore((s) => s.settings)
@@ -22,11 +44,9 @@ function Settings(): React.JSX.Element {
const removeRepo = useAppStore((s) => s.removeRepo)
const settingsNavigationTarget = useAppStore((s) => s.settingsNavigationTarget)
const clearSettingsTarget = useAppStore((s) => s.clearSettingsTarget)
+ const settingsSearchQuery = useAppStore((s) => s.settingsSearchQuery)
+ const setSettingsSearchQuery = useAppStore((s) => s.setSettingsSearchQuery)
- const [selectedPane, setSelectedPane] = useState<
- 'general' | 'appearance' | 'terminal' | 'shortcuts' | 'repo'
- >('general')
- const [selectedRepoId, setSelectedRepoId] = useState(null)
const [repoHooksMap, setRepoHooksMap] = useState<
Record
>({})
@@ -36,23 +56,36 @@ function Settings(): React.JSX.Element {
const [terminalFontSuggestions, setTerminalFontSuggestions] = useState(
getFallbackTerminalFonts()
)
+ const [activeSectionId, setActiveSectionId] = useState('general')
+ const contentScrollRef = useRef(null)
const terminalFontsLoadedRef = useRef(false)
+ const pendingScrollTargetRef = useRef(null)
useEffect(() => {
fetchSettings()
}, [fetchSettings])
+ useEffect(
+ () => () => {
+ // Why: the settings search is a transient in-page filter. Leaving it behind makes the next
+ // visit look partially broken because whole sections stay hidden before the user types again.
+ setSettingsSearchQuery('')
+ },
+ [setSettingsSearchQuery]
+ )
+
useEffect(() => {
if (!settingsNavigationTarget) {
return
}
- // Why: the create-worktree dialog links here so setup configuration stays
- // out of the dialog until the user explicitly asks to edit it.
- setSelectedPane(settingsNavigationTarget.pane)
- if (settingsNavigationTarget.repoId) {
- setSelectedRepoId(settingsNavigationTarget.repoId)
- }
+ // Why: settings entry points elsewhere in the app target a section, not a
+ // transient tab, so the scroll-based settings page needs an explicit anchor
+ // handoff to land the user on the intended configuration block.
+ pendingScrollTargetRef.current = getSettingsSectionId(
+ settingsNavigationTarget.pane,
+ settingsNavigationTarget.repoId
+ )
clearSettingsTarget()
}, [clearSettingsTarget, settingsNavigationTarget])
@@ -67,7 +100,7 @@ function Settings(): React.JSX.Element {
}, [])
useEffect(() => {
- if (selectedPane !== 'terminal' || terminalFontsLoadedRef.current) {
+ if (terminalFontsLoadedRef.current) {
return
}
@@ -91,7 +124,7 @@ function Settings(): React.JSX.Element {
return () => {
stale = true
}
- }, [selectedPane])
+ }, [])
if (settings !== prevSettings) {
setPrevSettings(settings)
@@ -107,7 +140,8 @@ function Settings(): React.JSX.Element {
useEffect(() => {
let stale = false
- const checkHooks = async () => {
+
+ const checkHooks = async (): Promise => {
const results = await Promise.all(
repos.map(async (repo) => {
try {
@@ -125,7 +159,7 @@ function Settings(): React.JSX.Element {
}
if (repos.length > 0) {
- checkHooks()
+ void checkHooks()
} else {
setRepoHooksMap({})
}
@@ -135,18 +169,6 @@ function Settings(): React.JSX.Element {
}
}, [repos])
- // Validate selectedRepoId against current repos (adjusting state during render)
- if (repos.length === 0) {
- if (selectedRepoId !== null) {
- setSelectedRepoId(null)
- if (selectedPane === 'repo') {
- setSelectedPane('general')
- }
- }
- } else if (!selectedRepoId || !repos.some((repo) => repo.id === selectedRepoId)) {
- setSelectedRepoId(repos[0].id)
- }
-
const applyTheme = useCallback((theme: 'system' | 'dark' | 'light') => {
const root = document.documentElement
if (theme === 'dark') {
@@ -163,15 +185,139 @@ function Settings(): React.JSX.Element {
}
}, [])
- const selectedRepo = repos.find((repo) => repo.id === selectedRepoId) ?? null
- const selectedRepoHooksState = selectedRepo ? repoHooksMap[selectedRepo.id] : undefined
- const selectedYamlHooks = selectedRepoHooksState?.hooks ?? null
- const showGeneralPane = selectedPane === 'general'
- const showAppearancePane = selectedPane === 'appearance'
- const showTerminalPane = selectedPane === 'terminal'
- const showShortcutsPane = selectedPane === 'shortcuts'
- const showRepoPane = selectedPane === 'repo' && !!selectedRepo
- const displayedGitUsername = (selectedRepo ?? repos[0])?.gitUsername ?? ''
+ const displayedGitUsername = repos[0]?.gitUsername ?? ''
+
+ const navSections = useMemo(
+ () => [
+ {
+ id: 'general',
+ title: 'General',
+ description: 'Workspace, editor, naming, and updates.',
+ icon: SlidersHorizontal,
+ searchEntries: GENERAL_PANE_SEARCH_ENTRIES
+ },
+ {
+ id: 'appearance',
+ title: 'Appearance',
+ description: 'Theme and UI scaling.',
+ icon: Palette,
+ searchEntries: APPEARANCE_PANE_SEARCH_ENTRIES
+ },
+ {
+ id: 'terminal',
+ title: 'Terminal',
+ description: 'Terminal appearance, previews, and defaults for new panes.',
+ icon: SquareTerminal,
+ searchEntries: TERMINAL_PANE_SEARCH_ENTRIES
+ },
+ {
+ id: 'shortcuts',
+ title: 'Shortcuts',
+ description: 'Keyboard shortcuts for common actions.',
+ icon: Keyboard,
+ searchEntries: SHORTCUTS_PANE_SEARCH_ENTRIES
+ },
+ ...repos.map((repo) => ({
+ id: `repo-${repo.id}`,
+ title: repo.displayName,
+ description: repo.path,
+ icon: SlidersHorizontal,
+ searchEntries: getRepositoryPaneSearchEntries(repo)
+ }))
+ ],
+ [repos]
+ )
+
+ const visibleNavSections = useMemo(
+ () =>
+ navSections.filter((section) =>
+ matchesSettingsSearch(settingsSearchQuery, section.searchEntries)
+ ),
+ [navSections, settingsSearchQuery]
+ )
+
+ useEffect(() => {
+ const scrollTargetId = pendingScrollTargetRef.current
+ const visibleIds = new Set(visibleNavSections.map((section) => section.id))
+
+ if (scrollTargetId && visibleIds.has(scrollTargetId)) {
+ const target = document.getElementById(scrollTargetId)
+ target?.scrollIntoView({ behavior: 'smooth', block: 'start' })
+ setActiveSectionId(scrollTargetId)
+ pendingScrollTargetRef.current = null
+ return
+ }
+
+ if (scrollTargetId && settingsSearchQuery.trim() !== '') {
+ // Why: keep the ref set so the *next* effect cycle (after the search clears and
+ // sections become visible) can scroll to the target via the branch above.
+ // The loop concern is mitigated because once the search clears, the target becomes
+ // visible, the branch above consumes and clears the ref, and the cycle stops.
+ setSettingsSearchQuery('')
+ return
+ }
+
+ if (!visibleIds.has(activeSectionId) && visibleNavSections.length > 0) {
+ setActiveSectionId(getFallbackVisibleSection(visibleNavSections)?.id ?? activeSectionId)
+ }
+ }, [activeSectionId, setSettingsSearchQuery, settingsSearchQuery, visibleNavSections])
+
+ useEffect(() => {
+ const container = contentScrollRef.current
+ if (!container) {
+ return
+ }
+
+ const updateActiveSection = (): void => {
+ const sections = Array.from(
+ container.querySelectorAll('[data-settings-section]')
+ )
+ if (sections.length === 0) {
+ return
+ }
+
+ const containerTop = container.getBoundingClientRect().top
+ const candidate =
+ sections.find((section) => section.getBoundingClientRect().top - containerTop >= -24) ??
+ sections.at(-1)
+ if (!candidate) {
+ return
+ }
+ setActiveSectionId(candidate.dataset.settingsSection ?? candidate.id)
+ }
+
+ // Why: the scroll handler runs querySelectorAll + getBoundingClientRect for every
+ // section on each scroll event (60+ fps). Wrapping it in a requestAnimationFrame
+ // throttle limits it to once per frame, avoiding layout-thrashing jank.
+ let rafId: number | null = null
+ const throttledUpdateActiveSection = (): void => {
+ if (rafId !== null) {
+ return
+ }
+ rafId = requestAnimationFrame(() => {
+ rafId = null
+ updateActiveSection()
+ })
+ }
+
+ updateActiveSection()
+ container.addEventListener('scroll', throttledUpdateActiveSection, { passive: true })
+ return () => {
+ container.removeEventListener('scroll', throttledUpdateActiveSection)
+ if (rafId !== null) {
+ cancelAnimationFrame(rafId)
+ }
+ }
+ }, [visibleNavSections])
+
+ const scrollToSection = useCallback((sectionId: string) => {
+ const target = document.getElementById(sectionId)
+ if (!target) {
+ return
+ }
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' })
+ setActiveSectionId(sectionId)
+ }, [])
if (!settings) {
return (
@@ -181,192 +327,122 @@ function Settings(): React.JSX.Element {
)
}
- const contentClassName = 'w-full max-w-5xl px-8'
- const pageHeader = showGeneralPane ? (
-
-
General
-
Workspace, editor, naming, and updates.
-
- ) : showAppearancePane ? (
-
-
Appearance
-
Theme and UI scaling.
-
- ) : showTerminalPane ? (
-
-
Terminal
-
- Terminal appearance, previews, and defaults for new panes.
-
-
- ) : showShortcutsPane ? (
-
-
Shortcuts
-
Keyboard shortcuts for common actions.
-
- ) : selectedRepo ? (
-
-
-
-
{selectedRepo.displayName}
-
-
{selectedRepo.path}
-
- ) : (
-
-
Repository Settings
-
Select a repository to edit its settings.
-
- )
+ const generalNavSections = visibleNavSections.filter((section) => !section.id.startsWith('repo-'))
+ const repoNavSections = visibleNavSections
+ .filter((section) => section.id.startsWith('repo-'))
+ .map((section) => {
+ const repo = repos.find((entry) => entry.id === section.id.replace('repo-', ''))
+ return { ...section, badgeColor: repo?.badgeColor }
+ })
return (
-
-
-
setActiveView('terminal')}
- className="w-full justify-start gap-2 text-muted-foreground"
- >
-
- Back to app
-
-
-
-
-
-
-
setSelectedPane('general')}
- className={`flex w-full items-center rounded-lg px-3 py-2 text-left text-sm transition-colors ${
- showGeneralPane
- ? 'bg-accent font-medium text-accent-foreground'
- : 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
- }`}
- >
-
- General
-
-
setSelectedPane('appearance')}
- className={`flex w-full items-center rounded-lg px-3 py-2 text-left text-sm transition-colors ${
- showAppearancePane
- ? 'bg-accent font-medium text-accent-foreground'
- : 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
- }`}
- >
-
- Appearance
-
-
setSelectedPane('terminal')}
- className={`flex w-full items-center rounded-lg px-3 py-2 text-left text-sm transition-colors ${
- showTerminalPane
- ? 'bg-accent font-medium text-accent-foreground'
- : 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
- }`}
- >
-
- Terminal
-
-
setSelectedPane('shortcuts')}
- className={`flex w-full items-center rounded-lg px-3 py-2 text-left text-sm transition-colors ${
- showShortcutsPane
- ? 'bg-accent font-medium text-accent-foreground'
- : 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
- }`}
- >
-
- Shortcuts
-
-
-
-
-
- Repositories
-
-
- {repos.length === 0 ? (
-
No repositories added yet.
- ) : (
-
- {repos.map((repo) => (
- {
- setSelectedRepoId(repo.id)
- setSelectedPane('repo')
- }}
- className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm transition-colors ${
- showRepoPane && selectedRepoId === repo.id
- ? 'bg-accent font-medium text-accent-foreground'
- : 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
- }`}
- >
-
- {repo.displayName}
-
- ))}
-
- )}
-
-
-
-
+
0}
+ searchQuery={settingsSearchQuery}
+ onBack={() => setActiveView('terminal')}
+ onSearchChange={setSettingsSearchQuery}
+ onSelectSection={scrollToSection}
+ />
-
-
{pageHeader}
+
+
+
Settings
+
+ Search across every settings section without leaving the page.
+
+
-
-
- {showGeneralPane ? (
-
- ) : showAppearancePane ? (
-
- ) : showTerminalPane ? (
-
- ) : showShortcutsPane ? (
-
- ) : selectedRepo ? (
-
- ) : (
-
- Select a repository to edit its settings.
+
+
+ {visibleNavSections.length === 0 ? (
+
+ No settings found for "{settingsSearchQuery.trim()}"
+ ) : (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {repos.map((repo) => {
+ const repoSectionId = `repo-${repo.id}`
+ const repoHooksState = repoHooksMap[repo.id]
+
+ return (
+
+
+
+ )
+ })}
+ >
)}
-
+
)
diff --git a/src/renderer/src/components/settings/SettingsSection.tsx b/src/renderer/src/components/settings/SettingsSection.tsx
new file mode 100644
index 000000000..ca85978aa
--- /dev/null
+++ b/src/renderer/src/components/settings/SettingsSection.tsx
@@ -0,0 +1,36 @@
+import type React from 'react'
+import { useAppStore } from '../../store'
+import { matchesSettingsSearch, type SettingsSearchEntry } from './settings-search'
+
+type SettingsSectionProps = {
+ id: string
+ title: string
+ description: string
+ searchEntries: SettingsSearchEntry[]
+ children: React.ReactNode
+ className?: string
+}
+
+export function SettingsSection({
+ id,
+ title,
+ description,
+ searchEntries,
+ children,
+ className
+}: SettingsSectionProps): React.JSX.Element | null {
+ const query = useAppStore((state) => state.settingsSearchQuery)
+ if (!matchesSettingsSearch(query, searchEntries)) {
+ return null
+ }
+
+ return (
+
+
+
{title}
+
{description}
+
+ {children}
+
+ )
+}
diff --git a/src/renderer/src/components/settings/SettingsSidebar.tsx b/src/renderer/src/components/settings/SettingsSidebar.tsx
new file mode 100644
index 000000000..3f6399fa5
--- /dev/null
+++ b/src/renderer/src/components/settings/SettingsSidebar.tsx
@@ -0,0 +1,125 @@
+import { ArrowLeft, Search, type LucideIcon, type LucideProps } from 'lucide-react'
+import { Button } from '../ui/button'
+import { Input } from '../ui/input'
+
+type NavSection = {
+ id: string
+ title: string
+ icon: LucideIcon | ((props: LucideProps) => React.JSX.Element)
+}
+
+type RepoNavSection = NavSection & {
+ badgeColor?: string
+}
+
+type SettingsSidebarProps = {
+ activeSectionId: string
+ generalSections: NavSection[]
+ repoSections: RepoNavSection[]
+ hasRepos: boolean
+ searchQuery: string
+ onBack: () => void
+ onSearchChange: (query: string) => void
+ onSelectSection: (sectionId: string) => void
+}
+
+export function SettingsSidebar({
+ activeSectionId,
+ generalSections,
+ repoSections,
+ hasRepos,
+ searchQuery,
+ onBack,
+ onSearchChange,
+ onSelectSection
+}: SettingsSidebarProps): React.JSX.Element {
+ return (
+
+ )
+}
diff --git a/src/renderer/src/components/settings/ShortcutsPane.tsx b/src/renderer/src/components/settings/ShortcutsPane.tsx
index 5e11cf1fb..fffedc9fe 100644
--- a/src/renderer/src/components/settings/ShortcutsPane.tsx
+++ b/src/renderer/src/components/settings/ShortcutsPane.tsx
@@ -1,4 +1,7 @@
import React, { useMemo } from 'react'
+import { useAppStore } from '../../store'
+import { SearchableSetting } from './SearchableSetting'
+import { matchesSettingsSearch, type SettingsSearchEntry } from './settings-search'
type ShortcutItem = {
action: string
@@ -10,52 +13,179 @@ type ShortcutGroup = {
items: ShortcutItem[]
}
+type ShortcutDefinition = {
+ action: string
+ searchKeywords: string[]
+ keys: (labels: { mod: string; shift: string; enter: string }) => string[]
+}
+
+type ShortcutGroupDefinition = {
+ title: string
+ items: ShortcutDefinition[]
+}
+
+const SHORTCUT_GROUP_DEFINITIONS: ShortcutGroupDefinition[] = [
+ {
+ title: 'Global',
+ items: [
+ {
+ action: 'Go to File',
+ searchKeywords: ['shortcut', 'global', 'file'],
+ keys: ({ mod }) => [mod, 'P']
+ },
+ {
+ action: 'Create worktree',
+ searchKeywords: ['shortcut', 'global', 'worktree'],
+ keys: ({ mod }) => [mod, 'N']
+ },
+ {
+ action: 'Toggle Sidebar',
+ searchKeywords: ['shortcut', 'sidebar'],
+ keys: ({ mod }) => [mod, 'B']
+ },
+ {
+ action: 'Move up worktree',
+ searchKeywords: ['shortcut', 'global', 'worktree', 'move'],
+ keys: ({ mod, shift }) => [mod, shift, '↑']
+ },
+ {
+ action: 'Move down worktree',
+ searchKeywords: ['shortcut', 'global', 'worktree', 'move'],
+ keys: ({ mod, shift }) => [mod, shift, '↓']
+ },
+ {
+ action: 'Toggle File Explorer',
+ searchKeywords: ['shortcut', 'file explorer'],
+ keys: ({ mod, shift }) => [mod, shift, 'E']
+ },
+ {
+ action: 'Toggle Search',
+ searchKeywords: ['shortcut', 'search'],
+ keys: ({ mod, shift }) => [mod, shift, 'F']
+ },
+ {
+ action: 'Toggle Source Control',
+ searchKeywords: ['shortcut', 'source control'],
+ keys: ({ mod, shift }) => [mod, shift, 'G']
+ }
+ ]
+ },
+ {
+ title: 'Terminal Tabs',
+ items: [
+ {
+ action: 'New tab',
+ searchKeywords: ['shortcut', 'tab'],
+ keys: ({ mod }) => [mod, 'T']
+ },
+ {
+ action: 'Close active tab / pane',
+ searchKeywords: ['shortcut', 'close', 'tab', 'pane'],
+ keys: ({ mod }) => [mod, 'W']
+ },
+ {
+ action: 'Next tab',
+ searchKeywords: ['shortcut', 'tab', 'next'],
+ keys: ({ mod, shift }) => [mod, shift, ']']
+ },
+ {
+ action: 'Previous tab',
+ searchKeywords: ['shortcut', 'tab', 'previous'],
+ keys: ({ mod, shift }) => [mod, shift, '[']
+ }
+ ]
+ },
+ {
+ title: 'Terminal Panes',
+ items: [
+ {
+ action: 'Split pane right',
+ searchKeywords: ['shortcut', 'pane', 'split'],
+ keys: ({ mod }) => [mod, 'D']
+ },
+ {
+ action: 'Split pane down',
+ searchKeywords: ['shortcut', 'pane', 'split'],
+ keys: ({ mod, shift }) => [mod, shift, 'D']
+ },
+ {
+ action: 'Close pane (EOF)',
+ searchKeywords: ['shortcut', 'pane', 'close', 'eof'],
+ keys: () => ['Ctrl', 'D']
+ },
+ {
+ action: 'Focus next pane',
+ searchKeywords: ['shortcut', 'pane', 'focus', 'next'],
+ keys: ({ mod }) => [mod, ']']
+ },
+ {
+ action: 'Focus previous pane',
+ searchKeywords: ['shortcut', 'pane', 'focus', 'previous'],
+ keys: ({ mod }) => [mod, '[']
+ },
+ {
+ action: 'Clear active pane',
+ searchKeywords: ['shortcut', 'pane', 'clear'],
+ keys: ({ mod }) => [mod, 'K']
+ },
+ {
+ action: 'Expand / collapse pane',
+ searchKeywords: ['shortcut', 'pane', 'expand', 'collapse'],
+ keys: ({ mod, shift, enter }) => [mod, shift, enter]
+ }
+ ]
+ }
+]
+
+// Why: search is supposed to stay in lockstep with the rendered shortcuts. Deriving
+// both from one definition prevents the registry drift regression this branch introduced.
+export const SHORTCUTS_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] =
+ SHORTCUT_GROUP_DEFINITIONS.flatMap((group) =>
+ group.items.map((item) => ({
+ title: item.action,
+ description: `${group.title} shortcut`,
+ keywords: item.searchKeywords
+ }))
+ )
+
export function ShortcutsPane(): React.JSX.Element {
+ const searchQuery = useAppStore((state) => state.settingsSearchQuery)
const isMac = navigator.userAgent.includes('Mac')
const mod = isMac ? '⌘' : 'Ctrl'
const shift = isMac ? '⇧' : 'Shift'
const enter = isMac ? '↵' : 'Enter'
const groups = useMemo(
- () => [
- {
- title: 'Global',
- items: [
- { action: 'Go to File', keys: [mod, 'P'] },
- { action: 'Create worktree', keys: [mod, 'N'] },
- { action: 'Toggle Sidebar', keys: [mod, 'B'] },
- { action: 'Move up worktree', keys: [mod, shift, '↑'] },
- { action: 'Move down worktree', keys: [mod, shift, '↓'] },
- { action: 'Toggle File Explorer', keys: [mod, shift, 'E'] },
- { action: 'Toggle Search', keys: [mod, shift, 'F'] },
- { action: 'Toggle Source Control', keys: [mod, shift, 'G'] }
- ]
- },
- {
- title: 'Terminal Tabs',
- items: [
- { action: 'New tab', keys: [mod, 'T'] },
- { action: 'Close active tab / pane', keys: [mod, 'W'] },
- { action: 'Next tab', keys: [mod, shift, ']'] },
- { action: 'Previous tab', keys: [mod, shift, '['] }
- ]
- },
- {
- title: 'Terminal Panes',
- items: [
- { action: 'Split pane right', keys: [mod, 'D'] },
- { action: 'Split pane down', keys: [mod, shift, 'D'] },
- { action: 'Close pane (EOF)', keys: ['Ctrl', 'D'] },
- { action: 'Focus next pane', keys: [mod, ']'] },
- { action: 'Focus previous pane', keys: [mod, '['] },
- { action: 'Clear active pane', keys: [mod, 'K'] },
- { action: 'Expand / collapse pane', keys: [mod, shift, enter] }
- ]
- }
- ],
+ () =>
+ SHORTCUT_GROUP_DEFINITIONS.map((group) => ({
+ title: group.title,
+ items: group.items.map((item) => ({
+ action: item.action,
+ keys: item.keys({ mod, shift, enter })
+ }))
+ })),
[mod, shift, enter]
)
+ // Why: keywords here must match the ones used by SHORTCUTS_PANE_SEARCH_ENTRIES
+ // (which uses searchKeywords from SHORTCUT_GROUP_DEFINITIONS). Using item.keys
+ // (rendered key labels like ['Cmd', 'P']) would cause a mismatch where sidebar-level
+ // search finds a shortcut but the inner SearchableSetting hides it.
+ const groupEntries = useMemo>(
+ () =>
+ Object.fromEntries(
+ SHORTCUT_GROUP_DEFINITIONS.map((groupDef) => [
+ groupDef.title,
+ groupDef.items.map((defItem) => ({
+ title: defItem.action,
+ description: `${groupDef.title} shortcut`,
+ keywords: defItem.searchKeywords
+ }))
+ ])
+ ),
+ []
+ )
+
return (
@@ -68,32 +198,48 @@ export function ShortcutsPane(): React.JSX.Element {
- {groups.map((group) => (
-
-
- {group.title}
-
-
- {group.items.map((item, idx) => (
-
-
{item.action}
-
- {item.keys.map((key, kIdx) => (
-
-
- {key}
-
- {!isMac && kIdx < item.keys.length - 1 && (
- +
- )}
-
- ))}
-
-
- ))}
+ {groups
+ .filter((group) => matchesSettingsSearch(searchQuery, groupEntries[group.title] ?? []))
+ .map((group) => (
+
+
+ {group.title}
+
+
+ {group.items.map((item, idx) => {
+ // Why: look up the definition's searchKeywords so the inner
+ // SearchableSetting matches the same terms as the sidebar search.
+ const defGroup = SHORTCUT_GROUP_DEFINITIONS.find((g) => g.title === group.title)
+ const defItem = defGroup?.items.find((d) => d.action === item.action)
+ const keywords = defItem?.searchKeywords ?? item.keys
+
+ return (
+
+ {item.action}
+
+ {item.keys.map((key, kIdx) => (
+
+
+ {key}
+
+ {!isMac && kIdx < item.keys.length - 1 ? (
+ +
+ ) : null}
+
+ ))}
+
+
+ )
+ })}
+
-
- ))}
+ ))}
diff --git a/src/renderer/src/components/settings/TerminalPane.tsx b/src/renderer/src/components/settings/TerminalPane.tsx
index 57c0a7331..6d603ebe6 100644
--- a/src/renderer/src/components/settings/TerminalPane.tsx
+++ b/src/renderer/src/components/settings/TerminalPane.tsx
@@ -12,15 +12,29 @@ import { Input } from '../ui/input'
import { Label } from '../ui/label'
import { Separator } from '../ui/separator'
import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group'
-import { TerminalThemePreview } from './TerminalThemePreview'
import { Minus, Plus } from 'lucide-react'
import {
clampNumber,
resolveEffectiveTerminalAppearance,
resolvePaneStyleOptions
} from '@/lib/terminal-theme'
-import { ThemePicker, ColorField, NumberField, FontAutocomplete } from './SettingsFormControls'
+import { NumberField, FontAutocomplete } from './SettingsFormControls'
import { SCROLLBACK_PRESETS_MB } from './SettingsConstants'
+import { SearchableSetting } from './SearchableSetting'
+import { matchesSettingsSearch } from './settings-search'
+import { useAppStore } from '../../store'
+import {
+ TERMINAL_ADVANCED_SEARCH_ENTRIES,
+ TERMINAL_CURSOR_SEARCH_ENTRIES,
+ TERMINAL_DARK_THEME_SEARCH_ENTRIES,
+ TERMINAL_LIGHT_THEME_SEARCH_ENTRIES,
+ TERMINAL_PANE_SEARCH_ENTRIES,
+ TERMINAL_PANE_STYLE_SEARCH_ENTRIES,
+ TERMINAL_TYPOGRAPHY_SEARCH_ENTRIES
+} from './terminal-search'
+import { DarkTerminalThemeSection, LightTerminalThemeSection } from './TerminalThemeSections'
+
+export { TERMINAL_PANE_SEARCH_ENTRIES }
type TerminalPaneProps = {
settings: GlobalSettings
@@ -39,6 +53,7 @@ export function TerminalPane({
scrollbackMode,
setScrollbackMode
}: TerminalPaneProps): React.JSX.Element {
+ const searchQuery = useAppStore((state) => state.settingsSearchQuery)
const [themeSearchDark, setThemeSearchDark] = useState('')
const [themeSearchLight, setThemeSearchLight] = useState('')
@@ -58,17 +73,22 @@ export function TerminalPane({
const scrollbackToggleValue =
scrollbackMode === 'custom' ? 'custom' : isPreset ? `${scrollbackMb}` : 'custom'
- return (
-
-
+ const visibleSections = [
+ matchesSettingsSearch(searchQuery, TERMINAL_TYPOGRAPHY_SEARCH_ENTRIES) ? (
+
-
Typography
+
Typography
Default terminal typography for new panes and live updates.
-
+
-
+
Font Family
updateSettings({ terminalFontFamily: value })}
/>
-
+
-
- updateSettings({
- terminalFontWeight: normalizeTerminalFontWeight(value)
- })
- }
- />
+ keywords={['terminal', 'typography', 'weight']}
+ >
+
+ updateSettings({
+ terminalFontWeight: normalizeTerminalFontWeight(value)
+ })
+ }
+ />
+
-
-
-
-
+ ) : null,
+ matchesSettingsSearch(searchQuery, TERMINAL_CURSOR_SEARCH_ENTRIES) ? (
+
-
Cursor
+
Cursor
Default cursor appearance for Orca terminal panes.
-
+
Cursor Shape
{(['bar', 'block', 'underline'] as const).map((option) => (
@@ -164,9 +199,14 @@ export function TerminalPane({
))}
-
+
-
+
Blinking Cursor
@@ -191,172 +231,103 @@ export function TerminalPane({
}`}
/>
-
+
-
-
-
-
+ ) : null,
+ matchesSettingsSearch(searchQuery, TERMINAL_PANE_STYLE_SEARCH_ENTRIES) ? (
+
-
Pane Styling
+
Pane Styling
Control inactive pane dimming, divider thickness, and transition timing.
-
- updateSettings({
- terminalInactivePaneOpacity: clampNumber(value, 0, 1)
- })
- }
- />
-
- updateSettings({
- terminalDividerThicknessPx: clampNumber(value, 1, 32)
- })
- }
- />
-
-
-
-
-
-
-
- updateSettings({ terminalThemeDark: theme })}
- />
-
- updateSettings({ terminalDividerColorDark: value })}
- />
-
-
-
-
-
-
-
-
-
-
-
Use Separate Theme In Light Mode
-
- When disabled, light mode reuses the dark terminal theme.
-
-
-
- updateSettings({
- terminalUseSeparateLightTheme: !settings.terminalUseSeparateLightTheme
- })
- }
- className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
- settings.terminalUseSeparateLightTheme ? 'bg-foreground' : 'bg-muted-foreground/30'
- }`}
+ keywords={['pane', 'opacity', 'dimming']}
>
-
+ updateSettings({
+ terminalInactivePaneOpacity: clampNumber(value, 0, 1)
+ })
+ }
/>
-
-
-
-
-
-
-
- updateSettings({ terminalThemeLight: theme })}
- />
-
- updateSettings({ terminalDividerColorLight: value })}
- />
-
-
-
-
-
+
+
+
+ updateSettings({
+ terminalDividerThicknessPx: clampNumber(value, 1, 32)
+ })
+ }
+ />
+
-
-
-
-
+ ) : null,
+ matchesSettingsSearch(searchQuery, TERMINAL_DARK_THEME_SEARCH_ENTRIES) ? (
+
+ ) : null,
+ matchesSettingsSearch(searchQuery, TERMINAL_LIGHT_THEME_SEARCH_ENTRIES) ? (
+
+ ) : null,
+ matchesSettingsSearch(searchQuery, TERMINAL_ADVANCED_SEARCH_ENTRIES) ? (
+
-
Advanced
+
Advanced
Scrollback is bounded for stability. This setting applies to new terminal panes.
-
+
Scrollback Size
) : null}
-
+
+ ) : null
+ ].filter(Boolean)
+
+ return (
+
+ {visibleSections.map((section, index) => (
+
+ {index > 0 ? : null}
+ {section}
+
+ ))}
)
}
diff --git a/src/renderer/src/components/settings/TerminalThemeSections.tsx b/src/renderer/src/components/settings/TerminalThemeSections.tsx
new file mode 100644
index 000000000..6e75e6a9e
--- /dev/null
+++ b/src/renderer/src/components/settings/TerminalThemeSections.tsx
@@ -0,0 +1,200 @@
+import type { Dispatch, SetStateAction } from 'react'
+import type { GlobalSettings } from '../../../../shared/types'
+import type { EffectiveTerminalAppearance } from '@/lib/terminal-theme'
+import { ColorField, ThemePicker } from './SettingsFormControls'
+import { SearchableSetting } from './SearchableSetting'
+import { TerminalThemePreview } from './TerminalThemePreview'
+
+type ThemePreviewProps = {
+ dividerThicknessPx: number
+ inactivePaneOpacity: number
+ activePaneOpacity: number
+}
+
+type DarkTerminalThemeSectionProps = {
+ settings: GlobalSettings
+ systemPrefersDark: boolean
+ themeSearchDark: string
+ setThemeSearchDark: Dispatch>
+ updateSettings: (updates: Partial) => void
+ previewProps: ThemePreviewProps
+ darkPreviewAppearance: EffectiveTerminalAppearance
+}
+
+type LightTerminalThemeSectionProps = {
+ settings: GlobalSettings
+ themeSearchLight: string
+ setThemeSearchLight: Dispatch>
+ updateSettings: (updates: Partial) => void
+ previewProps: ThemePreviewProps
+ lightPreviewAppearance: EffectiveTerminalAppearance
+}
+
+export function DarkTerminalThemeSection({
+ settings,
+ systemPrefersDark,
+ themeSearchDark,
+ setThemeSearchDark,
+ updateSettings,
+ previewProps,
+ darkPreviewAppearance
+}: DarkTerminalThemeSectionProps): React.JSX.Element {
+ return (
+
+
+
+
Dark Theme
+
+ Choose the theme used for terminal panes in dark mode.
+
+
+
+
+ updateSettings({ terminalThemeDark: theme })}
+ />
+
+
+
+ updateSettings({ terminalDividerColorDark: value })}
+ />
+
+
+
+
+
+ )
+}
+
+export function LightTerminalThemeSection({
+ settings,
+ themeSearchLight,
+ setThemeSearchLight,
+ updateSettings,
+ previewProps,
+ lightPreviewAppearance
+}: LightTerminalThemeSectionProps): React.JSX.Element {
+ return (
+
+
+
+
Use Separate Theme In Light Mode
+
+ When disabled, light mode reuses the dark terminal theme.
+
+
+
+ updateSettings({
+ terminalUseSeparateLightTheme: !settings.terminalUseSeparateLightTheme
+ })
+ }
+ className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
+ settings.terminalUseSeparateLightTheme ? 'bg-foreground' : 'bg-muted-foreground/30'
+ }`}
+ >
+
+
+
+
+
+
+
+
+
+
Light Theme
+
+ Configure the optional light-mode terminal appearance.
+
+
+
+
+ updateSettings({ terminalThemeLight: theme })}
+ />
+
+
+
+ updateSettings({ terminalDividerColorLight: value })}
+ />
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/renderer/src/components/settings/general-search.ts b/src/renderer/src/components/settings/general-search.ts
new file mode 100644
index 000000000..fc17b4d5f
--- /dev/null
+++ b/src/renderer/src/components/settings/general-search.ts
@@ -0,0 +1,64 @@
+import type { SettingsSearchEntry } from './settings-search'
+
+export const GENERAL_WORKSPACE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
+ {
+ title: 'Workspace Directory',
+ description: 'Root directory where worktree folders are created.',
+ keywords: ['workspace', 'folder', 'path', 'worktree']
+ },
+ {
+ title: 'Nest Workspaces',
+ description: 'Create worktrees inside a repo-named subfolder.',
+ keywords: ['nested', 'subfolder', 'directory']
+ }
+]
+
+export const GENERAL_EDITOR_SEARCH_ENTRIES: SettingsSearchEntry[] = [
+ {
+ title: 'Auto Save Files',
+ description: 'Save editor and editable diff changes automatically after a short pause.',
+ keywords: ['autosave', 'save']
+ },
+ {
+ title: 'Auto Save Delay',
+ description: 'How long Orca waits after your last edit before saving automatically.',
+ keywords: ['autosave', 'delay', 'milliseconds']
+ }
+]
+
+export const GENERAL_CLI_SEARCH_ENTRIES: SettingsSearchEntry[] = [
+ {
+ title: 'Shell command',
+ description: 'Register or remove the orca shell command.',
+ keywords: ['cli', 'path', 'terminal', 'command']
+ },
+ {
+ title: 'Agent skill',
+ description: 'Install the Orca skill so agents know to use the orca CLI.',
+ keywords: ['skill', 'agents', 'npx']
+ }
+]
+
+export const GENERAL_BRANCH_SEARCH_ENTRIES: SettingsSearchEntry[] = [
+ {
+ title: 'Branch Prefix',
+ description: 'Prefix added to branch names when creating worktrees.',
+ keywords: ['branch naming', 'git username', 'custom']
+ }
+]
+
+export const GENERAL_UPDATE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
+ {
+ title: 'Check for Updates',
+ description: 'Check for app updates and install a newer Orca version.',
+ keywords: ['update', 'version', 'release notes', 'download']
+ }
+]
+
+export const GENERAL_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
+ ...GENERAL_WORKSPACE_SEARCH_ENTRIES,
+ ...GENERAL_EDITOR_SEARCH_ENTRIES,
+ ...GENERAL_CLI_SEARCH_ENTRIES,
+ ...GENERAL_BRANCH_SEARCH_ENTRIES,
+ ...GENERAL_UPDATE_SEARCH_ENTRIES
+]
diff --git a/src/renderer/src/components/settings/settings-search.ts b/src/renderer/src/components/settings/settings-search.ts
new file mode 100644
index 000000000..c72750bf5
--- /dev/null
+++ b/src/renderer/src/components/settings/settings-search.ts
@@ -0,0 +1,25 @@
+export type SettingsSearchEntry = {
+ title: string
+ description?: string
+ keywords?: string[]
+}
+
+export function normalizeSettingsSearchQuery(query: string): string {
+ return query.trim().toLowerCase()
+}
+
+export function matchesSettingsSearch(
+ query: string,
+ entries: SettingsSearchEntry | SettingsSearchEntry[]
+): boolean {
+ const normalizedQuery = normalizeSettingsSearchQuery(query)
+ if (!normalizedQuery) {
+ return true
+ }
+
+ const values = Array.isArray(entries) ? entries : [entries]
+ return values.some((entry) => {
+ const haystack = [entry.title, entry.description ?? '', ...(entry.keywords ?? [])]
+ return haystack.some((value) => value.toLowerCase().includes(normalizedQuery))
+ })
+}
diff --git a/src/renderer/src/components/settings/terminal-search.ts b/src/renderer/src/components/settings/terminal-search.ts
new file mode 100644
index 000000000..409854903
--- /dev/null
+++ b/src/renderer/src/components/settings/terminal-search.ts
@@ -0,0 +1,93 @@
+import type { SettingsSearchEntry } from './settings-search'
+
+export const TERMINAL_TYPOGRAPHY_SEARCH_ENTRIES: SettingsSearchEntry[] = [
+ {
+ title: 'Font Size',
+ description: 'Default terminal font size for new panes and live updates.',
+ keywords: ['terminal', 'typography', 'text size']
+ },
+ {
+ title: 'Font Family',
+ description: 'Default terminal font family for new panes and live updates.',
+ keywords: ['terminal', 'typography', 'font']
+ },
+ {
+ title: 'Font Weight',
+ description: 'Controls the terminal text font weight.',
+ keywords: ['terminal', 'typography', 'weight']
+ }
+]
+
+export const TERMINAL_CURSOR_SEARCH_ENTRIES: SettingsSearchEntry[] = [
+ {
+ title: 'Cursor Shape',
+ description: 'Default cursor appearance for Orca terminal panes.',
+ keywords: ['terminal', 'cursor', 'bar', 'block', 'underline']
+ },
+ {
+ title: 'Blinking Cursor',
+ description: 'Uses the blinking variant of the selected cursor shape.',
+ keywords: ['terminal', 'cursor', 'blink']
+ }
+]
+
+export const TERMINAL_PANE_STYLE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
+ {
+ title: 'Inactive Pane Opacity',
+ description: 'Opacity applied to panes that are not currently active.',
+ keywords: ['pane', 'opacity', 'dimming']
+ },
+ {
+ title: 'Divider Thickness',
+ description: 'Thickness of the pane divider line.',
+ keywords: ['pane', 'divider', 'thickness']
+ }
+]
+
+export const TERMINAL_DARK_THEME_SEARCH_ENTRIES: SettingsSearchEntry[] = [
+ {
+ title: 'Dark Theme',
+ description: 'Choose the terminal theme used in dark mode.',
+ keywords: ['terminal', 'theme', 'dark', 'preview']
+ },
+ {
+ title: 'Dark Divider Color',
+ description: 'Controls the split divider line between panes in dark mode.',
+ keywords: ['terminal', 'divider', 'dark', 'color']
+ }
+]
+
+export const TERMINAL_LIGHT_THEME_SEARCH_ENTRIES: SettingsSearchEntry[] = [
+ {
+ title: 'Use Separate Theme In Light Mode',
+ description: 'When disabled, light mode reuses the dark terminal theme.',
+ keywords: ['terminal', 'light mode', 'theme']
+ },
+ {
+ title: 'Light Theme',
+ description: 'Choose the theme used when Orca is in light mode.',
+ keywords: ['terminal', 'theme', 'light', 'preview']
+ },
+ {
+ title: 'Light Divider Color',
+ description: 'Controls the split divider line between panes in light mode.',
+ keywords: ['terminal', 'divider', 'light', 'color']
+ }
+]
+
+export const TERMINAL_ADVANCED_SEARCH_ENTRIES: SettingsSearchEntry[] = [
+ {
+ title: 'Scrollback Size',
+ description: 'Maximum terminal scrollback buffer size.',
+ keywords: ['terminal', 'scrollback', 'buffer', 'memory']
+ }
+]
+
+export const TERMINAL_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
+ ...TERMINAL_TYPOGRAPHY_SEARCH_ENTRIES,
+ ...TERMINAL_CURSOR_SEARCH_ENTRIES,
+ ...TERMINAL_PANE_STYLE_SEARCH_ENTRIES,
+ ...TERMINAL_DARK_THEME_SEARCH_ENTRIES,
+ ...TERMINAL_LIGHT_THEME_SEARCH_ENTRIES,
+ ...TERMINAL_ADVANCED_SEARCH_ENTRIES
+]
diff --git a/src/renderer/src/store/slices/settings.ts b/src/renderer/src/store/slices/settings.ts
index 31bdc33b5..7e01ca81b 100644
--- a/src/renderer/src/store/slices/settings.ts
+++ b/src/renderer/src/store/slices/settings.ts
@@ -4,12 +4,16 @@ import type { GlobalSettings } from '../../../../shared/types'
export type SettingsSlice = {
settings: GlobalSettings | null
+ settingsSearchQuery: string
+ setSettingsSearchQuery: (q: string) => void
fetchSettings: () => Promise
updateSettings: (updates: Partial) => Promise
}
export const createSettingsSlice: StateCreator = (set) => ({
settings: null,
+ settingsSearchQuery: '',
+ setSettingsSearchQuery: (q) => set({ settingsSearchQuery: q }),
fetchSettings: async () => {
try {