refactor: reorganize Settings navigation order and labels (#3988)

* refactor: reorganize Settings navigation order and labels

Rename the "Safety" group to "Privacy & Security" (the conventional term
for its permissions + telemetry contents), move Floating Workspace to the
end of Workflows so it sits with the Terminal/Browser surfaces it's built
from, and de-scramble the section registry so it reads in group order.

The registry array now mirrors SETTINGS_NAV_GROUPS top-to-bottom, which
makes the Cmd+J palette list settings in the same grouped order as the
sidebar instead of a jumbled sequence.

* refactor: move terminal visuals into appearance settings

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
This commit is contained in:
Trevin Chow 2026-05-30 23:22:46 -07:00 committed by GitHub
parent dd1b28ba2e
commit b7ea489456
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 579 additions and 522 deletions

View File

@ -29,6 +29,9 @@ import {
TYPOGRAPHY_ENTRIES,
ZOOM_ENTRIES
} from './appearance-search'
import { TERMINAL_APPEARANCE_SEARCH_ENTRIES } from './terminal-search'
import { TerminalAppearanceSection } from './TerminalAppearanceSection'
import type { UseGhosttyImportReturn } from './useGhosttyImport'
export { APPEARANCE_PANE_SEARCH_ENTRIES }
type AppearancePaneProps = {
@ -36,6 +39,9 @@ type AppearancePaneProps = {
updateSettings: (updates: Partial<GlobalSettings>) => void
applyTheme: (theme: 'system' | 'dark' | 'light') => void
fontSuggestions: string[]
terminalFontSuggestions: string[]
systemPrefersDark: boolean
ghostty: UseGhosttyImportReturn
}
function ShortcutHintList({ combos }: { combos: string[][] }): React.JSX.Element {
@ -61,7 +67,10 @@ export function AppearancePane({
settings,
updateSettings,
applyTheme,
fontSuggestions
fontSuggestions,
terminalFontSuggestions,
systemPrefersDark,
ghostty
}: AppearancePaneProps): React.JSX.Element {
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
const zoomInKeyCombos = useShortcutKeyCombos('zoom.in')
@ -149,6 +158,16 @@ export function AppearancePane({
) : null}
</section>
) : null,
matchesSettingsSearch(searchQuery, TERMINAL_APPEARANCE_SEARCH_ENTRIES) ? (
<TerminalAppearanceSection
key="terminal-appearance"
settings={settings}
updateSettings={updateSettings}
systemPrefersDark={systemPrefersDark}
terminalFontSuggestions={terminalFontSuggestions}
ghostty={ghostty}
/>
) : null,
matchesSettingsSearch(searchQuery, LAYOUT_ENTRIES) ? (
<section key="layout" className="space-y-3">
<SettingsSubsectionHeader title="File Explorer" />

View File

@ -24,9 +24,7 @@ import { ShortcutsPane } from './ShortcutsPane'
import { TerminalPane } from './TerminalPane'
import { FloatingWorkspacePane } from './FloatingWorkspacePane'
import { useGhosttyImport } from './useGhosttyImport'
import { Button } from '../ui/button'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
import ghosttyIcon from '../../../../../resources/ghostty.svg'
import { RepositoryPane } from './RepositoryPane'
import { GitPane } from './GitPane'
import { CommitMessageAiPane } from './CommitMessageAiPane'
@ -80,7 +78,7 @@ const SETTINGS_NAV_GROUPS = [
{ id: 'workflows', title: 'Workflows' },
{ id: 'interface', title: 'Interface' },
{ id: 'remote', title: 'Remote Access' },
{ id: 'safety', title: 'Safety' },
{ id: 'security', title: 'Privacy & Security' },
{ id: 'experimental', title: 'Experimental' }
] as const
@ -194,9 +192,8 @@ function Settings(): React.JSX.Element {
// reveals controls that the renderer will intentionally hide.
const [scrollbackMode, setScrollbackMode] = useState<'preset' | 'custom'>('preset')
const [prevScrollbackBytes, setPrevScrollbackBytes] = useState(settings?.terminalScrollbackBytes)
// Why: lifted out of TerminalPane so the Terminal section header can render
// the import trigger as a headerAction. The modal itself still lives inside
// TerminalPane, driven by this shared state.
// Why: Appearance owns terminal visual controls, but the Ghostty import flow
// still needs Settings-level state so the modal survives section remounts.
const ghostty = useGhosttyImport(updateSettings, settings)
const [fontSuggestions, setFontSuggestions] = useState<string[]>(
Array.from(new Set([DEFAULT_APP_FONT_FAMILY, ...getFallbackTerminalFonts()]))
@ -951,45 +948,18 @@ function Settings(): React.JSX.Element {
) : null}
</SettingsSection>
<SettingsSection
id="floating-workspace"
title="Floating Workspace"
description="Global terminal, browser, and markdown tabs."
searchEntries={getSectionSearchEntries('floating-workspace')}
>
{isSectionMounted('floating-workspace') ? (
<FloatingWorkspacePane settings={settings} updateSettings={updateSettings} />
) : null}
</SettingsSection>
<SettingsSection
id="terminal"
title="Terminal"
description="Shells, terminal appearance, and pane behavior."
description="Shells, renderer, sessions, and terminal behavior."
searchEntries={getSectionSearchEntries('terminal')}
headerAction={
<Button
variant="outline"
size="sm"
className="gap-1.5"
onClick={() => void ghostty.handleClick()}
>
<img src={ghosttyIcon} alt="" aria-hidden="true" className="size-4" />
Import from Ghostty
</Button>
}
>
{isSectionMounted('terminal') ? (
<TerminalPane
settings={settings}
updateSettings={updateSettings}
systemPrefersDark={systemPrefersDark}
terminalFontSuggestions={fontSuggestions.filter(
(font) => font !== DEFAULT_APP_FONT_FAMILY
)}
scrollbackMode={scrollbackMode}
setScrollbackMode={setScrollbackMode}
ghostty={ghostty}
wslAvailable={windowsTerminalCapabilities.wslAvailable}
wslDistros={windowsTerminalCapabilities.wslDistros}
wslCapabilitiesLoading={windowsTerminalCapabilities.isLoading}
@ -1031,10 +1001,21 @@ function Settings(): React.JSX.Element {
</SettingsSection>
) : null}
<SettingsSection
id="floating-workspace"
title="Floating Workspace"
description="Global terminal, browser, and markdown tabs."
searchEntries={getSectionSearchEntries('floating-workspace')}
>
{isSectionMounted('floating-workspace') ? (
<FloatingWorkspacePane settings={settings} updateSettings={updateSettings} />
) : null}
</SettingsSection>
<SettingsSection
id="appearance"
title="Appearance"
description="Theme, zoom, app font, sidebars, and status bar."
description="Theme, zoom, app and terminal appearance, sidebars, and status bar."
searchEntries={getSectionSearchEntries('appearance')}
>
{isSectionMounted('appearance') ? (
@ -1043,6 +1024,11 @@ function Settings(): React.JSX.Element {
updateSettings={updateSettings}
applyTheme={applyTheme}
fontSuggestions={fontSuggestions}
terminalFontSuggestions={fontSuggestions.filter(
(font) => font !== DEFAULT_APP_FONT_FAMILY
)}
systemPrefersDark={systemPrefersDark}
ghostty={ghostty}
/>
) : null}
</SettingsSection>

View File

@ -154,7 +154,7 @@ const ghosttyMock = {
handleOpenChange: vi.fn()
}
import { TerminalPane } from './TerminalPane'
import { TerminalAppearanceSection } from './TerminalAppearanceSection'
type ReactElementLike = {
type: unknown
@ -234,40 +234,36 @@ function findGhosttyImportModal(node: unknown): ReactElementLike | null {
return null
}
describe('TerminalPane ghostty import wiring', () => {
describe('TerminalAppearanceSection ghostty import wiring', () => {
beforeEach(() => {
mockStateValues.length = 0
resetMockState()
vi.clearAllMocks()
})
// Why: the Ghostty import trigger button lives on the section header in
// Settings.tsx (headerAction) — not inside TerminalPane. Keep this test
// around so a regression that moves the button back into the pane fails.
it('does not render an Import from Ghostty button inside the pane', () => {
const element = TerminalPane({
it('renders the Import from Ghostty button with terminal appearance controls', () => {
const element = TerminalAppearanceSection({
settings: {} as never,
updateSettings: () => {},
systemPrefersDark: true,
terminalFontSuggestions: [],
scrollbackMode: 'preset',
setScrollbackMode: () => {},
ghostty: ghosttyMock
})
const buttons = findButtons(element)
const importButton = buttons.find((b) => b.text === 'Import from Ghostty')
expect(importButton).toBeUndefined()
expect(importButton).toBeDefined()
importButton?.onClick?.()
expect(ghosttyMock.handleClick).toHaveBeenCalled()
})
it('passes hook state to GhosttyImportModal', () => {
const element = TerminalPane({
const element = TerminalAppearanceSection({
settings: {} as never,
updateSettings: () => {},
systemPrefersDark: true,
terminalFontSuggestions: [],
scrollbackMode: 'preset',
setScrollbackMode: () => {},
ghostty: ghosttyMock
})

View File

@ -0,0 +1,440 @@
/* eslint-disable max-lines -- Why: terminal visual controls stay together under Appearance so
search, previews, and the Ghostty import flow move as one user-facing surface. */
import { useState } from 'react'
import type { GlobalSettings } from '../../../../shared/types'
import {
DEFAULT_TERMINAL_FONT_WEIGHT,
TERMINAL_FONT_WEIGHT_MAX,
TERMINAL_FONT_WEIGHT_MIN,
TERMINAL_FONT_WEIGHT_STEP,
normalizeTerminalFontWeight
} from '../../../../shared/terminal-fonts'
import {
fontFamilyHasKnownLigatures,
resolveTerminalLigaturesEnabled
} from '../../../../shared/terminal-ligatures'
import { Minus, Plus } from 'lucide-react'
import { Button } from '../ui/button'
import { Input } from '../ui/input'
import {
FontAutocomplete,
NumberField,
SettingsRow,
SettingsSegmentedControl,
SettingsSubsectionHeader,
SettingsSwitchRow
} from './SettingsFormControls'
import { SearchableSetting } from './SearchableSetting'
import { matchesSettingsSearch } from './settings-search'
import { useAppStore } from '../../store'
import { clampNumber, resolvePaneStyleOptions } from '@/lib/terminal-theme'
import {
TERMINAL_CURSOR_SEARCH_ENTRIES,
TERMINAL_DARK_THEME_SEARCH_ENTRIES,
TERMINAL_GHOSTTY_IMPORT_SEARCH_ENTRIES,
TERMINAL_LIGHT_THEME_SEARCH_ENTRIES,
TERMINAL_PANE_APPEARANCE_SEARCH_ENTRIES,
TERMINAL_TYPOGRAPHY_SEARCH_ENTRIES,
TERMINAL_WINDOW_SEARCH_ENTRIES
} from './terminal-search'
import { DarkTerminalThemeSection, LightTerminalThemeSection } from './TerminalThemeSections'
import { TerminalWindowSection } from './TerminalWindowSection'
import { TerminalSettingsPreview } from './TerminalSettingsPreview'
import { GhosttyImportModal } from './GhosttyImportModal'
import type { UseGhosttyImportReturn } from './useGhosttyImport'
import ghosttyIcon from '../../../../../resources/ghostty.svg'
type TerminalAppearanceSectionProps = {
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void
systemPrefersDark: boolean
terminalFontSuggestions: string[]
ghostty: UseGhosttyImportReturn
}
export function TerminalAppearanceSection({
settings,
updateSettings,
systemPrefersDark,
terminalFontSuggestions,
ghostty
}: TerminalAppearanceSectionProps): React.JSX.Element {
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
const [themeSearchDark, setThemeSearchDark] = useState('')
const [themeSearchLight, setThemeSearchLight] = useState('')
// Why: hover preview lets the font picker update the sample without committing a setting.
const [previewFontFamily, setPreviewFontFamily] = useState<string | null>(null)
const paneStyleOptions = resolvePaneStyleOptions(settings)
const visibleSections = [
matchesSettingsSearch(searchQuery, TERMINAL_GHOSTTY_IMPORT_SEARCH_ENTRIES) ||
matchesSettingsSearch(searchQuery, TERMINAL_TYPOGRAPHY_SEARCH_ENTRIES) ? (
<section key="typography" className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
<div className="min-w-0 space-y-3">
<div className="flex flex-wrap items-start justify-between gap-3">
<SettingsSubsectionHeader
title="Terminal Typography"
description="Default terminal typography for new panes and live updates."
/>
<Button
variant="outline"
size="sm"
className="gap-1.5"
onClick={() => void ghostty.handleClick()}
>
<img src={ghosttyIcon} alt="" aria-hidden="true" className="size-4" />
Import from Ghostty
</Button>
</div>
<div className="divide-y divide-border/40">
<SearchableSetting
title="Font Size"
description="Default terminal font size for new panes and live updates."
keywords={['terminal', 'typography', 'text size']}
>
<SettingsRow
label="Font Size"
description="Default terminal font size for new panes and live updates."
control={
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon-sm"
onClick={() => {
const next = Math.max(10, settings.terminalFontSize - 1)
updateSettings({ terminalFontSize: next })
}}
disabled={settings.terminalFontSize <= 10}
>
<Minus className="size-3" />
</Button>
<Input
type="number"
min={10}
max={24}
value={settings.terminalFontSize}
onChange={(e) => {
const value = parseInt(e.target.value, 10)
if (!Number.isNaN(value) && value >= 10 && value <= 24) {
updateSettings({ terminalFontSize: value })
}
}}
className="w-14 text-center tabular-nums"
/>
<Button
variant="outline"
size="icon-sm"
onClick={() => {
const next = Math.min(24, settings.terminalFontSize + 1)
updateSettings({ terminalFontSize: next })
}}
disabled={settings.terminalFontSize >= 24}
>
<Plus className="size-3" />
</Button>
<span className="text-xs text-muted-foreground">px</span>
</div>
}
/>
</SearchableSetting>
<SearchableSetting
title="Font Family"
description="Default terminal font family for new panes and live updates."
keywords={['terminal', 'typography', 'font']}
>
<SettingsRow
alignTop
label="Font Family"
description="Default terminal font family for new panes and live updates."
control={
<FontAutocomplete
value={settings.terminalFontFamily}
suggestions={terminalFontSuggestions}
onChange={(value) => updateSettings({ terminalFontFamily: value })}
onPreviewFontFamily={setPreviewFontFamily}
/>
}
/>
</SearchableSetting>
<SearchableSetting
title="Font Weight"
description="Controls the terminal text font weight."
keywords={['terminal', 'typography', 'weight']}
>
<NumberField
label="Font Weight"
description="Controls the terminal text font weight."
value={normalizeTerminalFontWeight(settings.terminalFontWeight)}
defaultValue={DEFAULT_TERMINAL_FONT_WEIGHT}
min={TERMINAL_FONT_WEIGHT_MIN}
max={TERMINAL_FONT_WEIGHT_MAX}
step={TERMINAL_FONT_WEIGHT_STEP}
suffix="100-900"
onChange={(value) =>
updateSettings({
terminalFontWeight: normalizeTerminalFontWeight(value)
})
}
/>
</SearchableSetting>
<SearchableSetting
title="Line Height"
description="Controls the terminal line height multiplier."
keywords={['terminal', 'typography', 'line height', 'spacing']}
>
<NumberField
label="Line Height"
description="Controls the terminal line height multiplier."
value={settings.terminalLineHeight}
defaultValue={1}
min={1}
max={3}
step={0.1}
suffix="1-3"
onChange={(value) =>
updateSettings({
terminalLineHeight: clampNumber(value, 1, 3)
})
}
/>
</SearchableSetting>
<SearchableSetting
title="Font Ligatures"
description='Render programming ligatures (e.g. =>, !=, ===) for fonts that ship them. "Auto" enables ligatures only for known ligature fonts (Fira Code, JetBrains Mono, Cascadia Code, Iosevka, etc.).'
keywords={[
'terminal',
'typography',
'ligatures',
'ligature',
'fira code',
'jetbrains mono',
'cascadia code',
'iosevka',
'calt',
'font features'
]}
>
<SettingsRow
label="Font Ligatures"
description={
settings.terminalLigatures === 'on'
? 'Always on. Fonts without ligatures simply render as-is.'
: settings.terminalLigatures === 'off'
? 'Always off, even for fonts that ship them.'
: fontFamilyHasKnownLigatures(settings.terminalFontFamily)
? `Auto - enabled for "${settings.terminalFontFamily}".`
: `Auto - disabled for "${
settings.terminalFontFamily || 'the current font'
}".`
}
control={
<SettingsSegmentedControl
ariaLabel="Font Ligatures"
value={settings.terminalLigatures ?? 'auto'}
onChange={(option) => updateSettings({ terminalLigatures: option })}
options={[
{ value: 'auto', label: 'Auto' },
{ value: 'on', label: 'On' },
{ value: 'off', label: 'Off' }
]}
/>
}
/>
{/* Why: surface the resolved state explicitly so the "Auto" label
isn't ambiguous when a user is staring at it. */}
<p className="sr-only" aria-live="polite">
Ligatures are currently{' '}
{resolveTerminalLigaturesEnabled(
settings.terminalLigatures,
settings.terminalFontFamily
)
? 'enabled'
: 'disabled'}
.
</p>
</SearchableSetting>
</div>
</div>
<TerminalSettingsPreview
title="Preview"
settings={settings}
systemPrefersDark={systemPrefersDark}
previewFontFamily={previewFontFamily}
showThemeToggle
/>
</section>
) : null,
matchesSettingsSearch(searchQuery, TERMINAL_CURSOR_SEARCH_ENTRIES) ? (
<section key="cursor" className="space-y-3">
<SettingsSubsectionHeader
title="Terminal Cursor"
description="Default cursor appearance for Orca terminal panes."
/>
<div className="divide-y divide-border/40">
<SearchableSetting
title="Cursor Shape"
description="Default cursor appearance for Orca terminal panes."
keywords={['terminal', 'cursor', 'bar', 'block', 'underline']}
>
<SettingsRow
label="Cursor Shape"
description="Default cursor appearance for Orca terminal panes."
control={
<SettingsSegmentedControl
ariaLabel="Cursor Shape"
value={settings.terminalCursorStyle}
onChange={(option) => updateSettings({ terminalCursorStyle: option })}
options={[
{ value: 'bar', label: 'Bar' },
{ value: 'block', label: 'Block' },
{ value: 'underline', label: 'Underline' }
]}
/>
}
/>
</SearchableSetting>
<SearchableSetting
title="Blinking Cursor"
description="Uses the blinking variant of the selected cursor shape."
keywords={['terminal', 'cursor', 'blink']}
>
<SettingsSwitchRow
label="Blinking Cursor"
description="Uses the blinking variant of the selected cursor shape."
checked={settings.terminalCursorBlink}
onChange={() =>
updateSettings({ terminalCursorBlink: !settings.terminalCursorBlink })
}
/>
</SearchableSetting>
<SearchableSetting
title="Cursor Opacity"
description="Opacity of the terminal cursor."
keywords={['terminal', 'cursor', 'opacity', 'transparency']}
>
<NumberField
label="Cursor Opacity"
description="Opacity of the terminal cursor."
value={settings.terminalCursorOpacity ?? 1}
defaultValue={1}
min={0}
max={1}
step={0.05}
suffix="0-1"
onChange={(value) =>
updateSettings({
terminalCursorOpacity: clampNumber(value, 0, 1)
})
}
/>
</SearchableSetting>
</div>
</section>
) : null,
matchesSettingsSearch(searchQuery, TERMINAL_PANE_APPEARANCE_SEARCH_ENTRIES) ? (
<section key="pane-appearance" className="space-y-3">
<SettingsSubsectionHeader
title="Terminal Panes"
description="Control inactive pane dimming and split divider thickness."
/>
<div className="divide-y divide-border/40">
<SearchableSetting
title="Inactive Pane Opacity"
description="Opacity applied to panes that are not currently active."
keywords={['pane', 'opacity', 'dimming']}
>
<NumberField
label="Inactive Pane Opacity"
description="Opacity applied to panes that are not currently active."
value={paneStyleOptions.inactivePaneOpacity}
defaultValue={0.8}
min={0}
max={1}
step={0.05}
suffix="0-1"
onChange={(value) =>
updateSettings({
terminalInactivePaneOpacity: clampNumber(value, 0, 1)
})
}
/>
</SearchableSetting>
<SearchableSetting
title="Divider Thickness"
description="Thickness of the pane divider line."
keywords={['pane', 'divider', 'thickness']}
>
<NumberField
label="Divider Thickness"
description="Thickness of the pane divider line."
value={paneStyleOptions.dividerThicknessPx}
defaultValue={1}
min={1}
max={32}
step={1}
suffix="px"
onChange={(value) =>
updateSettings({
terminalDividerThicknessPx: clampNumber(value, 1, 32)
})
}
/>
</SearchableSetting>
</div>
</section>
) : null,
matchesSettingsSearch(searchQuery, TERMINAL_WINDOW_SEARCH_ENTRIES) ? (
<TerminalWindowSection key="window" settings={settings} updateSettings={updateSettings} />
) : null,
matchesSettingsSearch(searchQuery, TERMINAL_DARK_THEME_SEARCH_ENTRIES) ? (
<DarkTerminalThemeSection
key="dark-theme"
settings={settings}
systemPrefersDark={systemPrefersDark}
themeSearchDark={themeSearchDark}
setThemeSearchDark={setThemeSearchDark}
updateSettings={updateSettings}
previewFontFamily={previewFontFamily}
/>
) : null,
matchesSettingsSearch(searchQuery, TERMINAL_LIGHT_THEME_SEARCH_ENTRIES) ? (
<LightTerminalThemeSection
key="light-theme"
settings={settings}
themeSearchLight={themeSearchLight}
setThemeSearchLight={setThemeSearchLight}
updateSettings={updateSettings}
previewFontFamily={previewFontFamily}
/>
) : null
].filter(Boolean)
return (
<div className="space-y-6">
{visibleSections.map((section, index) => (
<div key={index} className="space-y-6">
{index > 0 ? <div className="h-px bg-border/60" /> : null}
{section}
</div>
))}
<GhosttyImportModal
open={ghostty.open}
onOpenChange={ghostty.handleOpenChange}
preview={ghostty.preview}
loading={ghostty.loading}
onApply={ghostty.handleApply}
applied={ghostty.applied}
applyError={ghostty.applyError}
/>
</div>
)
}

View File

@ -149,17 +149,6 @@ vi.mock('@/lib/terminal-theme', () => ({
resolvePaneStyleOptions: () => ({ inactivePaneOpacity: 0.8, dividerThicknessPx: 1 })
}))
const ghosttyMock = {
open: false,
preview: null,
loading: false,
applied: false,
applyError: null,
handleClick: vi.fn(),
handleApply: vi.fn(),
handleOpenChange: vi.fn()
}
import { TerminalPane } from './TerminalPane'
type ReactElementLike = {
@ -239,11 +228,8 @@ describe('TerminalPane PowerShell version setting', () => {
terminalWordSeparator: ''
} as never,
updateSettings: () => {},
systemPrefersDark: true,
terminalFontSuggestions: [],
scrollbackMode: 'preset',
setScrollbackMode: () => {},
ghostty: ghosttyMock,
wslAvailable: false,
pwshAvailable: false,
gitBashAvailable: false
@ -264,11 +250,8 @@ describe('TerminalPane PowerShell version setting', () => {
terminalWordSeparator: ''
} as never,
updateSettings: () => {},
systemPrefersDark: true,
terminalFontSuggestions: [],
scrollbackMode: 'preset',
setScrollbackMode: () => {},
ghostty: ghosttyMock,
wslAvailable: true,
wslDistros: ['Ubuntu'],
pwshAvailable: false,
@ -287,11 +270,8 @@ describe('TerminalPane PowerShell version setting', () => {
terminalWordSeparator: ''
} as never,
updateSettings: () => {},
systemPrefersDark: true,
terminalFontSuggestions: [],
scrollbackMode: 'preset',
setScrollbackMode: () => {},
ghostty: ghosttyMock,
wslAvailable: false,
pwshAvailable: false,
gitBashAvailable: false
@ -310,11 +290,8 @@ describe('TerminalPane PowerShell version setting', () => {
terminalWordSeparator: ''
} as never,
updateSettings: () => {},
systemPrefersDark: true,
terminalFontSuggestions: [],
scrollbackMode: 'preset',
setScrollbackMode: () => {},
ghostty: ghosttyMock,
wslAvailable: true,
wslDistros: ['Ubuntu', 'Debian'],
pwshAvailable: false,
@ -337,11 +314,8 @@ describe('TerminalPane PowerShell version setting', () => {
terminalWordSeparator: ''
} as never,
updateSettings: () => {},
systemPrefersDark: true,
terminalFontSuggestions: [],
scrollbackMode: 'preset',
setScrollbackMode: () => {},
ghostty: ghosttyMock,
wslAvailable: false,
pwshAvailable: false,
gitBashAvailable: true
@ -359,11 +333,8 @@ describe('TerminalPane PowerShell version setting', () => {
terminalWordSeparator: ''
} as never,
updateSettings: () => {},
systemPrefersDark: true,
terminalFontSuggestions: [],
scrollbackMode: 'preset',
setScrollbackMode: () => {},
ghostty: ghosttyMock,
wslAvailable: false,
pwshAvailable: false,
gitBashAvailable: false

View File

@ -1,29 +1,12 @@
/* eslint-disable max-lines -- Why: TerminalPane is the single owner of all terminal settings UI;
splitting individual settings into separate files would scatter related controls without a
meaningful abstraction boundary. Mirrors the same decision made for GeneralPane.tsx. */
import { useState } from 'react'
/* eslint-disable max-lines -- Why: TerminalPane keeps terminal workflow, runtime, and recovery
settings together so search shows one focused terminal behavior surface. */
import type { GlobalSettings, SetupScriptLaunchMode } from '../../../../shared/types'
import {
DEFAULT_TERMINAL_FONT_WEIGHT,
TERMINAL_FONT_WEIGHT_MAX,
TERMINAL_FONT_WEIGHT_MIN,
TERMINAL_FONT_WEIGHT_STEP,
normalizeTerminalFontWeight
} from '../../../../shared/terminal-fonts'
import {
fontFamilyHasKnownLigatures,
resolveTerminalLigaturesEnabled
} from '../../../../shared/terminal-ligatures'
import { Button } from '../ui/button'
import { Input } from '../ui/input'
import { Separator } from '../ui/separator'
import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
import { Minus, Plus } from 'lucide-react'
import { clampNumber, resolvePaneStyleOptions } from '@/lib/terminal-theme'
import { clampNumber } from '@/lib/terminal-theme'
import {
FontAutocomplete,
NumberField,
SettingsRow,
SettingsSegmentedControl,
SettingsSubsectionHeader,
@ -37,16 +20,11 @@ import { isMacUserAgent, isWindowsUserAgent } from '@/components/terminal-pane/p
import {
MANAGE_SESSIONS_SEARCH_ENTRIES,
TERMINAL_ADVANCED_SEARCH_ENTRIES,
TERMINAL_CURSOR_SEARCH_ENTRIES,
TERMINAL_DARK_THEME_SEARCH_ENTRIES,
TERMINAL_LIGHT_THEME_SEARCH_ENTRIES,
TERMINAL_MAC_OPTION_SEARCH_ENTRIES,
TERMINAL_MAC_YEN_SEARCH_ENTRIES,
TERMINAL_PANE_STYLE_SEARCH_ENTRIES,
TERMINAL_PANE_INTERACTION_SEARCH_ENTRIES,
TERMINAL_RENDERING_SEARCH_ENTRIES,
TERMINAL_SETUP_SCRIPT_SEARCH_ENTRIES,
TERMINAL_TYPOGRAPHY_SEARCH_ENTRIES,
TERMINAL_WINDOW_SEARCH_ENTRIES
TERMINAL_SETUP_SCRIPT_SEARCH_ENTRIES
} from './terminal-search'
import {
TERMINAL_RIGHT_CLICK_TO_PASTE_SEARCH_ENTRY,
@ -54,26 +32,15 @@ import {
TERMINAL_WINDOWS_SHELL_SEARCH_ENTRY
} from './terminal-windows-search'
import { useDetectedOptionAsAlt } from '@/lib/keyboard-layout/use-effective-mac-option-as-alt'
import { DarkTerminalThemeSection, LightTerminalThemeSection } from './TerminalThemeSections'
import { TerminalWindowSection } from './TerminalWindowSection'
import { GhosttyImportModal } from './GhosttyImportModal'
import type { UseGhosttyImportReturn } from './useGhosttyImport'
import { ManageSessionsSection } from './ManageSessionsSection'
import { TerminalSettingsPreview } from './TerminalSettingsPreview'
import { OSC52_CLIPBOARD_SETTING_ID } from '../terminal-pane/osc52-clipboard-setting-anchor'
import { WINDOWS_GIT_BASH_SHELL } from '../../../../shared/windows-terminal-shell'
type TerminalPaneProps = {
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void
systemPrefersDark: boolean
terminalFontSuggestions: string[]
scrollbackMode: 'preset' | 'custom'
setScrollbackMode: (mode: 'preset' | 'custom') => void
/** Ghostty import modal state + handlers. Lifted to the Settings shell so
* the section header can render the trigger button as a headerAction
* instead of taking its own row inside the settings list. */
ghostty: UseGhosttyImportReturn
/** Whether WSL is installed on this Windows machine. */
wslAvailable?: boolean
/** Installed WSL distro names, used to choose the default WSL terminal target. */
@ -89,11 +56,8 @@ type TerminalPaneProps = {
export function TerminalPane({
settings,
updateSettings,
systemPrefersDark,
terminalFontSuggestions,
scrollbackMode,
setScrollbackMode,
ghostty,
wslAvailable,
wslDistros = [],
wslCapabilitiesLoading = false,
@ -103,12 +67,6 @@ export function TerminalPane({
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
const isWindows = isWindowsUserAgent()
const isMac = isMacUserAgent()
const [themeSearchDark, setThemeSearchDark] = useState('')
const [themeSearchLight, setThemeSearchLight] = useState('')
// Why: hover preview lets the font picker update the sample without committing a setting.
const [previewFontFamily, setPreviewFontFamily] = useState<string | null>(null)
const paneStyleOptions = resolvePaneStyleOptions(settings)
const detectedLayout = useDetectedOptionAsAlt()
const detectedLayoutLabel =
detectedLayout === 'us'
@ -225,196 +183,6 @@ export function TerminalPane({
</div>
</section>
) : null,
matchesSettingsSearch(searchQuery, TERMINAL_TYPOGRAPHY_SEARCH_ENTRIES) ? (
<section key="typography" className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
<div className="min-w-0 space-y-3">
<SettingsSubsectionHeader
title="Typography"
description="Default terminal typography for new panes and live updates."
/>
<div className="divide-y divide-border/40">
<SearchableSetting
title="Font Size"
description="Default terminal font size for new panes and live updates."
keywords={['terminal', 'typography', 'text size']}
>
<SettingsRow
label="Font Size"
description="Default terminal font size for new panes and live updates."
control={
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon-sm"
onClick={() => {
const next = Math.max(10, settings.terminalFontSize - 1)
updateSettings({ terminalFontSize: next })
}}
disabled={settings.terminalFontSize <= 10}
>
<Minus className="size-3" />
</Button>
<Input
type="number"
min={10}
max={24}
value={settings.terminalFontSize}
onChange={(e) => {
const value = parseInt(e.target.value, 10)
if (!Number.isNaN(value) && value >= 10 && value <= 24) {
updateSettings({ terminalFontSize: value })
}
}}
className="w-14 text-center tabular-nums"
/>
<Button
variant="outline"
size="icon-sm"
onClick={() => {
const next = Math.min(24, settings.terminalFontSize + 1)
updateSettings({ terminalFontSize: next })
}}
disabled={settings.terminalFontSize >= 24}
>
<Plus className="size-3" />
</Button>
<span className="text-xs text-muted-foreground">px</span>
</div>
}
/>
</SearchableSetting>
<SearchableSetting
title="Font Family"
description="Default terminal font family for new panes and live updates."
keywords={['terminal', 'typography', 'font']}
>
<SettingsRow
alignTop
label="Font Family"
description="Default terminal font family for new panes and live updates."
control={
<FontAutocomplete
value={settings.terminalFontFamily}
suggestions={terminalFontSuggestions}
onChange={(value) => updateSettings({ terminalFontFamily: value })}
onPreviewFontFamily={setPreviewFontFamily}
/>
}
/>
</SearchableSetting>
<SearchableSetting
title="Font Weight"
description="Controls the terminal text font weight."
keywords={['terminal', 'typography', 'weight']}
>
<NumberField
label="Font Weight"
description="Controls the terminal text font weight."
value={normalizeTerminalFontWeight(settings.terminalFontWeight)}
defaultValue={DEFAULT_TERMINAL_FONT_WEIGHT}
min={TERMINAL_FONT_WEIGHT_MIN}
max={TERMINAL_FONT_WEIGHT_MAX}
step={TERMINAL_FONT_WEIGHT_STEP}
suffix="100900"
onChange={(value) =>
updateSettings({
terminalFontWeight: normalizeTerminalFontWeight(value)
})
}
/>
</SearchableSetting>
<SearchableSetting
title="Line Height"
description="Controls the terminal line height multiplier."
keywords={['terminal', 'typography', 'line height', 'spacing']}
>
<NumberField
label="Line Height"
description="Controls the terminal line height multiplier."
value={settings.terminalLineHeight}
defaultValue={1}
min={1}
max={3}
step={0.1}
suffix="13"
onChange={(value) =>
updateSettings({
terminalLineHeight: clampNumber(value, 1, 3)
})
}
/>
</SearchableSetting>
<SearchableSetting
title="Font Ligatures"
description='Render programming ligatures (e.g. =>, !=, ===) for fonts that ship them. "Auto" enables ligatures only for known ligature fonts (Fira Code, JetBrains Mono, Cascadia Code, Iosevka, etc.).'
keywords={[
'terminal',
'typography',
'ligatures',
'ligature',
'fira code',
'jetbrains mono',
'cascadia code',
'iosevka',
'calt',
'font features'
]}
>
<SettingsRow
label="Font Ligatures"
description={
settings.terminalLigatures === 'on'
? 'Always on. Fonts without ligatures simply render as-is.'
: settings.terminalLigatures === 'off'
? 'Always off, even for fonts that ship them.'
: fontFamilyHasKnownLigatures(settings.terminalFontFamily)
? `Auto — enabled for "${settings.terminalFontFamily}".`
: `Auto — disabled for "${
settings.terminalFontFamily || 'the current font'
}".`
}
control={
<SettingsSegmentedControl
ariaLabel="Font Ligatures"
value={settings.terminalLigatures ?? 'auto'}
onChange={(option) => updateSettings({ terminalLigatures: option })}
options={[
{ value: 'auto', label: 'Auto' },
{ value: 'on', label: 'On' },
{ value: 'off', label: 'Off' }
]}
/>
}
/>
{/* Why: surface the resolved state explicitly so the "Auto" label
isn't ambiguous when a user is staring at it. */}
<p className="sr-only" aria-live="polite">
Ligatures are currently{' '}
{resolveTerminalLigaturesEnabled(
settings.terminalLigatures,
settings.terminalFontFamily
)
? 'enabled'
: 'disabled'}
.
</p>
</SearchableSetting>
</div>
</div>
<TerminalSettingsPreview
title="Preview"
settings={settings}
systemPrefersDark={systemPrefersDark}
previewFontFamily={previewFontFamily}
showThemeToggle
/>
</section>
) : null,
matchesSettingsSearch(searchQuery, TERMINAL_RENDERING_SEARCH_ENTRIES) ? (
<section key="rendering" className="space-y-3">
<SettingsSubsectionHeader
@ -464,129 +232,16 @@ export function TerminalPane({
</div>
</section>
) : null,
matchesSettingsSearch(searchQuery, TERMINAL_CURSOR_SEARCH_ENTRIES) ? (
<section key="cursor" className="space-y-3">
<SettingsSubsectionHeader
title="Cursor"
description="Default cursor appearance for Orca terminal panes."
/>
<div className="divide-y divide-border/40">
<SearchableSetting
title="Cursor Shape"
description="Default cursor appearance for Orca terminal panes."
keywords={['terminal', 'cursor', 'bar', 'block', 'underline']}
>
<SettingsRow
label="Cursor Shape"
description="Default cursor appearance for Orca terminal panes."
control={
<SettingsSegmentedControl
ariaLabel="Cursor Shape"
value={settings.terminalCursorStyle}
onChange={(option) => updateSettings({ terminalCursorStyle: option })}
options={[
{ value: 'bar', label: 'Bar' },
{ value: 'block', label: 'Block' },
{ value: 'underline', label: 'Underline' }
]}
/>
}
/>
</SearchableSetting>
<SearchableSetting
title="Blinking Cursor"
description="Uses the blinking variant of the selected cursor shape."
keywords={['terminal', 'cursor', 'blink']}
>
<SettingsSwitchRow
label="Blinking Cursor"
description="Uses the blinking variant of the selected cursor shape."
checked={settings.terminalCursorBlink}
onChange={() =>
updateSettings({ terminalCursorBlink: !settings.terminalCursorBlink })
}
/>
</SearchableSetting>
<SearchableSetting
title="Cursor Opacity"
description="Opacity of the terminal cursor."
keywords={['terminal', 'cursor', 'opacity', 'transparency']}
>
<NumberField
label="Cursor Opacity"
description="Opacity of the terminal cursor."
value={settings.terminalCursorOpacity ?? 1}
defaultValue={1}
min={0}
max={1}
step={0.05}
suffix="01"
onChange={(value) =>
updateSettings({
terminalCursorOpacity: clampNumber(value, 0, 1)
})
}
/>
</SearchableSetting>
</div>
</section>
) : null,
matchesSettingsSearch(searchQuery, TERMINAL_PANE_STYLE_SEARCH_ENTRIES) ||
matchesSettingsSearch(searchQuery, TERMINAL_PANE_INTERACTION_SEARCH_ENTRIES) ||
(isWindows &&
matchesSettingsSearch(searchQuery, TERMINAL_RIGHT_CLICK_TO_PASTE_SEARCH_ENTRY)) ? (
<section key="pane-styling" className="space-y-3">
<section key="pane-interaction" className="space-y-3">
<SettingsSubsectionHeader
title="Pane Styling"
description="Control inactive pane dimming, divider thickness, mouse behavior, and transition timing."
title="Terminal Interaction"
description="Mouse and clipboard behavior for terminal panes."
/>
<div className="divide-y divide-border/40">
<SearchableSetting
title="Inactive Pane Opacity"
description="Opacity applied to panes that are not currently active."
keywords={['pane', 'opacity', 'dimming']}
>
<NumberField
label="Inactive Pane Opacity"
description="Opacity applied to panes that are not currently active."
value={paneStyleOptions.inactivePaneOpacity}
defaultValue={0.8}
min={0}
max={1}
step={0.05}
suffix="01"
onChange={(value) =>
updateSettings({
terminalInactivePaneOpacity: clampNumber(value, 0, 1)
})
}
/>
</SearchableSetting>
<SearchableSetting
title="Divider Thickness"
description="Thickness of the pane divider line."
keywords={['pane', 'divider', 'thickness']}
>
<NumberField
label="Divider Thickness"
description="Thickness of the pane divider line."
value={paneStyleOptions.dividerThicknessPx}
defaultValue={1}
min={1}
max={32}
step={1}
suffix="px"
onChange={(value) =>
updateSettings({
terminalDividerThicknessPx: clampNumber(value, 1, 32)
})
}
/>
</SearchableSetting>
{/* Why: the Windows-only right-click toggle lives in this section, so the
section must also match that search term or settings search would hide
the control even though it is present. */}
@ -687,30 +342,6 @@ export function TerminalPane({
</div>
</section>
) : null,
matchesSettingsSearch(searchQuery, TERMINAL_WINDOW_SEARCH_ENTRIES) ? (
<TerminalWindowSection key="window" settings={settings} updateSettings={updateSettings} />
) : null,
matchesSettingsSearch(searchQuery, TERMINAL_DARK_THEME_SEARCH_ENTRIES) ? (
<DarkTerminalThemeSection
key="dark-theme"
settings={settings}
systemPrefersDark={systemPrefersDark}
themeSearchDark={themeSearchDark}
setThemeSearchDark={setThemeSearchDark}
updateSettings={updateSettings}
previewFontFamily={previewFontFamily}
/>
) : null,
matchesSettingsSearch(searchQuery, TERMINAL_LIGHT_THEME_SEARCH_ENTRIES) ? (
<LightTerminalThemeSection
key="light-theme"
settings={settings}
themeSearchLight={themeSearchLight}
setThemeSearchLight={setThemeSearchLight}
updateSettings={updateSettings}
previewFontFamily={previewFontFamily}
/>
) : null,
matchesSettingsSearch(searchQuery, TERMINAL_SETUP_SCRIPT_SEARCH_ENTRIES) ? (
<section key="setup-script" className="space-y-3">
<SettingsSubsectionHeader
@ -1048,15 +679,6 @@ export function TerminalPane({
{section}
</div>
))}
<GhosttyImportModal
open={ghostty.open}
onOpenChange={ghostty.handleOpenChange}
preview={ghostty.preview}
loading={ghostty.loading}
onApply={ghostty.handleApply}
applied={ghostty.applied}
applyError={ghostty.applyError}
/>
</div>
)
}

View File

@ -1,5 +1,6 @@
import type { StatusBarItem } from '../../../../shared/types'
import type { SettingsSearchEntry } from './settings-search'
import { TERMINAL_APPEARANCE_SEARCH_ENTRIES } from './terminal-search'
export const STATUS_BAR_TOGGLES: readonly {
id: StatusBarItem
@ -123,6 +124,7 @@ export const APPEARANCE_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
...THEME_ENTRIES,
...TYPOGRAPHY_ENTRIES,
...ZOOM_ENTRIES,
...TERMINAL_APPEARANCE_SEARCH_ENTRIES,
...LAYOUT_ENTRIES,
...TITLEBAR_ENTRIES,
...STATUS_BAR_ENTRIES,

View File

@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import { getTerminalPaneSearchEntries } from './terminal-search'
import { APPEARANCE_PANE_SEARCH_ENTRIES } from './appearance-search'
describe('getTerminalPaneSearchEntries', () => {
it('includes the Windows right-click setting on Windows', () => {
@ -66,12 +67,18 @@ describe('getTerminalPaneSearchEntries', () => {
).toBe(true)
})
it('includes the Ghostty import setting on all platforms', () => {
it('keeps terminal appearance settings in the Appearance search index', () => {
const entriesWindows = getTerminalPaneSearchEntries({ isWindows: true, isMac: false })
const entriesMac = getTerminalPaneSearchEntries({ isWindows: false, isMac: true })
const entriesLinux = getTerminalPaneSearchEntries({ isWindows: false, isMac: false })
expect(entriesWindows.some((entry) => entry.title === 'Import from Ghostty')).toBe(true)
expect(entriesMac.some((entry) => entry.title === 'Import from Ghostty')).toBe(true)
expect(entriesLinux.some((entry) => entry.title === 'Import from Ghostty')).toBe(true)
expect(entriesWindows.some((entry) => entry.title === 'Import from Ghostty')).toBe(false)
expect(entriesMac.some((entry) => entry.title === 'Font Size')).toBe(false)
expect(entriesLinux.some((entry) => entry.title === 'Dark Theme')).toBe(false)
expect(
APPEARANCE_PANE_SEARCH_ENTRIES.some((entry) => entry.title === 'Import from Ghostty')
).toBe(true)
expect(APPEARANCE_PANE_SEARCH_ENTRIES.some((entry) => entry.title === 'Font Size')).toBe(true)
expect(APPEARANCE_PANE_SEARCH_ENTRIES.some((entry) => entry.title === 'Dark Theme')).toBe(true)
})
})

View File

@ -79,7 +79,7 @@ export const TERMINAL_CURSOR_SEARCH_ENTRIES: SettingsSearchEntry[] = [
}
]
export const TERMINAL_PANE_STYLE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
export const TERMINAL_PANE_APPEARANCE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
{
title: 'Inactive Pane Opacity',
description: 'Opacity applied to panes that are not currently active.',
@ -89,7 +89,10 @@ export const TERMINAL_PANE_STYLE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
title: 'Divider Thickness',
description: 'Thickness of the pane divider line.',
keywords: ['pane', 'divider', 'thickness']
},
}
]
export const TERMINAL_PANE_INTERACTION_SEARCH_ENTRIES: SettingsSearchEntry[] = [
{
title: 'Focus Follows Mouse',
description:
@ -269,6 +272,16 @@ export const TERMINAL_SETUP_SCRIPT_SEARCH_ENTRIES: SettingsSearchEntry[] = [
}
]
export const TERMINAL_APPEARANCE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
...TERMINAL_TYPOGRAPHY_SEARCH_ENTRIES,
...TERMINAL_CURSOR_SEARCH_ENTRIES,
...TERMINAL_PANE_APPEARANCE_SEARCH_ENTRIES,
...TERMINAL_DARK_THEME_SEARCH_ENTRIES,
...TERMINAL_LIGHT_THEME_SEARCH_ENTRIES,
...TERMINAL_WINDOW_SEARCH_ENTRIES,
...TERMINAL_GHOSTTY_IMPORT_SEARCH_ENTRIES
]
export function getTerminalPaneSearchEntries(platform: {
isWindows: boolean
isMac: boolean
@ -277,16 +290,10 @@ export function getTerminalPaneSearchEntries(platform: {
// platform-only controls out of other platforms' search results prevents
// users from landing on an option the UI intentionally hides.
return [
...TERMINAL_TYPOGRAPHY_SEARCH_ENTRIES,
...TERMINAL_RENDERING_SEARCH_ENTRIES,
...TERMINAL_CURSOR_SEARCH_ENTRIES,
...TERMINAL_PANE_STYLE_SEARCH_ENTRIES,
...TERMINAL_PANE_INTERACTION_SEARCH_ENTRIES,
...(platform.isWindows ? TERMINAL_WINDOWS_SEARCH_ENTRIES : []),
...TERMINAL_DARK_THEME_SEARCH_ENTRIES,
...TERMINAL_LIGHT_THEME_SEARCH_ENTRIES,
...TERMINAL_WINDOW_SEARCH_ENTRIES,
...TERMINAL_SETUP_SCRIPT_SEARCH_ENTRIES,
...TERMINAL_GHOSTTY_IMPORT_SEARCH_ENTRIES,
...MANAGE_SESSIONS_SEARCH_ENTRIES,
...TERMINAL_ADVANCED_SEARCH_ENTRIES,
...(platform.isMac

View File

@ -92,6 +92,9 @@ export function buildSettingsNavigationMetadata({
: RUNTIME_ENVIRONMENTS_SEARCH_ENTRY
return [
// Why: this array's order must mirror SETTINGS_NAV_GROUPS so the Settings
// sidebar and the Cmd+J palette both read top-to-bottom in the same grouped
// order — keep each new entry beside its group's siblings.
{
id: 'agents',
title: 'Agents',
@ -173,34 +176,10 @@ export function buildSettingsNavigationMetadata({
searchEntries: TASKS_PANE_SEARCH_ENTRIES,
group: 'workflows'
},
{
id: 'floating-workspace',
title: 'Floating Workspace',
description: 'Global terminal, browser, and markdown tabs.',
icon: PanelsTopLeft,
searchEntries: FLOATING_WORKSPACE_SEARCH_ENTRIES,
group: 'workflows'
},
{
id: 'appearance',
title: 'Appearance',
description: 'Theme, zoom, app font, sidebars, and status bar.',
icon: Palette,
searchEntries: APPEARANCE_PANE_SEARCH_ENTRIES,
group: 'interface'
},
{
id: 'input',
title: 'Input & Editing',
description: 'Selection and editing behavior.',
icon: TextCursorInput,
searchEntries: INPUT_PANE_SEARCH_ENTRIES,
group: 'interface'
},
{
id: 'terminal',
title: 'Terminal',
description: 'Shells, terminal appearance, and pane behavior.',
description: 'Shells, renderer, sessions, and terminal behavior.',
icon: SquareTerminal,
searchEntries: terminalPaneSearchEntries,
group: 'workflows'
@ -222,7 +201,35 @@ export function buildSettingsNavigationMetadata({
icon: Globe,
searchEntries: BROWSER_PANE_SEARCH_ENTRIES,
group: 'workflows'
},
}
]
: []),
{
id: 'floating-workspace',
title: 'Floating Workspace',
description: 'Global terminal, browser, and markdown tabs.',
icon: PanelsTopLeft,
searchEntries: FLOATING_WORKSPACE_SEARCH_ENTRIES,
group: 'workflows'
},
{
id: 'appearance',
title: 'Appearance',
description: 'Theme, zoom, app and terminal appearance, sidebars, and status bar.',
icon: Palette,
searchEntries: APPEARANCE_PANE_SEARCH_ENTRIES,
group: 'interface'
},
{
id: 'input',
title: 'Input & Editing',
description: 'Selection and editing behavior.',
icon: TextCursorInput,
searchEntries: INPUT_PANE_SEARCH_ENTRIES,
group: 'interface'
},
...(showDesktopOnlySettings
? [
{
id: 'notifications',
title: 'Notifications',
@ -233,6 +240,22 @@ export function buildSettingsNavigationMetadata({
}
]
: []),
{
id: 'shortcuts',
title: 'Shortcuts',
description: 'Keyboard shortcuts for common actions.',
icon: Keyboard,
searchEntries: SHORTCUTS_PANE_SEARCH_ENTRIES,
group: 'interface'
},
{
id: 'stats',
title: 'Stats & Usage',
description: 'Orca stats plus Claude, Codex, and OpenCode usage analytics.',
icon: BarChart3,
searchEntries: STATS_PANE_SEARCH_ENTRIES,
group: 'interface'
},
{
id: 'servers',
title: 'Remote Orca Servers',
@ -272,7 +295,7 @@ export function buildSettingsNavigationMetadata({
description: 'macOS privacy access for terminal-launched developer tools.',
icon: ShieldCheck,
searchEntries: DEVELOPER_PERMISSIONS_PANE_SEARCH_ENTRIES,
group: 'safety'
group: 'security'
}
]
: []),
@ -282,23 +305,7 @@ export function buildSettingsNavigationMetadata({
description: 'Anonymous usage data and telemetry controls.',
icon: Lock,
searchEntries: PRIVACY_PANE_SEARCH_ENTRIES,
group: 'safety'
},
{
id: 'shortcuts',
title: 'Shortcuts',
description: 'Keyboard shortcuts for common actions.',
icon: Keyboard,
searchEntries: SHORTCUTS_PANE_SEARCH_ENTRIES,
group: 'interface'
},
{
id: 'stats',
title: 'Stats & Usage',
description: 'Orca stats plus Claude, Codex, and OpenCode usage analytics.',
icon: BarChart3,
searchEntries: STATS_PANE_SEARCH_ENTRIES,
group: 'interface'
group: 'security'
},
{
id: 'experimental',