Simplify Appearance settings (#6459)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
6015ad07a0
commit
e58a5e9e17
|
|
@ -214,6 +214,9 @@ function main() {
|
|||
console.log(` -> ${outputIco} (filled multi-size ICO, safe-area inset trimmed)`)
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('trim-windows-icon-source.mjs')) {
|
||||
if (
|
||||
import.meta.url === `file://${process.argv[1]}` ||
|
||||
process.argv[1]?.endsWith('trim-windows-icon-source.mjs')
|
||||
) {
|
||||
main()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -204,8 +204,7 @@ const POWERSHELL_OSC133_ARGS = [
|
|||
// real absolute executable before handing it to ConPTY (PR #6537 / issue
|
||||
// #5161) — a bare/alias `pwsh.exe` makes CreateProcessW fail with error code 5.
|
||||
// These match the deterministic install roots pinned in the win32 beforeEach.
|
||||
const RESOLVED_WINDOWS_POWERSHELL =
|
||||
'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
|
||||
const RESOLVED_WINDOWS_POWERSHELL = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
|
||||
const RESOLVED_PWSH7 = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
|
||||
const TEST_CODEX_HOME =
|
||||
process.platform === 'win32'
|
||||
|
|
|
|||
|
|
@ -39,8 +39,7 @@ function makeAttempt(
|
|||
|
||||
// error code 5 == ERROR_ACCESS_DENIED from CreateProcessW inside ConPTY when a
|
||||
// bare/alias pwsh.exe is handed to node-pty.
|
||||
const ACCESS_DENIED_5 =
|
||||
'Cannot create process, error code: 5'
|
||||
const ACCESS_DENIED_5 = 'Cannot create process, error code: 5'
|
||||
|
||||
describe('spawnShellWithFallback on Windows', () => {
|
||||
it('repro: recovers when the primary PowerShell spawn fails with error code 5', () => {
|
||||
|
|
|
|||
|
|
@ -14,12 +14,10 @@ const WIN_ENV: NodeJS.ProcessEnv = {
|
|||
}
|
||||
|
||||
const PWSH7 = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
|
||||
const WINDOWS_POWERSHELL =
|
||||
'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
|
||||
const WINDOWS_POWERSHELL = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
|
||||
// The Microsoft Store App Execution Alias stub for pwsh — a zero-byte reparse
|
||||
// point under WindowsApps that ConPTY's CreateProcessW rejects with error 5.
|
||||
const PWSH_STORE_ALIAS =
|
||||
'C:\\Users\\dev\\AppData\\Local\\Microsoft\\WindowsApps\\pwsh.exe'
|
||||
const PWSH_STORE_ALIAS = 'C:\\Users\\dev\\AppData\\Local\\Microsoft\\WindowsApps\\pwsh.exe'
|
||||
|
||||
describe('resolveWindowsPowerShellExecutablePath', () => {
|
||||
it('returns null on non-Windows platforms', () => {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ describe('packaged CLI entry redirect', () => {
|
|||
})
|
||||
|
||||
it('does not match the entrypoint on non-Windows platforms', () => {
|
||||
expect(getPackagedCliEntryArgs([execPath, cliEntryPath, 'status'], cliEntryPath, 'linux')).toBeNull()
|
||||
expect(
|
||||
getPackagedCliEntryArgs([execPath, cliEntryPath, 'status'], cliEntryPath, 'linux')
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('spawns the in-package CLI in Electron node mode before the single-instance lock can win', () => {
|
||||
|
|
@ -50,19 +52,15 @@ describe('packaged CLI entry redirect', () => {
|
|||
})
|
||||
|
||||
expect(result).toEqual({ redirected: true, status: 0 })
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
execPath,
|
||||
[cliEntryPath, 'status', '--json'],
|
||||
{
|
||||
env: expect.objectContaining({
|
||||
ELECTRON_RUN_AS_NODE: '1',
|
||||
ORCA_PACKAGED_CLI_ENTRY_REDIRECTED: '1',
|
||||
ORCA_NODE_OPTIONS: '--inspect',
|
||||
ORCA_NODE_REPL_EXTERNAL_MODULE: 'external-loader'
|
||||
}),
|
||||
stdio: 'inherit'
|
||||
}
|
||||
)
|
||||
expect(spawn).toHaveBeenCalledWith(execPath, [cliEntryPath, 'status', '--json'], {
|
||||
env: expect.objectContaining({
|
||||
ELECTRON_RUN_AS_NODE: '1',
|
||||
ORCA_PACKAGED_CLI_ENTRY_REDIRECTED: '1',
|
||||
ORCA_NODE_OPTIONS: '--inspect',
|
||||
ORCA_NODE_REPL_EXTERNAL_MODULE: 'external-loader'
|
||||
}),
|
||||
stdio: 'inherit'
|
||||
})
|
||||
const spawnOptions = spawn.mock.calls[0]?.[2] as { env: NodeJS.ProcessEnv } | undefined
|
||||
expect(spawnOptions?.env).not.toHaveProperty('NODE_OPTIONS')
|
||||
expect(spawnOptions?.env).not.toHaveProperty('NODE_REPL_EXTERNAL_MODULE')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
import type React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAppStore } from '../../store'
|
||||
import { normalizeSettingsSearchQuery } from './settings-search'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
type AppearanceAdvancedDisclosureProps = {
|
||||
/** Optional override label; defaults to "Advanced". */
|
||||
label?: string
|
||||
showTopBorder?: boolean
|
||||
className?: string
|
||||
contentClassName?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/** Inline "Advanced" disclosure for low-frequency controls. An active settings
|
||||
* search force-opens it so matching controls stay reachable instead of being
|
||||
* hidden behind a collapsed trigger. */
|
||||
export function AppearanceAdvancedDisclosure({
|
||||
label,
|
||||
showTopBorder = true,
|
||||
className,
|
||||
contentClassName,
|
||||
children
|
||||
}: AppearanceAdvancedDisclosureProps): React.JSX.Element {
|
||||
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
|
||||
const isSearching = normalizeSettingsSearchQuery(searchQuery).length > 0
|
||||
const [open, setOpen] = useState(false)
|
||||
const expanded = open || isSearching
|
||||
|
||||
return (
|
||||
<div className={cn('mt-3 pt-2', showTopBorder && 'border-t border-border/50', className)}>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={expanded}
|
||||
onClick={() => setOpen((prev) => !prev)}
|
||||
// Why: while searching the disclosure is forced open, so disable the
|
||||
// toggle's collapse affordance rather than letting it fight the search.
|
||||
disabled={isSearching}
|
||||
className="flex w-full items-center gap-2 py-1 text-sm font-semibold text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-default"
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'size-3.5 text-muted-foreground transition-transform',
|
||||
expanded && 'rotate-90'
|
||||
)}
|
||||
/>
|
||||
{label ??
|
||||
translate('auto.components.settings.AppearanceAdvancedDisclosure.advanced', 'Advanced')}
|
||||
</button>
|
||||
{expanded ? <div className={cn('pt-1', contentClassName)}>{children}</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
import type React from 'react'
|
||||
|
||||
import type { GlobalSettings } from '../../../../shared/types'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
||||
import { UIZoomControl } from './UIZoomControl'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
import { AppearanceAdvancedDisclosure } from './AppearanceAdvancedDisclosure'
|
||||
import { useAppStore } from '../../store'
|
||||
import { useShortcutKeyComboDetails } from '@/hooks/useShortcutLabel'
|
||||
import { ShortcutHintList } from './AppearanceShortcutHintList'
|
||||
import {
|
||||
FontAutocomplete,
|
||||
SettingsRow,
|
||||
SettingsSegmentedControl,
|
||||
SettingsSwitchRow
|
||||
} from './SettingsFormControls'
|
||||
import { DEFAULT_APP_FONT_FAMILY } from '../../../../shared/constants'
|
||||
import {
|
||||
getLanguageEntries,
|
||||
getSystemTrayEntries,
|
||||
getThemeEntries,
|
||||
getTitlebarEntries,
|
||||
getTypographyEntries,
|
||||
getZoomEntries
|
||||
} from './appearance-search'
|
||||
import {
|
||||
getUiLanguageChoiceLabel,
|
||||
SHOW_UI_LANGUAGE_SETTING,
|
||||
UI_LANGUAGE_CHOICES
|
||||
} from '@/i18n/supported-languages'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { UiLanguage } from '../../../../shared/ui-language'
|
||||
import { matchesSettingsSearch, normalizeSettingsSearchQuery } from './settings-search'
|
||||
|
||||
type AppearanceInterfaceSectionProps = {
|
||||
settings: GlobalSettings
|
||||
updateSettings: (updates: Partial<GlobalSettings>) => void
|
||||
applyTheme: (theme: 'system' | 'dark' | 'light') => void
|
||||
fontSuggestions: string[]
|
||||
isDesktopWindows: boolean
|
||||
forceVisiblePrimary?: boolean
|
||||
}
|
||||
|
||||
export function AppearanceInterfaceSection({
|
||||
settings,
|
||||
updateSettings,
|
||||
applyTheme,
|
||||
fontSuggestions,
|
||||
isDesktopWindows,
|
||||
forceVisiblePrimary = false
|
||||
}: AppearanceInterfaceSectionProps): React.JSX.Element {
|
||||
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
|
||||
const isSearching = normalizeSettingsSearchQuery(searchQuery).length > 0
|
||||
const zoomInKeyCombos = useShortcutKeyComboDetails('zoom.in')
|
||||
const zoomOutKeyCombos = useShortcutKeyComboDetails('zoom.out')
|
||||
const languageEntry = getLanguageEntries()[0]
|
||||
const systemTrayEntry = getSystemTrayEntries({ showSystemTray: true })[0]
|
||||
const themeEntry = getThemeEntries()[0]
|
||||
const themeLabel = translate('auto.components.settings.AppearancePane.932ff1fbff', 'Theme')
|
||||
const titlebarEntry = getTitlebarEntries()[0]
|
||||
const typographyEntry = getTypographyEntries()[0]
|
||||
const zoomEntry = getZoomEntries()[0]
|
||||
const advancedEntries = [
|
||||
...(SHOW_UI_LANGUAGE_SETTING ? getLanguageEntries() : []),
|
||||
...getTitlebarEntries(),
|
||||
...getSystemTrayEntries({ showSystemTray: isDesktopWindows })
|
||||
]
|
||||
const showAdvanced = !isSearching || matchesSettingsSearch(searchQuery, advancedEntries)
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-border/40">
|
||||
<SearchableSetting
|
||||
title={themeLabel}
|
||||
description={themeEntry?.description}
|
||||
keywords={themeEntry?.keywords ?? ['dark', 'light', 'system']}
|
||||
forceVisible={forceVisiblePrimary}
|
||||
>
|
||||
<SettingsRow
|
||||
label={themeLabel}
|
||||
control={
|
||||
<SettingsSegmentedControl
|
||||
ariaLabel={themeLabel}
|
||||
value={settings.theme}
|
||||
onChange={(option) => {
|
||||
updateSettings({ theme: option })
|
||||
applyTheme(option)
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
value: 'system',
|
||||
label: translate('auto.components.settings.AppearancePane.fb0e0b4453', 'System')
|
||||
},
|
||||
{
|
||||
value: 'dark',
|
||||
label: translate('auto.components.settings.AppearancePane.7d26ccabe8', 'Dark')
|
||||
},
|
||||
{
|
||||
value: 'light',
|
||||
label: translate('auto.components.settings.AppearancePane.fd89b5487c', 'Light')
|
||||
}
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate('auto.components.settings.AppearancePane.5e6d7aba8d', 'UI Zoom')}
|
||||
description={zoomEntry?.description}
|
||||
keywords={zoomEntry?.keywords ?? ['zoom', 'scale', 'shortcut']}
|
||||
forceVisible={forceVisiblePrimary}
|
||||
>
|
||||
<SettingsRow
|
||||
label={translate('auto.components.settings.AppearancePane.5e6d7aba8d', 'UI Zoom')}
|
||||
// Why: keep only the shortcut hint — the control itself makes "scale the
|
||||
// interface" obvious, but the keyboard gesture and its terminal-pane
|
||||
// exception are not discoverable from the buttons alone.
|
||||
description={
|
||||
<>
|
||||
<ShortcutHintList combos={zoomInKeyCombos} /> /{' '}
|
||||
<ShortcutHintList combos={zoomOutKeyCombos} />{' '}
|
||||
{translate(
|
||||
'auto.components.settings.AppearancePane.ef89200c1f',
|
||||
'when not in a terminal pane.'
|
||||
)}
|
||||
</>
|
||||
}
|
||||
control={<UIZoomControl />}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate('auto.components.settings.AppearancePane.102d6b5f9b', 'IDE Font')}
|
||||
description={typographyEntry?.description}
|
||||
keywords={typographyEntry?.keywords ?? ['font', 'typeface', 'typography']}
|
||||
forceVisible={forceVisiblePrimary}
|
||||
>
|
||||
<SettingsRow
|
||||
label={translate('auto.components.settings.AppearancePane.102d6b5f9b', 'IDE Font')}
|
||||
control={
|
||||
<FontAutocomplete
|
||||
value={settings.appFontFamily}
|
||||
suggestions={fontSuggestions}
|
||||
placeholder={DEFAULT_APP_FONT_FAMILY}
|
||||
onChange={(value) =>
|
||||
updateSettings({ appFontFamily: value.trim() || DEFAULT_APP_FONT_FAMILY })
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
{showAdvanced ? (
|
||||
<AppearanceAdvancedDisclosure showTopBorder={false}>
|
||||
<div className="divide-y divide-border/40">
|
||||
{SHOW_UI_LANGUAGE_SETTING ? (
|
||||
<SearchableSetting
|
||||
title={translate('settings.appearance.language.title', 'Language')}
|
||||
description={languageEntry?.description}
|
||||
keywords={languageEntry?.keywords ?? []}
|
||||
>
|
||||
<SettingsRow
|
||||
label={translate('settings.appearance.language.title', 'Language')}
|
||||
control={
|
||||
<Select
|
||||
value={settings.uiLanguage}
|
||||
onValueChange={(value) => updateSettings({ uiLanguage: value as UiLanguage })}
|
||||
>
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="min-w-[220px]"
|
||||
aria-label={translate('settings.appearance.language.title', 'Language')}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{UI_LANGUAGE_CHOICES.map((choice) => (
|
||||
<SelectItem key={choice.value} value={choice.value}>
|
||||
{getUiLanguageChoiceLabel(choice, translate)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
) : null}
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.AppearancePane.9868f39007',
|
||||
'Titlebar App Name'
|
||||
)}
|
||||
description={titlebarEntry?.description}
|
||||
keywords={titlebarEntry?.keywords ?? ['titlebar', 'orca', 'app', 'name']}
|
||||
>
|
||||
<SettingsSwitchRow
|
||||
label={translate(
|
||||
'auto.components.settings.AppearancePane.9868f39007',
|
||||
'Titlebar App Name'
|
||||
)}
|
||||
checked={settings.showTitlebarAppName}
|
||||
onChange={() =>
|
||||
updateSettings({ showTitlebarAppName: !settings.showTitlebarAppName })
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
{isDesktopWindows ? (
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.AppearancePane.2edf606c46',
|
||||
'Minimize to Tray on Close'
|
||||
)}
|
||||
description={systemTrayEntry?.description}
|
||||
keywords={systemTrayEntry?.keywords ?? ['tray', 'minimize', 'close']}
|
||||
>
|
||||
<SettingsSwitchRow
|
||||
label={translate(
|
||||
'auto.components.settings.AppearancePane.2edf606c46',
|
||||
'Minimize to Tray on Close'
|
||||
)}
|
||||
// Why: platform constraint + "close keeps Orca running" consequence are
|
||||
// both non-obvious from the label alone.
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.b707773a0d',
|
||||
'When enabled, closing the window keeps Orca running in the system tray instead of quitting.'
|
||||
)}
|
||||
checked={settings.minimizeToTrayOnClose === true}
|
||||
onChange={() =>
|
||||
updateSettings({ minimizeToTrayOnClose: !settings.minimizeToTrayOnClose })
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
) : null}
|
||||
</div>
|
||||
</AppearanceAdvancedDisclosure>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -10,10 +10,18 @@ import type { GlobalSettings } from '../../../../shared/types'
|
|||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
state: {
|
||||
availableStatusBarToggles: [] as {
|
||||
description: string
|
||||
id: 'ports'
|
||||
keywords: string[]
|
||||
title: string
|
||||
toggleDescription: string
|
||||
}[],
|
||||
settingsSearchQuery: 'automations',
|
||||
statusBarItems: [],
|
||||
toggleStatusBarItem: vi.fn(),
|
||||
recordFeatureInteraction: vi.fn()
|
||||
recordFeatureInteraction: vi.fn(),
|
||||
setWorktreeCardMode: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
|
|
@ -26,7 +34,7 @@ vi.mock('@/hooks/useShortcutLabel', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('../status-bar/use-available-status-bar-toggles', () => ({
|
||||
useAvailableStatusBarToggles: () => []
|
||||
useAvailableStatusBarToggles: () => mocks.state.availableStatusBarToggles
|
||||
}))
|
||||
|
||||
vi.mock('./TerminalAppearanceSection', () => ({
|
||||
|
|
@ -85,6 +93,7 @@ vi.mock('../ui/select', async () => {
|
|||
})
|
||||
|
||||
import { AppearancePane } from './AppearancePane'
|
||||
import { TooltipProvider } from '../ui/tooltip'
|
||||
|
||||
const mountedRoots: Root[] = []
|
||||
|
||||
|
|
@ -130,16 +139,18 @@ async function renderAppearancePane(
|
|||
await act(async () => {
|
||||
root.render(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<AppearancePane
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
applyTheme={vi.fn()}
|
||||
fontSuggestions={[]}
|
||||
terminalFontSuggestions={[]}
|
||||
systemPrefersDark={false}
|
||||
ghostty={createGhosttyStub() as never}
|
||||
warpThemes={createWarpThemesStub() as never}
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<AppearancePane
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
applyTheme={vi.fn()}
|
||||
fontSuggestions={[]}
|
||||
terminalFontSuggestions={[]}
|
||||
systemPrefersDark={false}
|
||||
ghostty={createGhosttyStub() as never}
|
||||
warpThemes={createWarpThemesStub() as never}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
</I18nextProvider>
|
||||
)
|
||||
})
|
||||
|
|
@ -159,7 +170,22 @@ describe('AppearancePane', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.state.availableStatusBarToggles = []
|
||||
mocks.state.settingsSearchQuery = 'automations'
|
||||
// UIZoomControl reads window.api.ui on mount; the inline-expansion pane can
|
||||
// render the full Interface section, so provide a minimal renderer bridge
|
||||
// without clobbering happy-dom's window.location.
|
||||
;(window as unknown as { api: unknown }).api = {
|
||||
ui: {
|
||||
getZoomLevel: () => 0,
|
||||
onTerminalZoom: () => () => {},
|
||||
set: vi.fn()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
delete (window as unknown as { api?: unknown }).api
|
||||
})
|
||||
|
||||
it('renders the language dropdown with system, english, chinese, korean, japanese, and spanish options', async () => {
|
||||
|
|
@ -236,4 +262,117 @@ describe('AppearancePane', () => {
|
|||
|
||||
expect(updateSettings).toHaveBeenCalledWith({ showAutomationsButton: true })
|
||||
})
|
||||
|
||||
it('changes workspace card layout from the Appearance sidebar controls', async () => {
|
||||
mocks.state.settingsSearchQuery = 'workspace card layout'
|
||||
const settings = {
|
||||
...getDefaultSettings('/tmp'),
|
||||
compactWorktreeCards: false
|
||||
}
|
||||
|
||||
const container = await renderAppearancePane(settings)
|
||||
const compactButton = Array.from(
|
||||
container.querySelectorAll<HTMLButtonElement>('button[role="radio"]')
|
||||
).find((button) => button.textContent === 'Compact')
|
||||
|
||||
expect(compactButton).toBeDefined()
|
||||
|
||||
await act(async () => {
|
||||
compactButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
})
|
||||
|
||||
expect(mocks.state.setWorktreeCardMode).toHaveBeenCalledWith('Compact')
|
||||
})
|
||||
|
||||
it('renders the three top-level section rows and no Code & Markdown row when not searching', async () => {
|
||||
mocks.state.settingsSearchQuery = ''
|
||||
const container = await renderAppearancePane(getDefaultSettings('/tmp'))
|
||||
|
||||
expect(container.textContent).toContain('Interface')
|
||||
expect(container.textContent).toContain('Terminal')
|
||||
expect(container.textContent).toContain('Window & Sidebar')
|
||||
// Code & Markdown is intentionally omitted — Orca has no Appearance-level
|
||||
// code/markdown settings, so the row would be empty.
|
||||
expect(container.textContent).not.toContain('Code & Markdown')
|
||||
})
|
||||
|
||||
it('keeps the app icon control at the bottom of the pane, after the section rows', async () => {
|
||||
mocks.state.settingsSearchQuery = ''
|
||||
const container = await renderAppearancePane(getDefaultSettings('/tmp'))
|
||||
|
||||
const buttons = Array.from(container.querySelectorAll('button'))
|
||||
const interfaceRow = buttons.find((button) => button.textContent?.includes('Interface'))
|
||||
const appIconImage = container.querySelector<HTMLImageElement>('img[alt="Selected app icon"]')
|
||||
|
||||
expect(interfaceRow).toBeDefined()
|
||||
expect(appIconImage).not.toBeNull()
|
||||
// The App Icon block sits after the Interface section row in document order.
|
||||
expect(
|
||||
interfaceRow &&
|
||||
appIconImage &&
|
||||
interfaceRow.compareDocumentPosition(appIconImage) & Node.DOCUMENT_POSITION_FOLLOWING
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it('reveals an advanced sidebar control when its search matches, even though it is hidden by default', async () => {
|
||||
// The Show Tasks Button toggle lives behind the Window & Sidebar Advanced
|
||||
// disclosure; with no search it stays collapsed, but a matching query must
|
||||
// force the disclosure open so the control is reachable.
|
||||
mocks.state.settingsSearchQuery = ''
|
||||
const collapsedContainer = await renderAppearancePane(getDefaultSettings('/tmp'))
|
||||
expect(
|
||||
collapsedContainer.querySelector('button[role="switch"][aria-label="Show Tasks Button"]')
|
||||
).toBeNull()
|
||||
|
||||
mocks.state.settingsSearchQuery = 'tasks'
|
||||
const searchedContainer = await renderAppearancePane(getDefaultSettings('/tmp'))
|
||||
expect(
|
||||
searchedContainer.querySelector('button[role="switch"][aria-label="Show Tasks Button"]')
|
||||
).not.toBeNull()
|
||||
})
|
||||
|
||||
it('keeps description-only search matches visible after helper text is hidden', async () => {
|
||||
mocks.state.settingsSearchQuery = 'app window'
|
||||
const container = await renderAppearancePane(getDefaultSettings('/tmp'))
|
||||
|
||||
expect(container.textContent).toContain('Theme')
|
||||
expect(container.textContent).not.toContain('Advanced')
|
||||
})
|
||||
|
||||
it('shows useful primary rows for a Window & Sidebar section-label search', async () => {
|
||||
mocks.state.settingsSearchQuery = 'Window & Sidebar'
|
||||
const container = await renderAppearancePane(getDefaultSettings('/tmp'))
|
||||
|
||||
expect(container.textContent).toContain('Left Sidebar Appearance')
|
||||
expect(container.textContent).toContain('Status Bar')
|
||||
expect(container.textContent).not.toContain('Advanced')
|
||||
})
|
||||
|
||||
it('expands status bar controls for a section-label search', async () => {
|
||||
mocks.state.availableStatusBarToggles = [
|
||||
{
|
||||
id: 'ports',
|
||||
title: 'Ports',
|
||||
description: 'Show live workspace ports in the status bar.',
|
||||
toggleDescription: 'Show Ports in the status bar.',
|
||||
keywords: ['status bar', 'ports']
|
||||
}
|
||||
]
|
||||
mocks.state.settingsSearchQuery = 'status bar'
|
||||
const container = await renderAppearancePane(getDefaultSettings('/tmp'))
|
||||
|
||||
expect(container.querySelector('button[role="switch"][aria-label="Ports"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('collapses sibling sections so only the Interface section is expanded by default', async () => {
|
||||
mocks.state.settingsSearchQuery = ''
|
||||
const container = await renderAppearancePane(getDefaultSettings('/tmp'))
|
||||
|
||||
const expanded = Array.from(
|
||||
container.querySelectorAll<HTMLButtonElement>('button[aria-expanded="true"]')
|
||||
).filter((button) => button.getAttribute('aria-controls')?.startsWith('appearance-section-'))
|
||||
|
||||
expect(expanded).toHaveLength(1)
|
||||
expect(expanded[0]?.textContent).toContain('Interface')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,36 +1,22 @@
|
|||
/* eslint-disable max-lines -- Why: AppearancePane keeps theme, typography, zoom, and status-bar
|
||||
visibility settings together so the searchable settings rows share one filtered surface. */
|
||||
import type React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { AppWindow, PanelLeft, TerminalSquare } from 'lucide-react'
|
||||
|
||||
import type { GlobalSettings } from '../../../../shared/types'
|
||||
|
||||
import { Separator } from '../ui/separator'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
||||
import { UIZoomControl } from './UIZoomControl'
|
||||
import { AppearanceSection } from './AppearanceSection'
|
||||
import { AppearanceInterfaceSection } from './AppearanceInterfaceSection'
|
||||
import { AppearanceWindowSidebarSection } from './AppearanceWindowSidebarSection'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
import { matchesSettingsSearch } from './settings-search'
|
||||
import { matchesSettingsSearch, normalizeSettingsSearchQuery } from './settings-search'
|
||||
import { useAppStore } from '../../store'
|
||||
import { useShortcutKeyComboDetails, type ShortcutKeyComboDetails } from '@/hooks/useShortcutLabel'
|
||||
import { ShortcutKeyCombo } from '../ShortcutKeyCombo'
|
||||
import {
|
||||
FontAutocomplete,
|
||||
SettingsRow,
|
||||
SettingsSegmentedControl,
|
||||
SettingsSubsectionHeader,
|
||||
SettingsSwitchRow
|
||||
} from './SettingsFormControls'
|
||||
import { DEFAULT_APP_FONT_FAMILY } from '../../../../shared/constants'
|
||||
import { normalizeAppIconId } from '../../../../shared/app-icon'
|
||||
import { useAvailableStatusBarToggles } from '../status-bar/use-available-status-bar-toggles'
|
||||
import {
|
||||
getAppIconEntries,
|
||||
getAppearancePaneSearchEntries,
|
||||
getLanguageEntries,
|
||||
getLayoutEntries,
|
||||
getLeftSidebarAppearanceEntry,
|
||||
getSidebarEntries,
|
||||
getStatusBarEntries,
|
||||
getStatusBarToggles,
|
||||
getSystemTrayEntries,
|
||||
getThemeEntries,
|
||||
getTitlebarEntries,
|
||||
|
|
@ -42,17 +28,15 @@ import { TerminalAppearanceSection } from './TerminalAppearanceSection'
|
|||
import type { UseGhosttyImportReturn } from './useGhosttyImport'
|
||||
import type { UseWarpThemeImportReturn } from './useWarpThemeImport'
|
||||
import { AppIconSelector } from './AppIconSelector'
|
||||
import { normalizeAppIconId } from '../../../../shared/app-icon'
|
||||
import { getRendererAppPlatform } from '@/lib/renderer-app-platform'
|
||||
import { isWebClientLocation } from '@/lib/web-client-location'
|
||||
import {
|
||||
getUiLanguageChoiceLabel,
|
||||
SHOW_UI_LANGUAGE_SETTING,
|
||||
UI_LANGUAGE_CHOICES
|
||||
} from '@/i18n/supported-languages'
|
||||
import { SHOW_UI_LANGUAGE_SETTING } from '@/i18n/supported-languages'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { UiLanguage } from '../../../../shared/ui-language'
|
||||
import { LeftSidebarAppearanceSetting } from './LeftSidebarAppearanceSetting'
|
||||
import { getWorkspaceCardLayoutEntry } from './appearance-sidebar-search'
|
||||
import {
|
||||
getLeftSidebarAppearanceEntry,
|
||||
getWorkspaceCardLayoutEntry
|
||||
} from './appearance-sidebar-search'
|
||||
export { getAppearancePaneSearchEntries }
|
||||
|
||||
type AppearancePaneProps = {
|
||||
|
|
@ -66,28 +50,16 @@ type AppearancePaneProps = {
|
|||
warpThemes: UseWarpThemeImportReturn
|
||||
}
|
||||
|
||||
function ShortcutHintList({ combos }: { combos: ShortcutKeyComboDetails[] }): React.JSX.Element {
|
||||
if (combos.length === 0) {
|
||||
return (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{translate('auto.components.settings.AppearancePane.3057983501', 'Unassigned')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
type AppearanceSectionKey = 'interface' | 'terminal' | 'window'
|
||||
|
||||
return (
|
||||
<span className="inline-flex flex-wrap items-center gap-1 align-middle">
|
||||
{combos.map((combo) => (
|
||||
<ShortcutKeyCombo
|
||||
key={combo.keys.join('-')}
|
||||
keys={combo.keys}
|
||||
doubleTap={combo.doubleTap}
|
||||
className="inline-flex gap-0.5"
|
||||
separatorClassName="text-[10px] text-muted-foreground"
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
function resolveThemeSummary(theme: GlobalSettings['theme']): string {
|
||||
if (theme === 'system') {
|
||||
return translate('auto.components.settings.AppearancePane.fb0e0b4453', 'System')
|
||||
}
|
||||
if (theme === 'light') {
|
||||
return translate('auto.components.settings.AppearancePane.fd89b5487c', 'Light')
|
||||
}
|
||||
return translate('auto.components.settings.AppearancePane.7d26ccabe8', 'Dark')
|
||||
}
|
||||
|
||||
export function AppearancePane({
|
||||
|
|
@ -101,479 +73,165 @@ export function AppearancePane({
|
|||
warpThemes
|
||||
}: AppearancePaneProps): React.JSX.Element {
|
||||
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
|
||||
const isSearching = normalizeSettingsSearchQuery(searchQuery).length > 0
|
||||
const isWebClient = isWebClientLocation()
|
||||
// Why: the system tray behavior is desktop-Electron Windows-only; a Windows
|
||||
// browser web client has no local tray to control.
|
||||
const isDesktopWindows = getRendererAppPlatform() === 'win32' && !isWebClient
|
||||
const zoomInKeyCombos = useShortcutKeyComboDetails('zoom.in')
|
||||
const zoomOutKeyCombos = useShortcutKeyComboDetails('zoom.out')
|
||||
const statusBarItems = useAppStore((state) => state.statusBarItems)
|
||||
const toggleStatusBarItem = useAppStore((state) => state.toggleStatusBarItem)
|
||||
const recordFeatureInteraction = useAppStore((state) => state.recordFeatureInteraction)
|
||||
const visibleStatusBarToggles = useAvailableStatusBarToggles(getStatusBarToggles())
|
||||
const terminalAppearanceSearchEntries = getTerminalAppearanceSearchEntries({
|
||||
showWarpImport: !isWebClient
|
||||
|
||||
const [manuallyOpenSection, setManuallyOpenSection] = useState<AppearanceSectionKey | null>(
|
||||
'interface'
|
||||
)
|
||||
const interfaceTitle = translate(
|
||||
'auto.components.settings.AppearancePane.interfaceTitle',
|
||||
'Interface'
|
||||
)
|
||||
const terminalTitle = translate(
|
||||
'auto.components.settings.AppearancePane.terminalTitle',
|
||||
'Terminal'
|
||||
)
|
||||
const windowSidebarTitle = translate(
|
||||
'auto.components.settings.AppearancePane.windowSidebarTitle',
|
||||
'Window & Sidebar'
|
||||
)
|
||||
const windowSidebarSummary = translate(
|
||||
'auto.components.settings.AppearancePane.windowSidebarSummary',
|
||||
'Sidebar, status bar, and file explorer'
|
||||
)
|
||||
|
||||
// Search-entry buckets per section so a query can force-open the matching one.
|
||||
const interfaceSearchEntries = [
|
||||
{ title: interfaceTitle },
|
||||
...getThemeEntries(),
|
||||
...getZoomEntries(),
|
||||
...getTypographyEntries(),
|
||||
...(SHOW_UI_LANGUAGE_SETTING ? getLanguageEntries() : []),
|
||||
...getTitlebarEntries(),
|
||||
...getSystemTrayEntries({ showSystemTray: isDesktopWindows })
|
||||
]
|
||||
const terminalSearchEntries = [
|
||||
{ title: terminalTitle },
|
||||
...getTerminalAppearanceSearchEntries({ showWarpImport: !isWebClient })
|
||||
]
|
||||
const windowSearchEntries = [
|
||||
{
|
||||
title: windowSidebarTitle,
|
||||
description: windowSidebarSummary
|
||||
},
|
||||
...getStatusBarEntries(),
|
||||
...getSidebarEntries(),
|
||||
...getLayoutEntries(),
|
||||
getLeftSidebarAppearanceEntry(),
|
||||
getWorkspaceCardLayoutEntry()
|
||||
]
|
||||
|
||||
const interfaceMatches = matchesSettingsSearch(searchQuery, interfaceSearchEntries)
|
||||
const terminalMatches = matchesSettingsSearch(searchQuery, terminalSearchEntries)
|
||||
const windowMatches = matchesSettingsSearch(searchQuery, windowSearchEntries)
|
||||
const interfaceLabelMatches = matchesSettingsSearch(searchQuery, { title: interfaceTitle })
|
||||
const terminalLabelMatches = matchesSettingsSearch(searchQuery, { title: terminalTitle })
|
||||
const windowLabelMatches = matchesSettingsSearch(searchQuery, {
|
||||
title: windowSidebarTitle,
|
||||
description: windowSidebarSummary
|
||||
})
|
||||
const systemTrayEntries = getSystemTrayEntries({ showSystemTray: isDesktopWindows })
|
||||
const leftSidebarAppearanceEntry = getLeftSidebarAppearanceEntry()
|
||||
const workspaceCardLayoutEntry = getWorkspaceCardLayoutEntry()
|
||||
const visibleSections = [
|
||||
matchesSettingsSearch(searchQuery, getThemeEntries()) ||
|
||||
(SHOW_UI_LANGUAGE_SETTING && matchesSettingsSearch(searchQuery, getLanguageEntries())) ||
|
||||
matchesSettingsSearch(searchQuery, getZoomEntries()) ||
|
||||
matchesSettingsSearch(searchQuery, getTypographyEntries()) ? (
|
||||
<section key="interface" className="divide-y divide-border/40">
|
||||
{matchesSettingsSearch(searchQuery, getThemeEntries()) ? (
|
||||
<SearchableSetting
|
||||
title={translate('auto.components.settings.AppearancePane.932ff1fbff', 'Theme')}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.0f28e7b30c',
|
||||
'Choose how Orca looks in the app window.'
|
||||
)}
|
||||
keywords={getThemeEntries()[0]?.keywords ?? ['dark', 'light', 'system']}
|
||||
>
|
||||
<SettingsRow
|
||||
label={translate('auto.components.settings.AppearancePane.932ff1fbff', 'Theme')}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.0f28e7b30c',
|
||||
'Choose how Orca looks in the app window.'
|
||||
)}
|
||||
control={
|
||||
<SettingsSegmentedControl
|
||||
ariaLabel={translate(
|
||||
'auto.components.settings.AppearancePane.932ff1fbff',
|
||||
'Theme'
|
||||
)}
|
||||
value={settings.theme}
|
||||
onChange={(option) => {
|
||||
updateSettings({ theme: option })
|
||||
applyTheme(option)
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
value: 'system',
|
||||
label: translate(
|
||||
'auto.components.settings.AppearancePane.fb0e0b4453',
|
||||
'System'
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'dark',
|
||||
label: translate('auto.components.settings.AppearancePane.7d26ccabe8', 'Dark')
|
||||
},
|
||||
{
|
||||
value: 'light',
|
||||
label: translate(
|
||||
'auto.components.settings.AppearancePane.fd89b5487c',
|
||||
'Light'
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
) : null}
|
||||
const appIconMatches = matchesSettingsSearch(searchQuery, getAppIconEntries())
|
||||
|
||||
{SHOW_UI_LANGUAGE_SETTING && matchesSettingsSearch(searchQuery, getLanguageEntries()) ? (
|
||||
<SearchableSetting
|
||||
title={translate('settings.appearance.language.title', 'Language')}
|
||||
description={translate(
|
||||
'settings.appearance.language.description',
|
||||
'Choose the language used by the Orca interface.'
|
||||
)}
|
||||
keywords={getLanguageEntries()[0]?.keywords ?? []}
|
||||
>
|
||||
<SettingsRow
|
||||
label={translate('settings.appearance.language.title', 'Language')}
|
||||
description={translate(
|
||||
'settings.appearance.language.description',
|
||||
'Choose the language used by the Orca interface.'
|
||||
)}
|
||||
control={
|
||||
<Select
|
||||
value={settings.uiLanguage}
|
||||
onValueChange={(value) => updateSettings({ uiLanguage: value as UiLanguage })}
|
||||
>
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="min-w-[220px]"
|
||||
aria-label={translate('settings.appearance.language.title', 'Language')}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{UI_LANGUAGE_CHOICES.map((choice) => (
|
||||
<SelectItem key={choice.value} value={choice.value}>
|
||||
{getUiLanguageChoiceLabel(choice, translate)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
) : null}
|
||||
// While searching, force-open every section that contains a match so its
|
||||
// controls (including advanced ones) are revealed; otherwise the accordion
|
||||
// shows exactly one manually-chosen section.
|
||||
function isSectionOpen(key: AppearanceSectionKey): boolean {
|
||||
if (isSearching) {
|
||||
return key === 'interface'
|
||||
? interfaceMatches
|
||||
: key === 'terminal'
|
||||
? terminalMatches
|
||||
: windowMatches
|
||||
}
|
||||
return manuallyOpenSection === key
|
||||
}
|
||||
|
||||
{matchesSettingsSearch(searchQuery, getZoomEntries()) ? (
|
||||
<SearchableSetting
|
||||
title={translate('auto.components.settings.AppearancePane.5e6d7aba8d', 'UI Zoom')}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.622e1c3465',
|
||||
'Scale the entire application interface.'
|
||||
)}
|
||||
keywords={getZoomEntries()[0]?.keywords ?? ['zoom', 'scale', 'shortcut']}
|
||||
>
|
||||
<SettingsRow
|
||||
label={translate('auto.components.settings.AppearancePane.5e6d7aba8d', 'UI Zoom')}
|
||||
description={
|
||||
<>
|
||||
{translate(
|
||||
'auto.components.settings.AppearancePane.f687711a9b',
|
||||
'Scale the entire application interface. Use'
|
||||
)}{' '}
|
||||
<ShortcutHintList combos={zoomInKeyCombos} /> /{' '}
|
||||
<ShortcutHintList combos={zoomOutKeyCombos} />{' '}
|
||||
{translate(
|
||||
'auto.components.settings.AppearancePane.ef89200c1f',
|
||||
'when not in a terminal pane.'
|
||||
)}
|
||||
</>
|
||||
}
|
||||
control={<UIZoomControl />}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
) : null}
|
||||
function toggleSection(key: AppearanceSectionKey): void {
|
||||
setManuallyOpenSection((current) => (current === key ? null : key))
|
||||
}
|
||||
|
||||
{matchesSettingsSearch(searchQuery, getTypographyEntries()) ? (
|
||||
<SearchableSetting
|
||||
title={translate('auto.components.settings.AppearancePane.102d6b5f9b', 'IDE Font')}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.42554f615f',
|
||||
'Choose the font used by the Orca interface.'
|
||||
)}
|
||||
keywords={getTypographyEntries()[0]?.keywords ?? ['font', 'typeface', 'typography']}
|
||||
>
|
||||
<SettingsRow
|
||||
alignTop
|
||||
label={translate('auto.components.settings.AppearancePane.102d6b5f9b', 'IDE Font')}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.42554f615f',
|
||||
'Choose the font used by the Orca interface.'
|
||||
)}
|
||||
control={
|
||||
<FontAutocomplete
|
||||
value={settings.appFontFamily}
|
||||
suggestions={fontSuggestions}
|
||||
placeholder={DEFAULT_APP_FONT_FAMILY}
|
||||
onChange={(value) =>
|
||||
updateSettings({ appFontFamily: value.trim() || DEFAULT_APP_FONT_FAMILY })
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
) : null}
|
||||
</section>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, terminalAppearanceSearchEntries) ? (
|
||||
<TerminalAppearanceSection
|
||||
key="terminal-appearance"
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
systemPrefersDark={systemPrefersDark}
|
||||
terminalFontSuggestions={terminalFontSuggestions}
|
||||
ghostty={ghostty}
|
||||
warpThemes={warpThemes}
|
||||
/>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, getLayoutEntries()) ? (
|
||||
<section key="layout" className="space-y-3">
|
||||
<SettingsSubsectionHeader
|
||||
title={translate('auto.components.settings.AppearancePane.d496901cd0', 'File Explorer')}
|
||||
/>
|
||||
const interfaceSummary = `${resolveThemeSummary(settings.theme)} · ${
|
||||
settings.appFontFamily ||
|
||||
translate('auto.components.settings.AppearancePane.interfaceDefaultFont', 'Default font')
|
||||
}`
|
||||
const terminalSummary = `${
|
||||
settings.terminalFontFamily ||
|
||||
translate('auto.components.settings.AppearancePane.terminalDefaultFont', 'Default font')
|
||||
} · ${settings.terminalFontSize}px`
|
||||
|
||||
<div className="divide-y divide-border/40">
|
||||
<SearchableSetting
|
||||
title={
|
||||
getLayoutEntries()[0]?.title ??
|
||||
translate(
|
||||
'auto.components.settings.AppearancePane.0fafabcf35',
|
||||
'Show Git-Ignored Files'
|
||||
)
|
||||
}
|
||||
description={
|
||||
getLayoutEntries()[0]?.description ??
|
||||
translate(
|
||||
'auto.components.settings.AppearancePane.75f07ab60c',
|
||||
'Show files matched by .gitignore in the file explorer.'
|
||||
)
|
||||
}
|
||||
keywords={getLayoutEntries()[0]?.keywords ?? ['git', 'gitignore', 'ignored']}
|
||||
>
|
||||
<SettingsSwitchRow
|
||||
label={translate(
|
||||
'auto.components.settings.AppearancePane.0fafabcf35',
|
||||
'Show Git-Ignored Files'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.e9f2ca5582',
|
||||
'Turn off to hide files matched by .gitignore from the file explorer.'
|
||||
)}
|
||||
checked={settings.showGitIgnoredFiles ?? true}
|
||||
onChange={() =>
|
||||
updateSettings({ showGitIgnoredFiles: !(settings.showGitIgnoredFiles ?? true) })
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
</div>
|
||||
</section>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, getTitlebarEntries()) ? (
|
||||
<section key="titlebar" className="space-y-3">
|
||||
<SettingsSubsectionHeader
|
||||
title={translate('auto.components.settings.AppearancePane.6a272ca553', 'Titlebar')}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.4de76f6902',
|
||||
'Control what appears in the application titlebar.'
|
||||
)}
|
||||
/>
|
||||
return (
|
||||
<div className="space-y-2.5">
|
||||
{interfaceMatches ? (
|
||||
<AppearanceSection
|
||||
id="interface"
|
||||
icon={<AppWindow aria-hidden="true" />}
|
||||
title={interfaceTitle}
|
||||
summary={interfaceSummary}
|
||||
open={isSectionOpen('interface')}
|
||||
onToggle={() => toggleSection('interface')}
|
||||
>
|
||||
<AppearanceInterfaceSection
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
applyTheme={applyTheme}
|
||||
fontSuggestions={fontSuggestions}
|
||||
isDesktopWindows={isDesktopWindows}
|
||||
forceVisiblePrimary={interfaceLabelMatches}
|
||||
/>
|
||||
</AppearanceSection>
|
||||
) : null}
|
||||
|
||||
<div className="divide-y divide-border/40">
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.AppearancePane.9868f39007',
|
||||
'Titlebar App Name'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.2df8f79aa5',
|
||||
'Show Orca in the titlebar.'
|
||||
)}
|
||||
keywords={getTitlebarEntries()[0]?.keywords ?? ['titlebar', 'orca', 'app', 'name']}
|
||||
>
|
||||
<SettingsSwitchRow
|
||||
label={translate(
|
||||
'auto.components.settings.AppearancePane.9868f39007',
|
||||
'Titlebar App Name'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.2df8f79aa5',
|
||||
'Show Orca in the titlebar.'
|
||||
)}
|
||||
checked={settings.showTitlebarAppName}
|
||||
onChange={() =>
|
||||
updateSettings({ showTitlebarAppName: !settings.showTitlebarAppName })
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
</div>
|
||||
</section>
|
||||
) : null,
|
||||
isDesktopWindows && matchesSettingsSearch(searchQuery, systemTrayEntries) ? (
|
||||
<section key="system-tray" className="space-y-3">
|
||||
<SettingsSubsectionHeader
|
||||
title={translate('auto.components.settings.AppearancePane.872af9556e', 'System Tray')}
|
||||
/>
|
||||
{/* Why: Code & Markdown is intentionally omitted. Orca has no Appearance-level
|
||||
code/markdown settings — the Monaco editor reuses the terminal font and
|
||||
there is no markdown-style or line-number setting — so a fourth row would
|
||||
be empty. We surface only the three sections that hold real controls
|
||||
rather than fabricate settings. */}
|
||||
|
||||
<div className="divide-y divide-border/40">
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.AppearancePane.2edf606c46',
|
||||
'Minimize to Tray on Close'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.b707773a0d',
|
||||
'When enabled, closing the window keeps Orca running in the system tray instead of quitting.'
|
||||
)}
|
||||
keywords={systemTrayEntries[0]?.keywords ?? ['tray', 'minimize', 'close']}
|
||||
>
|
||||
<SettingsSwitchRow
|
||||
label={translate(
|
||||
'auto.components.settings.AppearancePane.2edf606c46',
|
||||
'Minimize to Tray on Close'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.b707773a0d',
|
||||
'When enabled, closing the window keeps Orca running in the system tray instead of quitting.'
|
||||
)}
|
||||
checked={settings.minimizeToTrayOnClose === true}
|
||||
onChange={() =>
|
||||
updateSettings({ minimizeToTrayOnClose: !settings.minimizeToTrayOnClose })
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
</div>
|
||||
</section>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, getStatusBarEntries()) ? (
|
||||
<section key="status-bar" className="space-y-3">
|
||||
<SettingsSubsectionHeader
|
||||
title={translate('auto.components.settings.AppearancePane.3e4175e5c6', 'Status Bar')}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.ea943d0db0',
|
||||
'Choose which indicators appear at the bottom of the window. You can also right-click the status bar for the same toggles.'
|
||||
)}
|
||||
/>
|
||||
{terminalMatches ? (
|
||||
<AppearanceSection
|
||||
id="terminal"
|
||||
icon={<TerminalSquare aria-hidden="true" />}
|
||||
title={terminalTitle}
|
||||
summary={terminalSummary}
|
||||
open={isSectionOpen('terminal')}
|
||||
onToggle={() => toggleSection('terminal')}
|
||||
>
|
||||
<TerminalAppearanceSection
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
systemPrefersDark={systemPrefersDark}
|
||||
terminalFontSuggestions={terminalFontSuggestions}
|
||||
ghostty={ghostty}
|
||||
warpThemes={warpThemes}
|
||||
forceVisiblePrimary={terminalLabelMatches}
|
||||
/>
|
||||
</AppearanceSection>
|
||||
) : null}
|
||||
|
||||
<div className="divide-y divide-border/40">
|
||||
{visibleStatusBarToggles.map((toggle) => {
|
||||
const enabled = statusBarItems.includes(toggle.id)
|
||||
return (
|
||||
<SearchableSetting
|
||||
key={toggle.id}
|
||||
title={toggle.title}
|
||||
description={toggle.description}
|
||||
keywords={toggle.keywords}
|
||||
>
|
||||
<SettingsSwitchRow
|
||||
label={toggle.title}
|
||||
description={toggle.toggleDescription}
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
if (toggle.id === 'resource-usage') {
|
||||
recordFeatureInteraction('resource-manager')
|
||||
} else if (toggle.id === 'ports') {
|
||||
recordFeatureInteraction('ports')
|
||||
} else if (toggle.id === 'ssh') {
|
||||
recordFeatureInteraction('ssh')
|
||||
} else if (
|
||||
toggle.id === 'claude' ||
|
||||
toggle.id === 'codex' ||
|
||||
toggle.id === 'gemini' ||
|
||||
toggle.id === 'opencode-go'
|
||||
) {
|
||||
recordFeatureInteraction('usage-tracking')
|
||||
}
|
||||
toggleStatusBarItem(toggle.id)
|
||||
}}
|
||||
ariaLabel={toggle.title}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, getSidebarEntries()) ? (
|
||||
<section key="sidebar" className="space-y-3">
|
||||
<SettingsSubsectionHeader
|
||||
title={translate('auto.components.settings.AppearancePane.dc29f3cc0d', 'Sidebar')}
|
||||
/>
|
||||
{windowMatches ? (
|
||||
<AppearanceSection
|
||||
id="window"
|
||||
icon={<PanelLeft aria-hidden="true" />}
|
||||
title={windowSidebarTitle}
|
||||
summary={windowSidebarSummary}
|
||||
open={isSectionOpen('window')}
|
||||
onToggle={() => toggleSection('window')}
|
||||
>
|
||||
<AppearanceWindowSidebarSection
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
forceVisiblePrimary={windowLabelMatches}
|
||||
/>
|
||||
</AppearanceSection>
|
||||
) : null}
|
||||
|
||||
<div className="divide-y divide-border/40">
|
||||
<SearchableSetting
|
||||
title={leftSidebarAppearanceEntry.title}
|
||||
description={leftSidebarAppearanceEntry.description}
|
||||
keywords={leftSidebarAppearanceEntry.keywords}
|
||||
className="space-y-2"
|
||||
>
|
||||
<LeftSidebarAppearanceSetting settings={settings} updateSettings={updateSettings} />
|
||||
</SearchableSetting>
|
||||
|
||||
{/* Why: this setting lives with the sidebar layout controls; Settings only
|
||||
points people to it so we do not create a second stateful control. */}
|
||||
<SearchableSetting
|
||||
title={workspaceCardLayoutEntry.title}
|
||||
description={workspaceCardLayoutEntry.description}
|
||||
keywords={workspaceCardLayoutEntry.keywords}
|
||||
>
|
||||
<SettingsRow
|
||||
label={workspaceCardLayoutEntry.title}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.workspaceCardLayoutGuidance',
|
||||
'Use the workspace sidebar options menu > Card layout > Compact.'
|
||||
)}
|
||||
control={null}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.AppearancePane.cf81907069',
|
||||
'Show Tasks Button'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.661942ab7f',
|
||||
'Show the Tasks button at the top of the left sidebar.'
|
||||
)}
|
||||
keywords={getSidebarEntries()[0]?.keywords ?? ['tasks', 'sidebar', 'button']}
|
||||
>
|
||||
<SettingsSwitchRow
|
||||
label={translate(
|
||||
'auto.components.settings.AppearancePane.cf81907069',
|
||||
'Show Tasks Button'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.661942ab7f',
|
||||
'Show the Tasks button at the top of the left sidebar.'
|
||||
)}
|
||||
checked={settings.showTasksButton !== false}
|
||||
onChange={() =>
|
||||
updateSettings({ showTasksButton: !(settings.showTasksButton !== false) })
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.AppearancePane.511f270ebb',
|
||||
'Show Automations Button'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.fa882a3e6b',
|
||||
'Show the Automations button at the top of the left sidebar.'
|
||||
)}
|
||||
keywords={getSidebarEntries()[1]?.keywords ?? ['automations', 'automation', 'schedule']}
|
||||
>
|
||||
<SettingsSwitchRow
|
||||
label={translate(
|
||||
'auto.components.settings.AppearancePane.511f270ebb',
|
||||
'Show Automations Button'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.fa882a3e6b',
|
||||
'Show the Automations button at the top of the left sidebar.'
|
||||
)}
|
||||
checked={settings.showAutomationsButton !== false}
|
||||
onChange={() =>
|
||||
updateSettings({
|
||||
showAutomationsButton: !(settings.showAutomationsButton !== false)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.AppearancePane.9da1020447',
|
||||
'Show Orca Mobile Button'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.5db6ba961f',
|
||||
'Show the Orca Mobile button at the top of the left sidebar.'
|
||||
)}
|
||||
keywords={getSidebarEntries()[2]?.keywords ?? ['mobile', 'phone', 'sidebar']}
|
||||
>
|
||||
<SettingsSwitchRow
|
||||
label={translate(
|
||||
'auto.components.settings.AppearancePane.9da1020447',
|
||||
'Show Orca Mobile Button'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.61d842eca0',
|
||||
'Show the Orca Mobile shortcut in the sidebar. It remains available from Toolbox.'
|
||||
)}
|
||||
checked={settings.showMobileButton !== false}
|
||||
onChange={() =>
|
||||
updateSettings({ showMobileButton: !(settings.showMobileButton !== false) })
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
</div>
|
||||
</section>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, getAppIconEntries()) ? (
|
||||
<section key="app-icon" className="space-y-3">
|
||||
{/* App icon stays at the bottom of Appearance as a small easter egg,
|
||||
matching production — not buried inside Interface advanced. */}
|
||||
{appIconMatches ? (
|
||||
<SearchableSetting
|
||||
title={translate('auto.components.settings.AppearancePane.ca1590d42f', 'App Icon')}
|
||||
description={translate(
|
||||
|
|
@ -585,25 +243,14 @@ export function AppearancePane({
|
|||
entry.description ?? '',
|
||||
...(entry.keywords ?? [])
|
||||
])}
|
||||
className="max-w-none py-2"
|
||||
className="max-w-none px-1 pt-2"
|
||||
>
|
||||
<AppIconSelector
|
||||
value={normalizeAppIconId(settings.appIcon)}
|
||||
onChange={(appIcon) => updateSettings({ appIcon })}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
</section>
|
||||
) : null
|
||||
].filter(Boolean)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{visibleSections.map((section, index) => (
|
||||
<div key={index} className="space-y-6">
|
||||
{index > 0 ? <Separator /> : null}
|
||||
{section}
|
||||
</div>
|
||||
))}
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
import type React from 'react'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type AppearanceSectionProps = {
|
||||
/** Stable id used for the accordion toggle + aria wiring. */
|
||||
id: string
|
||||
icon: React.ReactNode
|
||||
title: React.ReactNode
|
||||
/** Plain-language current value shown in the collapsed summary row. */
|
||||
summary: React.ReactNode
|
||||
open: boolean
|
||||
onToggle: () => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/** Compact summary row that expands its section inline. The parent owns the
|
||||
* open state so opening one row can collapse the previously open one
|
||||
* (accordion behavior) and search can force a section open. */
|
||||
export function AppearanceSection({
|
||||
id,
|
||||
icon,
|
||||
title,
|
||||
summary,
|
||||
open,
|
||||
onToggle,
|
||||
children
|
||||
}: AppearanceSectionProps): React.JSX.Element {
|
||||
const contentId = `appearance-section-${id}`
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'overflow-hidden rounded-xl border border-border/50 bg-card transition-colors',
|
||||
open && 'border-ring/40'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
aria-controls={contentId}
|
||||
onClick={onToggle}
|
||||
className="flex w-full items-center gap-3.5 px-4 py-3.5 text-left transition-colors hover:bg-accent/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
||||
>
|
||||
<span className="grid size-8 shrink-0 place-items-center rounded-md bg-secondary text-foreground [&_svg]:size-4">
|
||||
{icon}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-sm font-semibold">{title}</span>
|
||||
{!open ? (
|
||||
<span className="block truncate text-xs text-muted-foreground">{summary}</span>
|
||||
) : null}
|
||||
</span>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'size-[18px] shrink-0 text-muted-foreground transition-transform',
|
||||
open && 'rotate-90 text-foreground'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
className={cn(
|
||||
'grid overflow-hidden transition-[grid-template-rows,opacity,border-color] duration-200 ease-out motion-reduce:transition-none',
|
||||
open
|
||||
? 'grid-rows-[1fr] border-t border-border/50 opacity-100'
|
||||
: 'grid-rows-[0fr] border-t border-transparent opacity-0'
|
||||
)}
|
||||
aria-hidden={!open}
|
||||
inert={!open}
|
||||
>
|
||||
<div className="min-h-0 overflow-hidden">
|
||||
<div id={contentId} role="region" className="px-4 pt-1 pb-4">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import type React from 'react'
|
||||
|
||||
import { type ShortcutKeyComboDetails } from '@/hooks/useShortcutLabel'
|
||||
import { ShortcutKeyCombo } from '../ShortcutKeyCombo'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
/** Renders the primary keyboard shortcut combo inline, or an "Unassigned"
|
||||
* hint when the action has no binding. Platform-aware glyphs come from
|
||||
* ShortcutKeyCombo. */
|
||||
export function ShortcutHintList({
|
||||
combos
|
||||
}: {
|
||||
combos: ShortcutKeyComboDetails[]
|
||||
}): React.JSX.Element {
|
||||
if (combos.length === 0) {
|
||||
return (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{translate('auto.components.settings.AppearancePane.3057983501', 'Unassigned')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
const primaryCombo = combos[0]
|
||||
|
||||
return (
|
||||
<span className="inline-flex items-center align-middle">
|
||||
<ShortcutKeyCombo
|
||||
keys={primaryCombo.keys}
|
||||
doubleTap={primaryCombo.doubleTap}
|
||||
className="inline-flex gap-0.5"
|
||||
separatorClassName="text-[10px] text-muted-foreground"
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,307 @@
|
|||
import type React from 'react'
|
||||
|
||||
import type { GlobalSettings, StatusBarItem } from '../../../../shared/types'
|
||||
import type { FeatureInteractionId } from '../../../../shared/feature-interaction-catalog'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
import { AppearanceAdvancedDisclosure } from './AppearanceAdvancedDisclosure'
|
||||
import { useAppStore } from '../../store'
|
||||
import {
|
||||
SettingsRow,
|
||||
SettingsSegmentedControl,
|
||||
SettingsSubsectionHeader,
|
||||
SettingsSwitchRow
|
||||
} from './SettingsFormControls'
|
||||
import { useAvailableStatusBarToggles } from '../status-bar/use-available-status-bar-toggles'
|
||||
import { getLayoutEntries, getSidebarEntries, getStatusBarToggles } from './appearance-search'
|
||||
import { LeftSidebarAppearanceSetting } from './LeftSidebarAppearanceSetting'
|
||||
import {
|
||||
getLeftSidebarAppearanceEntry,
|
||||
getWorkspaceCardLayoutEntry
|
||||
} from './appearance-sidebar-search'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { matchesSettingsSearch, normalizeSettingsSearchQuery } from './settings-search'
|
||||
|
||||
type AppearanceWindowSidebarSectionProps = {
|
||||
settings: GlobalSettings
|
||||
updateSettings: (updates: Partial<GlobalSettings>) => void
|
||||
forceVisiblePrimary?: boolean
|
||||
}
|
||||
|
||||
function recordStatusBarToggleInteraction(
|
||||
id: StatusBarItem,
|
||||
recordFeatureInteraction: (feature: FeatureInteractionId) => void
|
||||
): void {
|
||||
if (id === 'resource-usage') {
|
||||
recordFeatureInteraction('resource-manager')
|
||||
} else if (id === 'ports') {
|
||||
recordFeatureInteraction('ports')
|
||||
} else if (id === 'ssh') {
|
||||
recordFeatureInteraction('ssh')
|
||||
} else if (id === 'claude' || id === 'codex' || id === 'gemini' || id === 'opencode-go') {
|
||||
recordFeatureInteraction('usage-tracking')
|
||||
}
|
||||
}
|
||||
|
||||
export function AppearanceWindowSidebarSection({
|
||||
settings,
|
||||
updateSettings,
|
||||
forceVisiblePrimary = false
|
||||
}: AppearanceWindowSidebarSectionProps): React.JSX.Element {
|
||||
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
|
||||
const isSearching = normalizeSettingsSearchQuery(searchQuery).length > 0
|
||||
const statusBarItems = useAppStore((state) => state.statusBarItems)
|
||||
const toggleStatusBarItem = useAppStore((state) => state.toggleStatusBarItem)
|
||||
const recordFeatureInteraction = useAppStore((state) => state.recordFeatureInteraction)
|
||||
const setWorktreeCardMode = useAppStore((state) => state.setWorktreeCardMode)
|
||||
const visibleStatusBarToggles = useAvailableStatusBarToggles(getStatusBarToggles())
|
||||
const leftSidebarAppearanceEntry = getLeftSidebarAppearanceEntry()
|
||||
const sidebarEntries = getSidebarEntries()
|
||||
const workspaceCardLayoutEntry = getWorkspaceCardLayoutEntry()
|
||||
const layoutEntries = getLayoutEntries()
|
||||
const statusBarTitle = translate(
|
||||
'auto.components.settings.AppearancePane.3e4175e5c6',
|
||||
'Status Bar'
|
||||
)
|
||||
const statusBarDescription = translate(
|
||||
'auto.components.settings.AppearancePane.statusBarDescription',
|
||||
'Choose which indicators appear in the status bar.'
|
||||
)
|
||||
const statusBarKeywords = ['status bar', 'indicators']
|
||||
const statusBarSectionMatches = matchesSettingsSearch(searchQuery, {
|
||||
title: statusBarTitle,
|
||||
description: statusBarDescription,
|
||||
keywords: statusBarKeywords
|
||||
})
|
||||
const statusBarControlMatches = visibleStatusBarToggles.some((toggle) =>
|
||||
matchesSettingsSearch(searchQuery, {
|
||||
title: toggle.title,
|
||||
description: toggle.description,
|
||||
keywords: toggle.keywords
|
||||
})
|
||||
)
|
||||
const sidebarAdvancedMatches = matchesSettingsSearch(searchQuery, [
|
||||
workspaceCardLayoutEntry,
|
||||
...sidebarEntries
|
||||
])
|
||||
const fileExplorerAdvancedMatches = matchesSettingsSearch(searchQuery, layoutEntries)
|
||||
const showStatusBarControls = !isSearching || statusBarSectionMatches || statusBarControlMatches
|
||||
const showSidebarAdvanced = !isSearching || sidebarAdvancedMatches
|
||||
const showFileExplorerAdvanced = !isSearching || fileExplorerAdvancedMatches
|
||||
const showAdvanced = showSidebarAdvanced || showFileExplorerAdvanced
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="divide-y divide-border/40">
|
||||
<SearchableSetting
|
||||
title={leftSidebarAppearanceEntry.title}
|
||||
description={leftSidebarAppearanceEntry.description}
|
||||
keywords={leftSidebarAppearanceEntry.keywords}
|
||||
className="space-y-2"
|
||||
forceVisible={forceVisiblePrimary}
|
||||
>
|
||||
<LeftSidebarAppearanceSetting settings={settings} updateSettings={updateSettings} />
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={statusBarTitle}
|
||||
keywords={statusBarKeywords}
|
||||
forceVisible={forceVisiblePrimary || statusBarSectionMatches || statusBarControlMatches}
|
||||
>
|
||||
<SettingsRow label={statusBarTitle} description={statusBarDescription} control={null} />
|
||||
{showStatusBarControls ? (
|
||||
<div className="ml-4 divide-y divide-border/40 border-t border-border/40">
|
||||
{visibleStatusBarToggles.map((toggle) => {
|
||||
const enabled = statusBarItems.includes(toggle.id)
|
||||
return (
|
||||
<SearchableSetting
|
||||
key={toggle.id}
|
||||
title={toggle.title}
|
||||
description={toggle.description}
|
||||
keywords={toggle.keywords}
|
||||
>
|
||||
<SettingsSwitchRow
|
||||
label={toggle.title}
|
||||
description={toggle.toggleDescription}
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
recordStatusBarToggleInteraction(toggle.id, recordFeatureInteraction)
|
||||
toggleStatusBarItem(toggle.id)
|
||||
}}
|
||||
ariaLabel={toggle.title}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</SearchableSetting>
|
||||
</div>
|
||||
|
||||
{showAdvanced ? (
|
||||
<AppearanceAdvancedDisclosure contentClassName="ml-4 pt-4">
|
||||
<div className="space-y-4">
|
||||
{showSidebarAdvanced ? (
|
||||
<div className="space-y-3">
|
||||
<SettingsSubsectionHeader
|
||||
title={translate('auto.components.settings.AppearancePane.dc29f3cc0d', 'Sidebar')}
|
||||
/>
|
||||
<div className="ml-4 divide-y divide-border/40">
|
||||
{/* Why: this setting lives with the sidebar layout controls; Settings only
|
||||
names that ownership so we do not create a second stateful control. */}
|
||||
<SearchableSetting
|
||||
title={workspaceCardLayoutEntry.title}
|
||||
description={workspaceCardLayoutEntry.description}
|
||||
keywords={workspaceCardLayoutEntry.keywords}
|
||||
>
|
||||
<SettingsRow
|
||||
label={workspaceCardLayoutEntry.title}
|
||||
description={workspaceCardLayoutEntry.description}
|
||||
control={
|
||||
<SettingsSegmentedControl
|
||||
value={settings.compactWorktreeCards ? 'compact' : 'detailed'}
|
||||
onChange={(value) =>
|
||||
setWorktreeCardMode(value === 'compact' ? 'Compact' : 'Default')
|
||||
}
|
||||
ariaLabel={workspaceCardLayoutEntry.title}
|
||||
options={[
|
||||
{
|
||||
value: 'detailed',
|
||||
label: translate(
|
||||
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.cc17bd443b',
|
||||
'Detailed'
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'compact',
|
||||
label: translate(
|
||||
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.25105b28cb',
|
||||
'Compact'
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.AppearancePane.cf81907069',
|
||||
'Show Tasks Button'
|
||||
)}
|
||||
description={sidebarEntries[0]?.description}
|
||||
keywords={sidebarEntries[0]?.keywords ?? ['tasks', 'sidebar', 'button']}
|
||||
>
|
||||
<SettingsSwitchRow
|
||||
label={translate(
|
||||
'auto.components.settings.AppearancePane.cf81907069',
|
||||
'Show Tasks Button'
|
||||
)}
|
||||
checked={settings.showTasksButton !== false}
|
||||
onChange={() =>
|
||||
updateSettings({ showTasksButton: !(settings.showTasksButton !== false) })
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.AppearancePane.511f270ebb',
|
||||
'Show Automations Button'
|
||||
)}
|
||||
description={sidebarEntries[1]?.description}
|
||||
keywords={
|
||||
sidebarEntries[1]?.keywords ?? ['automations', 'automation', 'schedule']
|
||||
}
|
||||
>
|
||||
<SettingsSwitchRow
|
||||
label={translate(
|
||||
'auto.components.settings.AppearancePane.511f270ebb',
|
||||
'Show Automations Button'
|
||||
)}
|
||||
checked={settings.showAutomationsButton !== false}
|
||||
onChange={() =>
|
||||
updateSettings({
|
||||
showAutomationsButton: !(settings.showAutomationsButton !== false)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.AppearancePane.9da1020447',
|
||||
'Show Orca Mobile Button'
|
||||
)}
|
||||
description={sidebarEntries[2]?.description}
|
||||
keywords={sidebarEntries[2]?.keywords ?? ['mobile', 'phone', 'sidebar']}
|
||||
>
|
||||
<SettingsSwitchRow
|
||||
label={translate(
|
||||
'auto.components.settings.AppearancePane.9da1020447',
|
||||
'Show Orca Mobile Button'
|
||||
)}
|
||||
// Why: clarify where the shortcut still lives after hiding it, so users
|
||||
// don't think the feature is gone.
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.61d842eca0',
|
||||
'Show the Orca Mobile shortcut in the sidebar. It remains available from Toolbox.'
|
||||
)}
|
||||
checked={settings.showMobileButton !== false}
|
||||
onChange={() =>
|
||||
updateSettings({ showMobileButton: !(settings.showMobileButton !== false) })
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showFileExplorerAdvanced ? (
|
||||
<div className="space-y-3">
|
||||
<SettingsSubsectionHeader
|
||||
title={translate(
|
||||
'auto.components.settings.AppearancePane.d496901cd0',
|
||||
'File Explorer'
|
||||
)}
|
||||
/>
|
||||
<div className="ml-4 divide-y divide-border/40">
|
||||
<SearchableSetting
|
||||
title={
|
||||
layoutEntries[0]?.title ??
|
||||
translate(
|
||||
'auto.components.settings.AppearancePane.0fafabcf35',
|
||||
'Show Git-Ignored Files'
|
||||
)
|
||||
}
|
||||
description={layoutEntries[0]?.description}
|
||||
keywords={layoutEntries[0]?.keywords ?? ['git', 'gitignore', 'ignored']}
|
||||
>
|
||||
<SettingsSwitchRow
|
||||
label={translate(
|
||||
'auto.components.settings.AppearancePane.0fafabcf35',
|
||||
'Show Git-Ignored Files'
|
||||
)}
|
||||
// Why: define what "git-ignored" matches; the location (file explorer)
|
||||
// is obvious from the section header.
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.gitIgnoredGlossary',
|
||||
'Files matched by .gitignore.'
|
||||
)}
|
||||
checked={settings.showGitIgnoredFiles ?? true}
|
||||
onChange={() =>
|
||||
updateSettings({
|
||||
showGitIgnoredFiles: !(settings.showGitIgnoredFiles ?? true)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</AppearanceAdvancedDisclosure>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -76,11 +76,19 @@ export function SettingsRow({
|
|||
}: SettingsRowProps): React.JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className={cn('flex gap-4 py-2', alignTop ? 'items-start' : 'items-center justify-between')}
|
||||
className={cn(
|
||||
'flex gap-4',
|
||||
description ? 'py-3' : 'py-2',
|
||||
alignTop ? 'items-start' : 'items-center justify-between'
|
||||
)}
|
||||
>
|
||||
<div className="min-w-0 flex-1 space-y-0.5">
|
||||
<Label id={labelId}>{label}</Label>
|
||||
{description ? <p className="text-xs text-muted-foreground">{description}</p> : null}
|
||||
<div className={cn('min-w-0 flex-1', description ? 'space-y-1' : 'space-y-0.5')}>
|
||||
<Label id={labelId} className="select-text">
|
||||
{label}
|
||||
</Label>
|
||||
{description ? (
|
||||
<p className="select-text text-xs text-muted-foreground">{description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="shrink-0">{control}</div>
|
||||
</div>
|
||||
|
|
@ -218,16 +226,18 @@ type SettingsSubsectionHeaderProps = {
|
|||
title: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
/** Consistent subsection header: h3 text-sm font-semibold + optional muted description. */
|
||||
export function SettingsSubsectionHeader({
|
||||
title,
|
||||
description,
|
||||
action
|
||||
action,
|
||||
className
|
||||
}: SettingsSubsectionHeaderProps): React.JSX.Element {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className={cn('flex items-start justify-between gap-3', className)}>
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-semibold">{title}</h3>
|
||||
{description ? <p className="text-xs text-muted-foreground">{description}</p> : null}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
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 { NumberField, SettingsRow, SettingsSegmentedControl } from './SettingsFormControls'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
import { clampNumber } from '@/lib/terminal-theme'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { getTerminalAdvancedTypographySearchEntries } from './terminal-typography-search'
|
||||
|
||||
type TerminalAdvancedTypographyControlsProps = {
|
||||
settings: GlobalSettings
|
||||
updateSettings: (updates: Partial<GlobalSettings>) => void
|
||||
}
|
||||
|
||||
/** Low-frequency terminal typography knobs (weight, line height, ligatures).
|
||||
* Split out of the primary font controls so the default Terminal scan stays
|
||||
* compact while these stay searchable inside the Advanced disclosure. */
|
||||
export function TerminalAdvancedTypographyControls({
|
||||
settings,
|
||||
updateSettings
|
||||
}: TerminalAdvancedTypographyControlsProps): React.JSX.Element {
|
||||
const searchEntries = getTerminalAdvancedTypographySearchEntries()
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-border/40">
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.4aae5db258',
|
||||
'Font Weight'
|
||||
)}
|
||||
description={searchEntries[0]?.description}
|
||||
keywords={searchEntries[0]?.keywords ?? ['terminal', 'typography', 'weight']}
|
||||
>
|
||||
<NumberField
|
||||
label={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.4aae5db258',
|
||||
'Font Weight'
|
||||
)}
|
||||
description=""
|
||||
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={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.c084eb7d4c',
|
||||
'Line Height'
|
||||
)}
|
||||
description={searchEntries[1]?.description}
|
||||
keywords={
|
||||
searchEntries[1]?.keywords ?? ['terminal', 'typography', 'line height', 'spacing']
|
||||
}
|
||||
>
|
||||
<NumberField
|
||||
label={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.c084eb7d4c',
|
||||
'Line Height'
|
||||
)}
|
||||
description=""
|
||||
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={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.be8da35e7f',
|
||||
'Font Ligatures'
|
||||
)}
|
||||
description={searchEntries[2]?.description}
|
||||
keywords={
|
||||
searchEntries[2]?.keywords ?? [
|
||||
'terminal',
|
||||
'typography',
|
||||
'ligatures',
|
||||
'ligature',
|
||||
'fira code',
|
||||
'jetbrains mono',
|
||||
'cascadia code',
|
||||
'iosevka',
|
||||
'calt',
|
||||
'font features'
|
||||
]
|
||||
}
|
||||
>
|
||||
<SettingsRow
|
||||
label={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.be8da35e7f',
|
||||
'Font Ligatures'
|
||||
)}
|
||||
// Why: "ligatures" is jargon; the per-state gloss tells the user whether
|
||||
// Auto resolves on/off for their current font.
|
||||
description={
|
||||
settings.terminalLigatures === 'on'
|
||||
? translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.7234abcd08',
|
||||
'Always on. Fonts without ligatures simply render as-is.'
|
||||
)
|
||||
: settings.terminalLigatures === 'off'
|
||||
? translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.04569feb07',
|
||||
'Always off, even for fonts that ship them.'
|
||||
)
|
||||
: fontFamilyHasKnownLigatures(settings.terminalFontFamily)
|
||||
? translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.400e950ca5',
|
||||
'Auto - enabled for "{{value0}}".',
|
||||
{ value0: settings.terminalFontFamily }
|
||||
)
|
||||
: translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.4b1f29598e',
|
||||
'Auto - disabled for "{{value0}}".',
|
||||
{ value0: settings.terminalFontFamily || 'the current font' }
|
||||
)
|
||||
}
|
||||
control={
|
||||
<SettingsSegmentedControl
|
||||
ariaLabel={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.be8da35e7f',
|
||||
'Font Ligatures'
|
||||
)}
|
||||
value={settings.terminalLigatures ?? 'auto'}
|
||||
onChange={(option) => updateSettings({ terminalLigatures: option })}
|
||||
options={[
|
||||
{
|
||||
value: 'auto',
|
||||
label: translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.bc9ff84d61',
|
||||
'Auto'
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'on',
|
||||
label: translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.84bd22f2cd',
|
||||
'On'
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'off',
|
||||
label: translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.870377082f',
|
||||
'Off'
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<p className="sr-only" aria-live="polite">
|
||||
{translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.31f6e61085',
|
||||
'Ligatures are currently'
|
||||
)}{' '}
|
||||
{resolveTerminalLigaturesEnabled(settings.terminalLigatures, settings.terminalFontFamily)
|
||||
? translate('auto.components.settings.TerminalAppearanceSection.4e7d41a9f0', 'enabled')
|
||||
: translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.4415beb958',
|
||||
'disabled'
|
||||
)}
|
||||
.
|
||||
</p>
|
||||
</SearchableSetting>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -97,8 +97,8 @@ vi.mock('./SettingsFormControls', () => ({
|
|||
}) {
|
||||
return options?.map((option) => option.label) ?? null
|
||||
},
|
||||
SettingsSubsectionHeader: function SettingsSubsectionHeader() {
|
||||
return null
|
||||
SettingsSubsectionHeader: function SettingsSubsectionHeader({ action }: { action?: unknown }) {
|
||||
return action ?? null
|
||||
},
|
||||
SettingsSwitchRow: function SettingsSwitchRow() {
|
||||
return null
|
||||
|
|
@ -322,6 +322,30 @@ function findWarpThemeImportModal(node: unknown): ReactElementLike | null {
|
|||
return null
|
||||
}
|
||||
|
||||
function findComponentByTypeName(node: unknown, targetTypeName: string): ReactElementLike | null {
|
||||
if (node == null) {
|
||||
return null
|
||||
}
|
||||
if (Array.isArray(node)) {
|
||||
for (const child of node) {
|
||||
const found = findComponentByTypeName(child, targetTypeName)
|
||||
if (found) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
const el = node as ReactElementLike
|
||||
const typeName = typeof el.type === 'function' ? el.type.name : String(el.type)
|
||||
if (typeName === targetTypeName) {
|
||||
return el
|
||||
}
|
||||
if (el.props?.children) {
|
||||
return findComponentByTypeName(el.props.children, targetTypeName)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
describe('TerminalAppearanceSection ghostty import wiring', () => {
|
||||
beforeEach(() => {
|
||||
mockStateValues.length = 0
|
||||
|
|
@ -421,6 +445,68 @@ describe('TerminalAppearanceSection ghostty import wiring', () => {
|
|||
expect(findTerminalThemeCatalogSection(darkPhraseElement)?.props.preferredTarget).toBe('dark')
|
||||
})
|
||||
|
||||
it('does not open advanced typography for primary terminal font searches', () => {
|
||||
mockSettingsSearchQuery = 'font size'
|
||||
|
||||
const element = TerminalAppearanceSection({
|
||||
settings: {} as never,
|
||||
updateSettings: () => {},
|
||||
systemPrefersDark: true,
|
||||
terminalFontSuggestions: [],
|
||||
ghostty: ghosttyMock,
|
||||
warpThemes: warpThemesMock
|
||||
})
|
||||
|
||||
expect(findComponentByTypeName(element, 'TerminalAdvancedTypographyControls')).toBeNull()
|
||||
})
|
||||
|
||||
it('does not show primary typography chrome for unrelated terminal searches', () => {
|
||||
mockSettingsSearchQuery = 'cursor opacity'
|
||||
|
||||
const element = TerminalAppearanceSection({
|
||||
settings: {} as never,
|
||||
updateSettings: () => {},
|
||||
systemPrefersDark: true,
|
||||
terminalFontSuggestions: [],
|
||||
ghostty: ghosttyMock,
|
||||
warpThemes: warpThemesMock
|
||||
})
|
||||
|
||||
expect(findComponentByTypeName(element, 'TerminalFontSizeSetting')).toBeNull()
|
||||
expect(findButtons(element).some((button) => button.text === 'Import from Ghostty')).toBe(false)
|
||||
})
|
||||
|
||||
it('shows the Ghostty import button for Ghostty-only searches', () => {
|
||||
mockSettingsSearchQuery = 'ghostty'
|
||||
|
||||
const element = TerminalAppearanceSection({
|
||||
settings: {} as never,
|
||||
updateSettings: () => {},
|
||||
systemPrefersDark: true,
|
||||
terminalFontSuggestions: [],
|
||||
ghostty: ghosttyMock,
|
||||
warpThemes: warpThemesMock
|
||||
})
|
||||
|
||||
expect(findButtons(element).some((button) => button.text === 'Import from Ghostty')).toBe(true)
|
||||
})
|
||||
|
||||
it('opens typography advanced inside the typography section for advanced searches', () => {
|
||||
mockSettingsSearchQuery = 'line height'
|
||||
|
||||
const element = TerminalAppearanceSection({
|
||||
settings: {} as never,
|
||||
updateSettings: () => {},
|
||||
systemPrefersDark: true,
|
||||
terminalFontSuggestions: [],
|
||||
ghostty: ghosttyMock,
|
||||
warpThemes: warpThemesMock
|
||||
})
|
||||
|
||||
expect(findComponentByTypeName(element, 'TerminalAdvancedTypographyControls')).not.toBeNull()
|
||||
expect(findComponentByTypeName(element, 'TerminalFontSizeSetting')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('hides the theme import affordance on paired web clients', () => {
|
||||
vi.stubGlobal('window', {
|
||||
__ORCA_WEB_CLIENT__: true,
|
||||
|
|
|
|||
|
|
@ -2,32 +2,42 @@ import { useState } from 'react'
|
|||
import type { GlobalSettings } from '../../../../shared/types'
|
||||
import {
|
||||
matchesSettingsSearch,
|
||||
normalizeSettingsSearchQuery,
|
||||
scoreSettingsSearch,
|
||||
type SettingsSearchEntry
|
||||
} from './settings-search'
|
||||
import { useAppStore } from '../../store'
|
||||
import {
|
||||
getTerminalAdvancedTypographySearchEntries,
|
||||
getTerminalCursorSearchEntries,
|
||||
getTerminalDarkThemeSearchEntries,
|
||||
getTerminalGhosttyImportSearchEntries,
|
||||
getTerminalLightThemeSearchEntries,
|
||||
getTerminalPaneAppearanceSearchEntries,
|
||||
getTerminalThemeTargetSearchEntries,
|
||||
getTerminalTypographySearchEntries,
|
||||
getTerminalWarpImportSearchEntries,
|
||||
getTerminalWindowSearchEntries,
|
||||
getTerminalYamlImportSearchEntries
|
||||
getTerminalYamlImportSearchEntries,
|
||||
getTerminalTypographySearchEntries,
|
||||
getTerminalWindowSearchEntries
|
||||
} from './terminal-search'
|
||||
import { Button } from '../ui/button'
|
||||
import { SettingsRow, SettingsSubsectionHeader } from './SettingsFormControls'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
import { FontAutocomplete } from './SettingsFormControls'
|
||||
import { TerminalFontSizeSetting } from './TerminalFontSizeSetting'
|
||||
import { TerminalAdvancedTypographyControls } from './TerminalAdvancedTypographyControls'
|
||||
import { TerminalThemeCatalogSection } from './TerminalThemeSections'
|
||||
import { TerminalWindowSection } from './TerminalWindowSection'
|
||||
import { TerminalTypographyAppearanceSection } from './TerminalTypographyAppearanceSection'
|
||||
import { TerminalCursorAppearanceSection } from './TerminalCursorAppearanceSection'
|
||||
import { TerminalPaneAppearanceSection } from './TerminalPaneAppearanceSection'
|
||||
import { AppearanceAdvancedDisclosure } from './AppearanceAdvancedDisclosure'
|
||||
import { GhosttyImportModal } from './GhosttyImportModal'
|
||||
import type { UseGhosttyImportReturn } from './useGhosttyImport'
|
||||
import { WarpThemeImportModal } from './WarpThemeImportModal'
|
||||
import type { UseWarpThemeImportReturn } from './useWarpThemeImport'
|
||||
import { isWebClientLocation } from '@/hooks/useSettingsNavigationMetadata'
|
||||
import ghosttyIcon from '../../../../../resources/ghostty.svg'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
type TerminalAppearanceSectionProps = {
|
||||
settings: GlobalSettings
|
||||
|
|
@ -36,6 +46,7 @@ type TerminalAppearanceSectionProps = {
|
|||
terminalFontSuggestions: string[]
|
||||
ghostty: UseGhosttyImportReturn
|
||||
warpThemes: UseWarpThemeImportReturn
|
||||
forceVisiblePrimary?: boolean
|
||||
}
|
||||
|
||||
type TerminalThemeTarget = 'dark' | 'light'
|
||||
|
|
@ -64,88 +75,194 @@ export function TerminalAppearanceSection({
|
|||
systemPrefersDark,
|
||||
terminalFontSuggestions,
|
||||
ghostty,
|
||||
warpThemes
|
||||
warpThemes,
|
||||
forceVisiblePrimary = false
|
||||
}: TerminalAppearanceSectionProps): React.JSX.Element {
|
||||
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
|
||||
const isSearching = normalizeSettingsSearchQuery(searchQuery).length > 0
|
||||
const [themeSearch, setThemeSearch] = useState('')
|
||||
const [previewFontFamily, setPreviewFontFamily] = useState<string | null>(null)
|
||||
const showWarpThemeImport = !isWebClientLocation()
|
||||
const darkThemeSearchEntries = getTerminalDarkThemeSearchEntries()
|
||||
const lightThemeSearchEntries = getTerminalLightThemeSearchEntries()
|
||||
const darkThemeSearchScore = scoreSettingsSearch(searchQuery, darkThemeSearchEntries)
|
||||
const lightThemeSearchScore = scoreSettingsSearch(searchQuery, lightThemeSearchEntries)
|
||||
const terminalTypographyEntries = getTerminalTypographySearchEntries()
|
||||
const ghosttyImportEntries = getTerminalGhosttyImportSearchEntries()
|
||||
const themeCatalogSearchEntries = [
|
||||
...getTerminalThemeTargetSearchEntries(),
|
||||
...darkThemeSearchEntries,
|
||||
...lightThemeSearchEntries,
|
||||
...(showWarpThemeImport
|
||||
? [...getTerminalWarpImportSearchEntries(), ...getTerminalYamlImportSearchEntries()]
|
||||
: [])
|
||||
]
|
||||
const darkThemeTargetScore = scoreThemeTargetIntent(searchQuery, darkThemeSearchEntries)
|
||||
const lightThemeTargetScore = scoreThemeTargetIntent(searchQuery, lightThemeSearchEntries)
|
||||
const darkThemeMatches = darkThemeSearchScore > 0
|
||||
const lightThemeMatches = lightThemeSearchScore > 0
|
||||
const themeTargetMatches = matchesSettingsSearch(
|
||||
searchQuery,
|
||||
getTerminalThemeTargetSearchEntries()
|
||||
)
|
||||
const themeImportMatches =
|
||||
showWarpThemeImport &&
|
||||
(matchesSettingsSearch(searchQuery, getTerminalWarpImportSearchEntries()) ||
|
||||
matchesSettingsSearch(searchQuery, getTerminalYamlImportSearchEntries()))
|
||||
const showTerminalThemeCatalog =
|
||||
darkThemeMatches || lightThemeMatches || themeTargetMatches || themeImportMatches
|
||||
const preferredThemeTarget = getPreferredThemeTarget(darkThemeTargetScore, lightThemeTargetScore)
|
||||
|
||||
const visibleSections = [
|
||||
matchesSettingsSearch(searchQuery, getTerminalGhosttyImportSearchEntries()) ||
|
||||
matchesSettingsSearch(searchQuery, getTerminalTypographySearchEntries()) ? (
|
||||
<TerminalTypographyAppearanceSection
|
||||
key="typography"
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
systemPrefersDark={systemPrefersDark}
|
||||
terminalFontSuggestions={terminalFontSuggestions}
|
||||
ghostty={ghostty}
|
||||
previewFontFamily={previewFontFamily}
|
||||
setPreviewFontFamily={setPreviewFontFamily}
|
||||
/>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, getTerminalCursorSearchEntries()) ? (
|
||||
<TerminalCursorAppearanceSection
|
||||
key="cursor"
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
/>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, getTerminalPaneAppearanceSearchEntries()) ? (
|
||||
<TerminalPaneAppearanceSection
|
||||
key="pane-appearance"
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
/>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, getTerminalWindowSearchEntries()) ? (
|
||||
<TerminalWindowSection key="window" settings={settings} updateSettings={updateSettings} />
|
||||
) : null,
|
||||
showTerminalThemeCatalog ? (
|
||||
<TerminalThemeCatalogSection
|
||||
key={`theme-catalog-${preferredThemeTarget ?? 'manual'}`}
|
||||
settings={settings}
|
||||
systemPrefersDark={systemPrefersDark}
|
||||
themeSearch={themeSearch}
|
||||
setThemeSearch={setThemeSearch}
|
||||
updateSettings={updateSettings}
|
||||
previewFontFamily={previewFontFamily}
|
||||
importedHighlightSignal={warpThemes.importSignal}
|
||||
warpThemes={warpThemes}
|
||||
showThemeImport={showWarpThemeImport}
|
||||
preferredTarget={preferredThemeTarget}
|
||||
/>
|
||||
) : null
|
||||
].filter(Boolean)
|
||||
// Why: low-frequency knobs are force-opened during search; render each group
|
||||
// only when its own search matches so an active query never leaves a dangling header.
|
||||
const typographyMatches = matchesSettingsSearch(
|
||||
searchQuery,
|
||||
getTerminalAdvancedTypographySearchEntries()
|
||||
)
|
||||
const cursorMatches = matchesSettingsSearch(searchQuery, getTerminalCursorSearchEntries())
|
||||
const paneMatches = matchesSettingsSearch(searchQuery, getTerminalPaneAppearanceSearchEntries())
|
||||
const windowMatches = matchesSettingsSearch(searchQuery, getTerminalWindowSearchEntries())
|
||||
const themeCatalogMatches = matchesSettingsSearch(searchQuery, themeCatalogSearchEntries)
|
||||
const previewAdvancedMatches = cursorMatches || paneMatches || windowMatches
|
||||
const showThemeCatalog = !isSearching || themeCatalogMatches || previewAdvancedMatches
|
||||
const primaryTypographyMatches = matchesSettingsSearch(
|
||||
searchQuery,
|
||||
terminalTypographyEntries.slice(0, 2)
|
||||
)
|
||||
const ghosttyImportMatches = matchesSettingsSearch(searchQuery, ghosttyImportEntries)
|
||||
const showPrimaryTypography =
|
||||
!isSearching ||
|
||||
forceVisiblePrimary ||
|
||||
primaryTypographyMatches ||
|
||||
typographyMatches ||
|
||||
ghosttyImportMatches
|
||||
const showGhosttyImport = !isSearching || forceVisiblePrimary || ghosttyImportMatches
|
||||
const showTypographyAdvancedDisclosure = !isSearching || typographyMatches
|
||||
|
||||
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}
|
||||
const advancedGroups = [
|
||||
cursorMatches
|
||||
? {
|
||||
key: 'cursor',
|
||||
node: (
|
||||
<TerminalCursorAppearanceSection settings={settings} updateSettings={updateSettings} />
|
||||
)
|
||||
}
|
||||
: null,
|
||||
paneMatches
|
||||
? {
|
||||
key: 'pane',
|
||||
node: (
|
||||
<TerminalPaneAppearanceSection settings={settings} updateSettings={updateSettings} />
|
||||
)
|
||||
}
|
||||
: null,
|
||||
windowMatches
|
||||
? {
|
||||
key: 'window',
|
||||
node: <TerminalWindowSection settings={settings} updateSettings={updateSettings} />
|
||||
}
|
||||
: null
|
||||
].filter((group): group is { key: string; node: React.JSX.Element } => group !== null)
|
||||
const showAdvancedDisclosure = !isSearching || advancedGroups.length > 0
|
||||
const previewAdvancedContent = showAdvancedDisclosure ? (
|
||||
<AppearanceAdvancedDisclosure
|
||||
showTopBorder={false}
|
||||
className="mt-0 pt-2"
|
||||
contentClassName="ml-4 pt-4"
|
||||
>
|
||||
{advancedGroups.map((group, index) => (
|
||||
<div
|
||||
key={group.key}
|
||||
className={index > 0 ? 'mt-2 border-t border-border/60 pt-4' : undefined}
|
||||
>
|
||||
{group.node}
|
||||
</div>
|
||||
))}
|
||||
</AppearanceAdvancedDisclosure>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* Primary: font + theme + previews. The expanded section column is far
|
||||
narrower than the xl breakpoint, so the preview grids inside the
|
||||
theme catalog already stack full-width below their controls. */}
|
||||
{showPrimaryTypography ? (
|
||||
<section className="space-y-3 pt-2">
|
||||
<SettingsSubsectionHeader
|
||||
className="items-center"
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.048aac8a64',
|
||||
'Terminal Typography'
|
||||
)}
|
||||
action={
|
||||
showGhosttyImport ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1.5"
|
||||
onClick={() => void ghostty.handleClick()}
|
||||
>
|
||||
<img src={ghosttyIcon} alt="" aria-hidden="true" className="size-4" />
|
||||
{translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.855a76343a',
|
||||
'Import from Ghostty'
|
||||
)}
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="ml-4 divide-y divide-border/40 border-y border-border/40">
|
||||
<TerminalFontSizeSetting
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
forceVisible={forceVisiblePrimary}
|
||||
/>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.a408266e67',
|
||||
'Font Family'
|
||||
)}
|
||||
description={terminalTypographyEntries[1]?.description}
|
||||
keywords={
|
||||
terminalTypographyEntries[1]?.keywords ?? ['terminal', 'typography', 'font']
|
||||
}
|
||||
forceVisible={forceVisiblePrimary}
|
||||
>
|
||||
<SettingsRow
|
||||
label={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.a408266e67',
|
||||
'Font Family'
|
||||
)}
|
||||
control={
|
||||
<FontAutocomplete
|
||||
value={settings.terminalFontFamily}
|
||||
suggestions={terminalFontSuggestions}
|
||||
onChange={(value) => updateSettings({ terminalFontFamily: value })}
|
||||
onPreviewFontFamily={setPreviewFontFamily}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
</div>
|
||||
|
||||
{showTypographyAdvancedDisclosure ? (
|
||||
<div className="ml-4">
|
||||
<AppearanceAdvancedDisclosure showTopBorder={false} contentClassName="ml-4">
|
||||
<TerminalAdvancedTypographyControls
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
/>
|
||||
</AppearanceAdvancedDisclosure>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{showThemeCatalog ? (
|
||||
<TerminalThemeCatalogSection
|
||||
key={`theme-catalog-${preferredThemeTarget ?? 'manual'}`}
|
||||
settings={settings}
|
||||
systemPrefersDark={systemPrefersDark}
|
||||
themeSearch={themeSearch}
|
||||
setThemeSearch={setThemeSearch}
|
||||
updateSettings={updateSettings}
|
||||
previewFontFamily={previewFontFamily}
|
||||
importedHighlightSignal={warpThemes.importSignal}
|
||||
warpThemes={warpThemes}
|
||||
showThemeImport={showWarpThemeImport}
|
||||
preferredTarget={preferredThemeTarget}
|
||||
advancedContent={previewAdvancedContent}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<GhosttyImportModal
|
||||
open={ghostty.open}
|
||||
onOpenChange={ghostty.handleOpenChange}
|
||||
|
|
|
|||
|
|
@ -26,13 +26,9 @@ export function TerminalCursorAppearanceSection({
|
|||
'auto.components.settings.TerminalAppearanceSection.abcb4dd019',
|
||||
'Terminal Cursor'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.d455f2ef4f',
|
||||
'Default cursor appearance for Orca terminal panes.'
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="divide-y divide-border/40">
|
||||
<div className="ml-4 divide-y divide-border/40">
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.db270cc9a9',
|
||||
|
|
@ -44,15 +40,12 @@ export function TerminalCursorAppearanceSection({
|
|||
)}
|
||||
keywords={['terminal', 'cursor', 'bar', 'block', 'underline']}
|
||||
>
|
||||
{/* Why: Bar/Block/Underline options convey the meaning; helper text pruned. */}
|
||||
<SettingsRow
|
||||
label={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.db270cc9a9',
|
||||
'Cursor Shape'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.d455f2ef4f',
|
||||
'Default cursor appearance for Orca terminal panes.'
|
||||
)}
|
||||
control={
|
||||
<SettingsSegmentedControl
|
||||
ariaLabel={translate(
|
||||
|
|
@ -105,10 +98,6 @@ export function TerminalCursorAppearanceSection({
|
|||
'auto.components.settings.TerminalAppearanceSection.74736cc9b1',
|
||||
'Blinking Cursor'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.2de6b5a699',
|
||||
'Uses the blinking variant of the selected cursor shape.'
|
||||
)}
|
||||
checked={settings.terminalCursorBlink}
|
||||
onChange={() => updateSettings({ terminalCursorBlink: !settings.terminalCursorBlink })}
|
||||
/>
|
||||
|
|
@ -130,10 +119,7 @@ export function TerminalCursorAppearanceSection({
|
|||
'auto.components.settings.TerminalAppearanceSection.b9f1804422',
|
||||
'Cursor Opacity'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.04cdf85dec',
|
||||
'Opacity of the terminal cursor.'
|
||||
)}
|
||||
description=""
|
||||
value={settings.terminalCursorOpacity ?? 1}
|
||||
defaultValue={1}
|
||||
min={0}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ import { translate } from '@/i18n/i18n'
|
|||
|
||||
export function TerminalFontSizeSetting({
|
||||
settings,
|
||||
updateSettings
|
||||
updateSettings,
|
||||
forceVisible = false
|
||||
}: {
|
||||
settings: GlobalSettings
|
||||
updateSettings: (updates: Partial<GlobalSettings>) => void
|
||||
forceVisible?: boolean
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<SearchableSetting
|
||||
|
|
@ -21,16 +23,15 @@ export function TerminalFontSizeSetting({
|
|||
'Default terminal font size for new panes and live updates.'
|
||||
)}
|
||||
keywords={['terminal', 'typography', 'text size']}
|
||||
forceVisible={forceVisible}
|
||||
>
|
||||
{/* Why: helper text dropped per the copy audit — "Font Size" + px control
|
||||
is self-evident; the search index keeps the longer description. */}
|
||||
<SettingsRow
|
||||
label={translate(
|
||||
'auto.components.settings.TerminalFontSizeSetting.a4a352b1e9',
|
||||
'Font Size'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalFontSizeSetting.0f4c92e595',
|
||||
'Default terminal font size for new panes and live updates.'
|
||||
)}
|
||||
control={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -22,13 +22,9 @@ export function TerminalPaneAppearanceSection({
|
|||
'auto.components.settings.TerminalAppearanceSection.e1a5c25555',
|
||||
'Terminal Panes'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.1b79379d4f',
|
||||
'Control inactive pane dimming and split divider thickness.'
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="divide-y divide-border/40">
|
||||
<div className="ml-4 divide-y divide-border/40">
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.a6fdd6a3b1',
|
||||
|
|
@ -45,9 +41,10 @@ export function TerminalPaneAppearanceSection({
|
|||
'auto.components.settings.TerminalAppearanceSection.a6fdd6a3b1',
|
||||
'Inactive Pane Opacity'
|
||||
)}
|
||||
// Why: clarify which panes get dimmed; tightened per the copy audit.
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.db632cb50e',
|
||||
'Opacity applied to panes that are not currently active.'
|
||||
'auto.components.settings.TerminalAppearanceSection.dimUnfocusedPanes',
|
||||
'Dim unfocused panes.'
|
||||
)}
|
||||
value={paneStyleOptions.inactivePaneOpacity}
|
||||
defaultValue={0.8}
|
||||
|
|
@ -78,10 +75,7 @@ export function TerminalPaneAppearanceSection({
|
|||
'auto.components.settings.TerminalAppearanceSection.f27a99978d',
|
||||
'Divider Thickness'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalAppearanceSection.a14a427ae4',
|
||||
'Thickness of the pane divider line.'
|
||||
)}
|
||||
description=""
|
||||
value={paneStyleOptions.dividerThicknessPx}
|
||||
defaultValue={1}
|
||||
min={1}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, type Dispatch, type SetStateAction } from 'react'
|
||||
import { useState, type Dispatch, type ReactNode, type SetStateAction } from 'react'
|
||||
import type { GlobalSettings } from '../../../../shared/types'
|
||||
import {
|
||||
ColorField,
|
||||
|
|
@ -29,6 +29,7 @@ type TerminalThemeCatalogSectionProps = {
|
|||
warpThemes: UseWarpThemeImportReturn
|
||||
showThemeImport: boolean
|
||||
preferredTarget?: TerminalThemeTarget
|
||||
advancedContent?: ReactNode
|
||||
}
|
||||
|
||||
export function TerminalThemeCatalogSection({
|
||||
|
|
@ -41,7 +42,8 @@ export function TerminalThemeCatalogSection({
|
|||
importedHighlightSignal,
|
||||
warpThemes,
|
||||
showThemeImport,
|
||||
preferredTarget
|
||||
preferredTarget,
|
||||
advancedContent
|
||||
}: TerminalThemeCatalogSectionProps): React.JSX.Element {
|
||||
const [target, setTarget] = useState<TerminalThemeTarget>(preferredTarget ?? 'dark')
|
||||
const themeOptions = getAvailableTerminalThemeOptions(settings)
|
||||
|
|
@ -78,25 +80,23 @@ export function TerminalThemeCatalogSection({
|
|||
return (
|
||||
<section className="space-y-5">
|
||||
<SettingsSubsectionHeader
|
||||
className="items-center"
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalThemeSections.catalog_title',
|
||||
'Terminal Themes'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalThemeSections.catalog_description',
|
||||
'Choose terminal themes and divider colors for dark and light mode.'
|
||||
)}
|
||||
action={
|
||||
showThemeImport ? (
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<WarpThemeImportButton warpThemes={warpThemes} />
|
||||
<YamlThemeImportButton warpThemes={warpThemes} />
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
|
||||
{showThemeImport ? (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<WarpThemeImportButton warpThemes={warpThemes} />
|
||||
<YamlThemeImportButton warpThemes={warpThemes} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<div>
|
||||
<div className="ml-4 grid gap-4">
|
||||
<div className={advancedContent ? 'border-b border-border/40' : undefined}>
|
||||
<div className="space-y-3">
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
|
|
@ -237,6 +237,8 @@ export function TerminalThemeCatalogSection({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{advancedContent ? <div className="-mt-4">{advancedContent}</div> : null}
|
||||
|
||||
<TerminalSettingsPreview
|
||||
title={
|
||||
isLightTarget
|
||||
|
|
|
|||
|
|
@ -59,132 +59,126 @@ export function TerminalWindowSection({
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalWindowSection.ea7b1a158e',
|
||||
'Background Opacity'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalWindowSection.03acb60aa0',
|
||||
'Controls the transparency of the terminal background.'
|
||||
)}
|
||||
keywords={['opacity', 'transparency', 'background', 'alpha']}
|
||||
>
|
||||
<NumberField
|
||||
label={translate(
|
||||
<div className="ml-4 space-y-4">
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalWindowSection.ea7b1a158e',
|
||||
'Background Opacity'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalWindowSection.809f37738d',
|
||||
'Controls the transparency of the terminal background. 1 is fully opaque, 0 is fully transparent.'
|
||||
'auto.components.settings.TerminalWindowSection.03acb60aa0',
|
||||
'Controls the transparency of the terminal background.'
|
||||
)}
|
||||
value={settings.terminalBackgroundOpacity ?? 1}
|
||||
defaultValue={1}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
suffix="0 to 1"
|
||||
onChange={(value) =>
|
||||
updateSettings({ terminalBackgroundOpacity: clampNumber(value, 0, 1) })
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
keywords={['opacity', 'transparency', 'background', 'alpha']}
|
||||
>
|
||||
<NumberField
|
||||
label={translate(
|
||||
'auto.components.settings.TerminalWindowSection.ea7b1a158e',
|
||||
'Background Opacity'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalWindowSection.809f37738d',
|
||||
'Controls the transparency of the terminal background. 1 is fully opaque, 0 is fully transparent.'
|
||||
)}
|
||||
value={settings.terminalBackgroundOpacity ?? 1}
|
||||
defaultValue={1}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
suffix="0 to 1"
|
||||
onChange={(value) =>
|
||||
updateSettings({ terminalBackgroundOpacity: clampNumber(value, 0, 1) })
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalWindowSection.2b82242f43',
|
||||
'Window Blur'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalWindowSection.97950bb087',
|
||||
'Apply background blur to the terminal window. Requires restart.'
|
||||
)}
|
||||
keywords={['window', 'blur', 'background', 'transparency', 'vibrancy']}
|
||||
className="space-y-3 py-2"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="space-y-0.5">
|
||||
<Label>
|
||||
{translate(
|
||||
'auto.components.settings.TerminalWindowSection.2b82242f43',
|
||||
'Window Blur'
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.TerminalWindowSection.97950bb087',
|
||||
'Apply background blur to the terminal window. Requires restart.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={settings.windowBackgroundBlur ?? false}
|
||||
onClick={() => updateSettings({ windowBackgroundBlur: !settings.windowBackgroundBlur })}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
(settings.windowBackgroundBlur ?? false) ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
(settings.windowBackgroundBlur ?? false) ? 'translate-x-4' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{blurPendingRestart ? (
|
||||
<div className="flex items-center justify-between gap-3 rounded-md border border-yellow-500/50 bg-yellow-500/10 px-3 py-2.5">
|
||||
<div className="min-w-0 flex-1 space-y-0.5">
|
||||
<p className="text-sm font-medium text-yellow-700 dark:text-yellow-300">
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalWindowSection.2b82242f43',
|
||||
'Window Blur'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalWindowSection.97950bb087',
|
||||
'Apply background blur to the terminal window. Requires restart.'
|
||||
)}
|
||||
keywords={['window', 'blur', 'background', 'transparency', 'vibrancy']}
|
||||
className="space-y-3 py-2"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="space-y-0.5">
|
||||
<Label>
|
||||
{translate(
|
||||
'auto.components.settings.TerminalWindowSection.c65bb9ce63',
|
||||
'Restart required'
|
||||
'auto.components.settings.TerminalWindowSection.2b82242f43',
|
||||
'Window Blur'
|
||||
)}
|
||||
</p>
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.TerminalWindowSection.53ce336e15',
|
||||
'Restart Orca to apply the window blur change.'
|
||||
'auto.components.settings.TerminalWindowSection.97950bb087',
|
||||
'Apply background blur to the terminal window. Requires restart.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
className="shrink-0 gap-1.5"
|
||||
disabled={relaunchingBlur}
|
||||
onClick={() => void handleRelaunch()}
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={settings.windowBackgroundBlur ?? false}
|
||||
onClick={() =>
|
||||
updateSettings({ windowBackgroundBlur: !settings.windowBackgroundBlur })
|
||||
}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
(settings.windowBackgroundBlur ?? false)
|
||||
? 'bg-foreground'
|
||||
: 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<RotateCw className={`size-3 ${relaunchingBlur ? 'animate-spin' : ''}`} />
|
||||
{relaunchingBlur
|
||||
? translate(
|
||||
'auto.components.settings.TerminalWindowSection.907131d741',
|
||||
'Restarting…'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.settings.TerminalWindowSection.8abdab9f7c',
|
||||
'Restart now'
|
||||
)}
|
||||
</Button>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
(settings.windowBackgroundBlur ?? false) ? 'translate-x-4' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalWindowSection.36b8402015',
|
||||
'Horizontal Padding'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalWindowSection.25e2f8e8e1',
|
||||
'Horizontal padding around the terminal grid in pixels.'
|
||||
)}
|
||||
keywords={['padding', 'horizontal', 'spacing', 'margin']}
|
||||
>
|
||||
<NumberField
|
||||
label={translate(
|
||||
{blurPendingRestart ? (
|
||||
<div className="flex items-center justify-between gap-3 rounded-md border border-yellow-500/50 bg-yellow-500/10 px-3 py-2.5">
|
||||
<div className="min-w-0 flex-1 space-y-0.5">
|
||||
<p className="text-sm font-medium text-yellow-700 dark:text-yellow-300">
|
||||
{translate(
|
||||
'auto.components.settings.TerminalWindowSection.c65bb9ce63',
|
||||
'Restart required'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.TerminalWindowSection.53ce336e15',
|
||||
'Restart Orca to apply the window blur change.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
className="shrink-0 gap-1.5"
|
||||
disabled={relaunchingBlur}
|
||||
onClick={() => void handleRelaunch()}
|
||||
>
|
||||
<RotateCw className={`size-3 ${relaunchingBlur ? 'animate-spin' : ''}`} />
|
||||
{relaunchingBlur
|
||||
? translate(
|
||||
'auto.components.settings.TerminalWindowSection.907131d741',
|
||||
'Restarting…'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.settings.TerminalWindowSection.8abdab9f7c',
|
||||
'Restart now'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalWindowSection.36b8402015',
|
||||
'Horizontal Padding'
|
||||
)}
|
||||
|
|
@ -192,29 +186,26 @@ export function TerminalWindowSection({
|
|||
'auto.components.settings.TerminalWindowSection.25e2f8e8e1',
|
||||
'Horizontal padding around the terminal grid in pixels.'
|
||||
)}
|
||||
value={settings.terminalPaddingX ?? 4}
|
||||
defaultValue={4}
|
||||
min={0}
|
||||
max={512}
|
||||
step={1}
|
||||
suffix="px"
|
||||
onChange={(value) => updateSettings({ terminalPaddingX: Math.max(0, value) })}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
keywords={['padding', 'horizontal', 'spacing', 'margin']}
|
||||
>
|
||||
<NumberField
|
||||
label={translate(
|
||||
'auto.components.settings.TerminalWindowSection.36b8402015',
|
||||
'Horizontal Padding'
|
||||
)}
|
||||
description=""
|
||||
value={settings.terminalPaddingX ?? 4}
|
||||
defaultValue={4}
|
||||
min={0}
|
||||
max={512}
|
||||
step={1}
|
||||
suffix="px"
|
||||
onChange={(value) => updateSettings({ terminalPaddingX: Math.max(0, value) })}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalWindowSection.1afcc1d973',
|
||||
'Vertical Padding'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalWindowSection.1846f6ee6a',
|
||||
'Vertical padding around the terminal grid in pixels.'
|
||||
)}
|
||||
keywords={['padding', 'vertical', 'spacing', 'margin']}
|
||||
>
|
||||
<NumberField
|
||||
label={translate(
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalWindowSection.1afcc1d973',
|
||||
'Vertical Padding'
|
||||
)}
|
||||
|
|
@ -222,133 +213,140 @@ export function TerminalWindowSection({
|
|||
'auto.components.settings.TerminalWindowSection.1846f6ee6a',
|
||||
'Vertical padding around the terminal grid in pixels.'
|
||||
)}
|
||||
value={settings.terminalPaddingY ?? 4}
|
||||
defaultValue={4}
|
||||
min={0}
|
||||
max={512}
|
||||
step={1}
|
||||
suffix="px"
|
||||
onChange={(value) => updateSettings({ terminalPaddingY: Math.max(0, value) })}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalWindowSection.3530908ef9',
|
||||
'Hide Mouse While Typing'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalWindowSection.1d1920dc8a',
|
||||
'Hide the mouse cursor when typing in the terminal.'
|
||||
)}
|
||||
keywords={['mouse', 'hide', 'typing', 'cursor']}
|
||||
className="flex items-center justify-between gap-4 py-2"
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
<Label>
|
||||
{translate(
|
||||
'auto.components.settings.TerminalWindowSection.3530908ef9',
|
||||
'Hide Mouse While Typing'
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.TerminalWindowSection.1d1920dc8a',
|
||||
'Hide the mouse cursor when typing in the terminal.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={settings.terminalMouseHideWhileTyping ?? false}
|
||||
onClick={() =>
|
||||
updateSettings({
|
||||
terminalMouseHideWhileTyping: !settings.terminalMouseHideWhileTyping
|
||||
})
|
||||
}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
(settings.terminalMouseHideWhileTyping ?? false)
|
||||
? 'bg-foreground'
|
||||
: 'bg-muted-foreground/30'
|
||||
}`}
|
||||
keywords={['padding', 'vertical', 'spacing', 'margin']}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
(settings.terminalMouseHideWhileTyping ?? false) ? 'translate-x-4' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalWindowSection.63f8d9336e',
|
||||
'Color Overrides'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalWindowSection.e86e09b5c7',
|
||||
'Override individual terminal colors.'
|
||||
)}
|
||||
keywords={['color', 'override', 'ansi', 'palette', 'theme']}
|
||||
className="space-y-3"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => setColorOverridesExpanded((prev) => !prev)}
|
||||
className="flex items-center gap-2 text-sm font-medium"
|
||||
>
|
||||
<span className={`transition-transform ${colorOverridesExpanded ? 'rotate-90' : ''}`}>
|
||||
▶
|
||||
</span>
|
||||
{translate(
|
||||
'auto.components.settings.TerminalWindowSection.63f8d9336e',
|
||||
'Color Overrides'
|
||||
<NumberField
|
||||
label={translate(
|
||||
'auto.components.settings.TerminalWindowSection.1afcc1d973',
|
||||
'Vertical Padding'
|
||||
)}
|
||||
</button>
|
||||
<div
|
||||
className={`grid overflow-hidden transition-all duration-300 ease-out ${
|
||||
colorOverridesExpanded ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
|
||||
description=""
|
||||
value={settings.terminalPaddingY ?? 4}
|
||||
defaultValue={4}
|
||||
min={0}
|
||||
max={512}
|
||||
step={1}
|
||||
suffix="px"
|
||||
onChange={(value) => updateSettings({ terminalPaddingY: Math.max(0, value) })}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalWindowSection.3530908ef9',
|
||||
'Hide Mouse While Typing'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalWindowSection.1d1920dc8a',
|
||||
'Hide the mouse cursor when typing in the terminal.'
|
||||
)}
|
||||
keywords={['mouse', 'hide', 'typing', 'cursor']}
|
||||
className="flex items-center justify-between gap-4 py-2"
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
{/* Why: helper text dropped per copy audit — near-verbatim restatement
|
||||
of the label; the search index keeps the longer phrasing. */}
|
||||
<Label>
|
||||
{translate(
|
||||
'auto.components.settings.TerminalWindowSection.3530908ef9',
|
||||
'Hide Mouse While Typing'
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={settings.terminalMouseHideWhileTyping ?? false}
|
||||
onClick={() =>
|
||||
updateSettings({
|
||||
terminalMouseHideWhileTyping: !settings.terminalMouseHideWhileTyping
|
||||
})
|
||||
}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
(settings.terminalMouseHideWhileTyping ?? false)
|
||||
? 'bg-foreground'
|
||||
: 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<div className="min-h-0 space-y-4">
|
||||
{COLOR_OVERRIDE_GROUPS.map((group) => (
|
||||
<div key={group.label} className="space-y-2">
|
||||
<p className="text-xs font-semibold text-muted-foreground">{group.label}</p>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{group.keys.map((item) => (
|
||||
<ColorField
|
||||
key={item.key}
|
||||
label={item.label}
|
||||
description={item.description}
|
||||
value={settings.terminalColorOverrides?.[item.key] ?? ''}
|
||||
fallback=""
|
||||
onChange={(value) =>
|
||||
updateSettings({
|
||||
terminalColorOverrides: {
|
||||
...settings.terminalColorOverrides,
|
||||
[item.key]: value || undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
(settings.terminalMouseHideWhileTyping ?? false)
|
||||
? 'translate-x-4'
|
||||
: 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalWindowSection.63f8d9336e',
|
||||
'Color Overrides'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalWindowSection.e86e09b5c7',
|
||||
'Override individual terminal colors.'
|
||||
)}
|
||||
keywords={['color', 'override', 'ansi', 'palette', 'theme']}
|
||||
className="space-y-3"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => setColorOverridesExpanded((prev) => !prev)}
|
||||
className="flex items-center gap-2 text-sm font-medium"
|
||||
>
|
||||
<span className={`transition-transform ${colorOverridesExpanded ? 'rotate-90' : ''}`}>
|
||||
▶
|
||||
</span>
|
||||
{translate(
|
||||
'auto.components.settings.TerminalWindowSection.63f8d9336e',
|
||||
'Color Overrides'
|
||||
)}
|
||||
</button>
|
||||
<div
|
||||
className={`grid overflow-hidden transition-all duration-300 ease-out ${
|
||||
colorOverridesExpanded ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
|
||||
}`}
|
||||
>
|
||||
<div className="min-h-0 space-y-4">
|
||||
{COLOR_OVERRIDE_GROUPS.map((group) => (
|
||||
<div key={group.label} className="space-y-2">
|
||||
<p className="text-xs font-semibold text-muted-foreground">{group.label}</p>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{group.keys.map((item) => (
|
||||
<ColorField
|
||||
key={item.key}
|
||||
label={item.label}
|
||||
description={item.description}
|
||||
value={settings.terminalColorOverrides?.[item.key] ?? ''}
|
||||
fallback=""
|
||||
onChange={(value) =>
|
||||
updateSettings({
|
||||
terminalColorOverrides: {
|
||||
...settings.terminalColorOverrides,
|
||||
[item.key]: value || undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => updateSettings({ terminalColorOverrides: undefined })}
|
||||
>
|
||||
{translate(
|
||||
'auto.components.settings.TerminalWindowSection.03c855d15f',
|
||||
'Reset all color overrides'
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => updateSettings({ terminalColorOverrides: undefined })}
|
||||
>
|
||||
{translate(
|
||||
'auto.components.settings.TerminalWindowSection.03c855d15f',
|
||||
'Reset all color overrides'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SearchableSetting>
|
||||
</SearchableSetting>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -245,6 +245,25 @@ export function getSystemTrayEntries(options: SystemTraySearchOptions = {}): Set
|
|||
return shouldShowSystemTrayEntries(options) ? getSystemTrayEntryCatalog() : []
|
||||
}
|
||||
|
||||
const getAppearanceSectionEntries = createLocalizedCatalog((): SettingsSearchEntry[] => [
|
||||
{
|
||||
title: translate('auto.components.settings.AppearancePane.interfaceTitle', 'Interface')
|
||||
},
|
||||
{
|
||||
title: translate('auto.components.settings.AppearancePane.terminalTitle', 'Terminal')
|
||||
},
|
||||
{
|
||||
title: translate(
|
||||
'auto.components.settings.AppearancePane.windowSidebarTitle',
|
||||
'Window & Sidebar'
|
||||
),
|
||||
description: translate(
|
||||
'auto.components.settings.AppearancePane.windowSidebarSummary',
|
||||
'Sidebar, status bar, and file explorer'
|
||||
)
|
||||
}
|
||||
])
|
||||
|
||||
type AppearancePaneSearchOptions = {
|
||||
showWarpImport?: boolean
|
||||
showSystemTray?: boolean
|
||||
|
|
@ -254,6 +273,7 @@ function buildAppearancePaneSearchEntries(
|
|||
options: AppearancePaneSearchOptions
|
||||
): SettingsSearchEntry[] {
|
||||
return [
|
||||
...getAppearanceSectionEntries(),
|
||||
...getThemeEntries(),
|
||||
...(SHOW_UI_LANGUAGE_SETTING ? getLanguageEntries() : []),
|
||||
...getTypographyEntries(),
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export const getWorkspaceCardLayoutEntry = createLocalizedCatalog(
|
|||
),
|
||||
description: translate(
|
||||
'auto.components.settings.appearance.search.workspaceCardLayout.description',
|
||||
'Switch between compact and detailed workspace cards from the workspace sidebar options menu.'
|
||||
'Workspace cards can use compact or detailed layouts.'
|
||||
),
|
||||
keywords: [
|
||||
...translateSearchKeyword(
|
||||
|
|
|
|||
|
|
@ -180,6 +180,8 @@ describe('getTerminalPaneSearchEntries', () => {
|
|||
|
||||
expect(getSidebarEntries()).toContainEqual(entry)
|
||||
expect(getAppearancePaneSearchEntries()).toContainEqual(entry)
|
||||
expect(entry.description).toBe('Workspace cards can use compact or detailed layouts.')
|
||||
expect(entry.description).not.toContain('options menu')
|
||||
})
|
||||
|
||||
it.each(['compact', 'compact display', 'workspace cards', 'sidebar', 'card layout'])(
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
import { createLocalizedCatalog } from '@/i18n/localized-catalog'
|
||||
|
||||
export {
|
||||
getTerminalAdvancedTypographySearchEntries,
|
||||
getTerminalTypographySearchEntries,
|
||||
getTerminalRenderingSearchEntries,
|
||||
getTerminalCursorSearchEntries
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { translate } from '@/i18n/i18n'
|
|||
import { translateSearchKeyword } from './settings-search-keywords'
|
||||
import { createLocalizedCatalog } from '@/i18n/localized-catalog'
|
||||
|
||||
export const getTerminalTypographySearchEntries = createLocalizedCatalog(() => [
|
||||
const getTerminalTypographySearchEntryCatalog = createLocalizedCatalog(() => [
|
||||
{
|
||||
title: translate('auto.components.settings.terminal.search.5930244899', 'Font Size'),
|
||||
description: translate(
|
||||
|
|
@ -100,6 +100,14 @@ export const getTerminalTypographySearchEntries = createLocalizedCatalog(() => [
|
|||
}
|
||||
])
|
||||
|
||||
export const getTerminalTypographySearchEntries = createLocalizedCatalog(() => [
|
||||
...getTerminalTypographySearchEntryCatalog()
|
||||
])
|
||||
|
||||
export const getTerminalAdvancedTypographySearchEntries = createLocalizedCatalog(() =>
|
||||
getTerminalTypographySearchEntryCatalog().slice(2)
|
||||
)
|
||||
|
||||
export const getTerminalRenderingSearchEntries = createLocalizedCatalog(() => [
|
||||
{
|
||||
title: translate('auto.components.settings.terminal.search.13a2502dfc', 'GPU Acceleration'),
|
||||
|
|
|
|||
|
|
@ -6215,6 +6215,249 @@ describe('connectPanePty', () => {
|
|||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('abandons a stalled hidden restore and drains pending foreground chunks warning-first', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport('pty-id')
|
||||
const capturedDataCallback: {
|
||||
current: ((data: string, meta?: { seq?: number; rawLength?: number }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
})
|
||||
transportFactoryQueue.push(transport)
|
||||
const getMainBufferSnapshot = window.api.pty.getMainBufferSnapshot as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>
|
||||
const snapshot = createDeferred<{ data: string; cols: number; rows: number; seq: number }>()
|
||||
getMainBufferSnapshot.mockReturnValue(snapshot.promise)
|
||||
const hidden = 'hidden-codex-output\r\n'
|
||||
const firstLive = 'first-live-output\r\n'
|
||||
const secondLive = 'second-live-output\r\n'
|
||||
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
isVisibleRef: { current: false },
|
||||
startup: { command: 'codex' }
|
||||
})
|
||||
const disposable = connectPanePty(pane as never, manager as never, deps as never)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
expect(capturedDataCallback.current).not.toBeNull()
|
||||
vi.useFakeTimers()
|
||||
capturedDataCallback.current?.(hidden, { seq: hidden.length, rawLength: hidden.length })
|
||||
;(deps.isVisibleRef as { current: boolean }).current = true
|
||||
capturedDataCallback.current?.(firstLive, {
|
||||
seq: hidden.length + firstLive.length,
|
||||
rawLength: firstLive.length
|
||||
})
|
||||
await flushAsyncTicks(4)
|
||||
capturedDataCallback.current?.(secondLive, {
|
||||
seq: hidden.length + firstLive.length + secondLive.length,
|
||||
rawLength: secondLive.length
|
||||
})
|
||||
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledWith('pty-id', { scrollbackRows: 5000 })
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(firstLive, expect.any(Function))
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(secondLive, expect.any(Function))
|
||||
|
||||
vi.advanceTimersByTime(749)
|
||||
await flushAsyncTicks(4)
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('main recovery was unavailable'),
|
||||
expect.any(Function)
|
||||
)
|
||||
|
||||
vi.advanceTimersByTime(1)
|
||||
vi.advanceTimersByTime(0)
|
||||
await flushAsyncTicks(10)
|
||||
|
||||
const written = pane.terminal.write.mock.calls.map(([data]) => data as string)
|
||||
const warningIndex = written.findIndex((data) => data.includes('main recovery was unavailable'))
|
||||
const combinedLiveIndex = written.indexOf(firstLive + secondLive)
|
||||
expect(warningIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(combinedLiveIndex).toBeGreaterThan(warningIndex)
|
||||
|
||||
snapshot.resolve({
|
||||
data: 'late-snapshot-state\r\n',
|
||||
cols: 100,
|
||||
rows: 30,
|
||||
seq: hidden.length + firstLive.length + secondLive.length
|
||||
})
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(
|
||||
'late-snapshot-state\r\n',
|
||||
expect.any(Function)
|
||||
)
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('falls back after repeated null hidden restore retries and drains blocked foreground', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport('pty-id')
|
||||
const capturedDataCallback: {
|
||||
current: ((data: string, meta?: { seq?: number; rawLength?: number }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
})
|
||||
transportFactoryQueue.push(transport)
|
||||
const getMainBufferSnapshot = window.api.pty.getMainBufferSnapshot as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>
|
||||
getMainBufferSnapshot.mockResolvedValue(null)
|
||||
const hidden = 'hidden-codex-output\r\n'
|
||||
const live = 'visible-after-null-retries\r\n'
|
||||
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
isVisibleRef: { current: false },
|
||||
startup: { command: 'codex' }
|
||||
})
|
||||
const disposable = connectPanePty(pane as never, manager as never, deps as never)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
expect(capturedDataCallback.current).not.toBeNull()
|
||||
vi.useFakeTimers()
|
||||
capturedDataCallback.current?.(hidden, { seq: hidden.length, rawLength: hidden.length })
|
||||
;(deps.isVisibleRef as { current: boolean }).current = true
|
||||
capturedDataCallback.current?.(live, {
|
||||
seq: hidden.length + live.length,
|
||||
rawLength: live.length
|
||||
})
|
||||
await flushAsyncTicks(10)
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(live, expect.any(Function))
|
||||
vi.advanceTimersByTime(50)
|
||||
vi.advanceTimersByTime(0)
|
||||
await flushAsyncTicks(10)
|
||||
}
|
||||
|
||||
const written = pane.terminal.write.mock.calls.map(([data]) => data as string)
|
||||
const warningIndex = written.findIndex((data) => data.includes('main recovery was unavailable'))
|
||||
const liveIndex = written.indexOf(live)
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledTimes(4)
|
||||
expect(warningIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(liveIndex).toBeGreaterThan(warningIndex)
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('drops pending foreground overflow when a stalled hidden restore falls back', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport('pty-id')
|
||||
const capturedDataCallback: {
|
||||
current: ((data: string, meta?: { seq?: number; rawLength?: number }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
})
|
||||
transportFactoryQueue.push(transport)
|
||||
const getMainBufferSnapshot = window.api.pty.getMainBufferSnapshot as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>
|
||||
getMainBufferSnapshot.mockReturnValue(
|
||||
createDeferred<{ data: string; cols: number; rows: number; seq: number }>().promise
|
||||
)
|
||||
const hidden = 'hidden-codex-output\r\n'
|
||||
const liveOverflow = 'v'.repeat(512 * 1024 + 1)
|
||||
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
isVisibleRef: { current: false },
|
||||
startup: { command: 'codex' }
|
||||
})
|
||||
const disposable = connectPanePty(pane as never, manager as never, deps as never)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
expect(capturedDataCallback.current).not.toBeNull()
|
||||
vi.useFakeTimers()
|
||||
capturedDataCallback.current?.(hidden, { seq: hidden.length, rawLength: hidden.length })
|
||||
;(deps.isVisibleRef as { current: boolean }).current = true
|
||||
capturedDataCallback.current?.(liveOverflow, {
|
||||
seq: hidden.length + liveOverflow.length,
|
||||
rawLength: liveOverflow.length
|
||||
})
|
||||
await flushAsyncTicks(4)
|
||||
|
||||
vi.advanceTimersByTime(750)
|
||||
await vi.runAllTimersAsync()
|
||||
await flushAsyncTicks(20)
|
||||
vi.advanceTimersByTime(0)
|
||||
await flushAsyncTicks(10)
|
||||
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(
|
||||
expect.stringContaining('main recovery was unavailable'),
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(liveOverflow, expect.any(Function))
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('coalesces tiny pending foreground chunks when stalled hidden restore falls back', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport('pty-id')
|
||||
const capturedDataCallback: {
|
||||
current: ((data: string, meta?: { seq?: number; rawLength?: number }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
})
|
||||
transportFactoryQueue.push(transport)
|
||||
const getMainBufferSnapshot = window.api.pty.getMainBufferSnapshot as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>
|
||||
getMainBufferSnapshot.mockReturnValue(
|
||||
createDeferred<{ data: string; cols: number; rows: number; seq: number }>().promise
|
||||
)
|
||||
const hidden = 'hidden-codex-output\r\n'
|
||||
const chunkCount = 2_000
|
||||
const liveChunk = 'x'
|
||||
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
isVisibleRef: { current: false },
|
||||
startup: { command: 'codex' }
|
||||
})
|
||||
const disposable = connectPanePty(pane as never, manager as never, deps as never)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
expect(capturedDataCallback.current).not.toBeNull()
|
||||
vi.useFakeTimers()
|
||||
capturedDataCallback.current?.(hidden, { seq: hidden.length, rawLength: hidden.length })
|
||||
;(deps.isVisibleRef as { current: boolean }).current = true
|
||||
for (let index = 0; index < chunkCount; index += 1) {
|
||||
capturedDataCallback.current?.(liveChunk, {
|
||||
seq: hidden.length + index + 1,
|
||||
rawLength: liveChunk.length
|
||||
})
|
||||
}
|
||||
await flushAsyncTicks(4)
|
||||
|
||||
vi.advanceTimersByTime(750)
|
||||
vi.advanceTimersByTime(0)
|
||||
await flushAsyncTicks(10)
|
||||
|
||||
const written = pane.terminal.write.mock.calls.map(([data]) => data as string)
|
||||
const warningIndex = written.findIndex((data) => data.includes('main recovery was unavailable'))
|
||||
const combinedLive = liveChunk.repeat(chunkCount)
|
||||
const liveWrites = written.filter((data) => data === combinedLive)
|
||||
expect(warningIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(liveWrites).toHaveLength(1)
|
||||
expect(written.indexOf(combinedLive)).toBeGreaterThan(warningIndex)
|
||||
|
||||
disposable.dispose()
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
})
|
||||
|
||||
it('keeps foreground output when hidden-backlog snapshot recovery is unavailable', async () => {
|
||||
const pendingTimeouts: {
|
||||
canceled: boolean
|
||||
|
|
@ -6324,6 +6567,106 @@ describe('connectPanePty', () => {
|
|||
}
|
||||
})
|
||||
|
||||
it('keeps a newer same-PTY hidden restore after a timed-out snapshot resolves late', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport('pty-id')
|
||||
const capturedDataCallback: {
|
||||
current: ((data: string, meta?: { seq?: number; rawLength?: number }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
})
|
||||
transportFactoryQueue.push(transport)
|
||||
const getMainBufferSnapshot = window.api.pty.getMainBufferSnapshot as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>
|
||||
const firstSnapshot = createDeferred<{
|
||||
data: string
|
||||
cols: number
|
||||
rows: number
|
||||
seq: number
|
||||
}>()
|
||||
const secondSnapshot = createDeferred<{
|
||||
data: string
|
||||
cols: number
|
||||
rows: number
|
||||
seq: number
|
||||
}>()
|
||||
getMainBufferSnapshot
|
||||
.mockReturnValueOnce(firstSnapshot.promise)
|
||||
.mockReturnValueOnce(secondSnapshot.promise)
|
||||
const firstHidden = 'first-hidden-output\r\n'
|
||||
const firstLive = 'first-live-output\r\n'
|
||||
const secondHidden = 'second-hidden-output\r\n'
|
||||
const secondLive = 'second-live-output\r\n'
|
||||
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
isVisibleRef: { current: false },
|
||||
startup: { command: 'codex' }
|
||||
})
|
||||
const disposable = connectPanePty(pane as never, manager as never, deps as never)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
expect(capturedDataCallback.current).not.toBeNull()
|
||||
vi.useFakeTimers()
|
||||
capturedDataCallback.current?.(firstHidden, {
|
||||
seq: firstHidden.length,
|
||||
rawLength: firstHidden.length
|
||||
})
|
||||
;(deps.isVisibleRef as { current: boolean }).current = true
|
||||
capturedDataCallback.current?.(firstLive, {
|
||||
seq: firstHidden.length + firstLive.length,
|
||||
rawLength: firstLive.length
|
||||
})
|
||||
await flushAsyncTicks(4)
|
||||
vi.advanceTimersByTime(750)
|
||||
vi.advanceTimersByTime(0)
|
||||
await flushAsyncTicks(10)
|
||||
|
||||
pane.terminal.write.mockClear()
|
||||
;(deps.isVisibleRef as { current: boolean }).current = false
|
||||
capturedDataCallback.current?.(secondHidden, {
|
||||
seq: firstHidden.length + firstLive.length + secondHidden.length,
|
||||
rawLength: secondHidden.length
|
||||
})
|
||||
;(deps.isVisibleRef as { current: boolean }).current = true
|
||||
capturedDataCallback.current?.(secondLive, {
|
||||
seq: firstHidden.length + firstLive.length + secondHidden.length + secondLive.length,
|
||||
rawLength: secondLive.length
|
||||
})
|
||||
await flushAsyncTicks(4)
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledTimes(2)
|
||||
|
||||
firstSnapshot.resolve({
|
||||
data: 'stale-first-snapshot\r\n',
|
||||
cols: 100,
|
||||
rows: 30,
|
||||
seq: firstHidden.length + firstLive.length
|
||||
})
|
||||
await flushAsyncTicks(10)
|
||||
secondSnapshot.resolve({
|
||||
data: 'fresh-second-snapshot\r\n',
|
||||
cols: 100,
|
||||
rows: 30,
|
||||
seq: firstHidden.length + firstLive.length + secondHidden.length + secondLive.length
|
||||
})
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(
|
||||
'stale-first-snapshot\r\n',
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(
|
||||
'fresh-second-snapshot\r\n',
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(secondLive, expect.any(Function))
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('ignores an async hidden-backlog snapshot if the pane changes PTYs first', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport('old-pty-id')
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ const HIDDEN_OUTPUT_RESTORE_SCROLLBACK_ROWS = 5000
|
|||
const HIDDEN_OUTPUT_RESTORE_PENDING_CHARS = 512 * 1024
|
||||
const HIDDEN_OUTPUT_RESTORE_DEFERRED_RETRY_MS = 50
|
||||
const HIDDEN_OUTPUT_RESTORE_DEFERRED_RETRY_MAX = 3
|
||||
const HIDDEN_OUTPUT_RESTORE_FOREGROUND_TIMEOUT_MS = 750
|
||||
const TERMINAL_RENDERER_RISK_SCAN_TAIL_CHARS = 256
|
||||
const CURSOR_SHOW_SEQUENCE = '\x1b[?25h'
|
||||
const CURSOR_HIDE_SEQUENCE = '\x1b[?25l'
|
||||
|
|
@ -811,6 +812,7 @@ export function connectPanePty(
|
|||
let unregisterBacklogRecovery: (() => void) | null = null
|
||||
let unregisterDocumentVisibilityRecovery: (() => void) | null = null
|
||||
let cleanupHiddenOutputRestoreDeferredRetry = (): void => {}
|
||||
let cleanupHiddenOutputRestoreForegroundDeadline = (): void => {}
|
||||
let unregisterE2ePtyDataInjection = (): void => {}
|
||||
let startupInjectTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let sshShellReadyFallbackTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
|
@ -2619,6 +2621,7 @@ export function connectPanePty(
|
|||
let hiddenOutputRestoreRetryDeferred = false
|
||||
let hiddenOutputRestoreScheduled = false
|
||||
let hiddenOutputRestoreDeferredRetryTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let hiddenOutputRestoreForegroundDeadlineTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let hiddenOutputRestoreDeferredRetryAttempts = 0
|
||||
// Why: hidden recovery state belongs to one PTY stream. Reattach/restart
|
||||
// can reuse the pane object for a different session before visibility.
|
||||
|
|
@ -2856,7 +2859,8 @@ export function connectPanePty(
|
|||
(synchronizedForegroundOutput ||
|
||||
nativeWindowsCursorRestore ||
|
||||
foregroundRenderRefreshNeeded),
|
||||
followupForegroundRefresh: nativeWindowsCursorRestore || nativeWindowsInPlaceRewriteFollowup,
|
||||
followupForegroundRefresh:
|
||||
nativeWindowsCursorRestore || nativeWindowsInPlaceRewriteFollowup,
|
||||
stripTransientCursorShows: shouldProtectNativeWindowsSynchronizedOutput && foreground,
|
||||
coalesceForeground: synchronizedForegroundOutput && synchronizedOutputEnded,
|
||||
holdForeground: synchronizedForegroundOutput && nextSynchronizedForegroundOutputActive
|
||||
|
|
@ -3030,6 +3034,7 @@ export function connectPanePty(
|
|||
hiddenOutputRestorePendingChunks = []
|
||||
hiddenOutputRestorePendingChars = 0
|
||||
hiddenOutputRestorePendingOverflow = true
|
||||
armHiddenOutputRestoreForegroundDeadline()
|
||||
return
|
||||
}
|
||||
const pending: PendingHiddenOutputRestoreChunk = { data }
|
||||
|
|
@ -3041,6 +3046,7 @@ export function connectPanePty(
|
|||
}
|
||||
hiddenOutputRestorePendingChunks.push(pending)
|
||||
hiddenOutputRestorePendingChars += data.length
|
||||
armHiddenOutputRestoreForegroundDeadline()
|
||||
}
|
||||
|
||||
function getChunkDataAfterSnapshot(
|
||||
|
|
@ -3109,34 +3115,10 @@ export function connectPanePty(
|
|||
hiddenOutputRestoreScheduled = false
|
||||
cancelScheduledHiddenOutputRestore(pane.terminal)
|
||||
clearHiddenOutputRestoreDeferredRetryTimer()
|
||||
clearHiddenOutputRestoreForegroundDeadlineTimer()
|
||||
hiddenOutputRestoreDeferredRetryAttempts = 0
|
||||
}
|
||||
|
||||
function drainPendingLiveChunksWithoutSnapshot(): void {
|
||||
if (hiddenOutputRestorePendingOverflow) {
|
||||
hiddenOutputRestorePendingChunks = []
|
||||
hiddenOutputRestorePendingChars = 0
|
||||
hiddenOutputRestorePendingOverflow = false
|
||||
return
|
||||
}
|
||||
// Why: once snapshot retries are exhausted, these bounded chunks are the
|
||||
// only known visible-era PTY bytes; replay them without overlap trimming.
|
||||
while (hiddenOutputRestorePendingChunks.length > 0) {
|
||||
const chunks = hiddenOutputRestorePendingChunks
|
||||
hiddenOutputRestorePendingChunks = []
|
||||
hiddenOutputRestorePendingChars = 0
|
||||
for (const chunk of chunks) {
|
||||
writePtyOutputToXterm(chunk.data, true)
|
||||
}
|
||||
if (hiddenOutputRestorePendingOverflow) {
|
||||
hiddenOutputRestorePendingChunks = []
|
||||
hiddenOutputRestorePendingChars = 0
|
||||
hiddenOutputRestorePendingOverflow = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearHiddenOutputRestoreDeferredRetryTimer(): void {
|
||||
if (hiddenOutputRestoreDeferredRetryTimer === null) {
|
||||
return
|
||||
|
|
@ -3146,6 +3128,81 @@ export function connectPanePty(
|
|||
}
|
||||
cleanupHiddenOutputRestoreDeferredRetry = clearHiddenOutputRestoreDeferredRetryTimer
|
||||
|
||||
function clearHiddenOutputRestoreForegroundDeadlineTimer(): void {
|
||||
if (hiddenOutputRestoreForegroundDeadlineTimer === null) {
|
||||
return
|
||||
}
|
||||
clearTimeout(hiddenOutputRestoreForegroundDeadlineTimer)
|
||||
hiddenOutputRestoreForegroundDeadlineTimer = null
|
||||
}
|
||||
cleanupHiddenOutputRestoreForegroundDeadline = clearHiddenOutputRestoreForegroundDeadlineTimer
|
||||
|
||||
function armHiddenOutputRestoreForegroundDeadline(): void {
|
||||
if (
|
||||
disposed ||
|
||||
hiddenOutputRestoreForegroundDeadlineTimer !== null ||
|
||||
!shouldWritePtyOutputForeground(deps.isVisibleRef.current) ||
|
||||
(hiddenOutputRestorePendingChunks.length === 0 && !hiddenOutputRestorePendingOverflow)
|
||||
) {
|
||||
return
|
||||
}
|
||||
const ptyId = hiddenOutputRestorePtyId
|
||||
if (ptyId === null || transport.getPtyId() !== ptyId) {
|
||||
return
|
||||
}
|
||||
const deadlineGeneration = hiddenOutputRestoreGeneration
|
||||
// Why: only foreground-visible output blocked behind recovery gets a
|
||||
// deadline; hidden-time restore work can continue without user impact.
|
||||
hiddenOutputRestoreForegroundDeadlineTimer = setTimeout(() => {
|
||||
hiddenOutputRestoreForegroundDeadlineTimer = null
|
||||
if (
|
||||
disposed ||
|
||||
hiddenOutputRestoreGeneration !== deadlineGeneration ||
|
||||
hiddenOutputRestorePtyId !== ptyId ||
|
||||
!shouldWritePtyOutputForeground(deps.isVisibleRef.current)
|
||||
) {
|
||||
return
|
||||
}
|
||||
abandonHiddenOutputRestoreAndDrainPendingForeground(ptyId)
|
||||
}, HIDDEN_OUTPUT_RESTORE_FOREGROUND_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
function abandonHiddenOutputRestoreAndDrainPendingForeground(expectedPtyId: string): void {
|
||||
if (transport.getPtyId() !== expectedPtyId || hiddenOutputRestorePtyId !== expectedPtyId) {
|
||||
resetHiddenOutputRestoreIfPtyChanged()
|
||||
return
|
||||
}
|
||||
const pendingChunks = hiddenOutputRestorePendingOverflow
|
||||
? []
|
||||
: hiddenOutputRestorePendingChunks.slice()
|
||||
const hadPendingOverflow = hiddenOutputRestorePendingOverflow
|
||||
hiddenOutputRestoreGeneration += 1
|
||||
hiddenOutputRestoreInFlight = null
|
||||
hiddenOutputRestoreNeeded = false
|
||||
hiddenOutputRestorePtyId = null
|
||||
hiddenOutputRestorePendingChunks = []
|
||||
hiddenOutputRestorePendingChars = 0
|
||||
hiddenOutputRestorePendingOverflow = false
|
||||
hiddenOutputRestoreFreshSnapshotNeeded = false
|
||||
hiddenOutputRestoreRetryDeferred = false
|
||||
hiddenOutputRestoreScheduled = false
|
||||
hiddenStartupRendererQueryPending = ''
|
||||
hiddenRendererStateDirty = false
|
||||
cancelScheduledHiddenOutputRestore(pane.terminal)
|
||||
clearHiddenOutputRestoreDeferredRetryTimer()
|
||||
clearHiddenOutputRestoreForegroundDeadlineTimer()
|
||||
hiddenOutputRestoreDeferredRetryAttempts = 0
|
||||
|
||||
writeRestoreUnavailableWarning()
|
||||
if (hadPendingOverflow) {
|
||||
return
|
||||
}
|
||||
const pendingData = pendingChunks.map((chunk) => chunk.data).join('')
|
||||
if (pendingData) {
|
||||
writePtyOutputToXterm(pendingData, true)
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleHiddenOutputRestoreDeferredRetry(): void {
|
||||
if (
|
||||
disposed ||
|
||||
|
|
@ -3155,9 +3212,13 @@ export function connectPanePty(
|
|||
return
|
||||
}
|
||||
if (hiddenOutputRestoreDeferredRetryAttempts >= HIDDEN_OUTPUT_RESTORE_DEFERRED_RETRY_MAX) {
|
||||
writeRestoreUnavailableWarning()
|
||||
drainPendingLiveChunksWithoutSnapshot()
|
||||
clearHiddenOutputRestoreState()
|
||||
const ptyId = hiddenOutputRestorePtyId
|
||||
if (ptyId !== null) {
|
||||
abandonHiddenOutputRestoreAndDrainPendingForeground(ptyId)
|
||||
} else {
|
||||
clearHiddenOutputRestoreState()
|
||||
writeRestoreUnavailableWarning()
|
||||
}
|
||||
return
|
||||
}
|
||||
hiddenOutputRestoreDeferredRetryAttempts += 1
|
||||
|
|
@ -3283,6 +3344,7 @@ export function connectPanePty(
|
|||
}
|
||||
hiddenOutputRestorePtyId = ptyId
|
||||
if (hiddenOutputRestoreInFlight) {
|
||||
armHiddenOutputRestoreForegroundDeadline()
|
||||
return true
|
||||
}
|
||||
if (!opts?.bypassScheduler) {
|
||||
|
|
@ -3356,14 +3418,15 @@ export function connectPanePty(
|
|||
if (disposed) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
hiddenOutputRestoreGeneration !== restoreGeneration ||
|
||||
transport.getPtyId() !== currentPtyId ||
|
||||
hiddenOutputRestorePtyId !== currentPtyId
|
||||
) {
|
||||
const restoreGenerationChanged = hiddenOutputRestoreGeneration !== restoreGeneration
|
||||
const restorePtyChanged =
|
||||
transport.getPtyId() !== currentPtyId || hiddenOutputRestorePtyId !== currentPtyId
|
||||
if (restoreGenerationChanged || restorePtyChanged) {
|
||||
// Why: the snapshot belongs to the requested PTY; after reattach,
|
||||
// replaying it would show stale/cleared output in the new terminal.
|
||||
if (hiddenOutputRestorePtyId === currentPtyId) {
|
||||
// A stale generation may be an abandoned timeout while a newer
|
||||
// restore for the same PTY owns the current hidden-recovery state.
|
||||
if (restorePtyChanged && hiddenOutputRestorePtyId === currentPtyId) {
|
||||
clearHiddenOutputRestoreState()
|
||||
}
|
||||
return
|
||||
|
|
@ -3382,6 +3445,7 @@ export function connectPanePty(
|
|||
if (drainPendingLiveChunksAfterSnapshot(snapshot.seq) && !needsFreshSnapshot) {
|
||||
hiddenOutputRestoreNeeded = false
|
||||
hiddenOutputRestorePtyId = null
|
||||
clearHiddenOutputRestoreForegroundDeadlineTimer()
|
||||
return
|
||||
}
|
||||
if (!shouldWritePtyOutputForeground(deps.isVisibleRef.current)) {
|
||||
|
|
@ -3393,10 +3457,16 @@ export function connectPanePty(
|
|||
}
|
||||
hiddenOutputRestoreNeeded = true
|
||||
}
|
||||
})().finally(() => {
|
||||
hiddenOutputRestoreInFlight = null
|
||||
})()
|
||||
const hiddenOutputRestoreTask = hiddenOutputRestoreInFlight
|
||||
let trackedHiddenOutputRestore: Promise<void>
|
||||
trackedHiddenOutputRestore = hiddenOutputRestoreTask.finally(() => {
|
||||
if (hiddenOutputRestoreInFlight === trackedHiddenOutputRestore) {
|
||||
hiddenOutputRestoreInFlight = null
|
||||
}
|
||||
if (hiddenOutputRestorePendingChunks.length > 0 || hiddenOutputRestorePendingOverflow) {
|
||||
hiddenOutputRestoreNeeded = true
|
||||
armHiddenOutputRestoreForegroundDeadline()
|
||||
}
|
||||
if (
|
||||
!hiddenOutputRestoreRetryDeferred &&
|
||||
|
|
@ -3406,6 +3476,7 @@ export function connectPanePty(
|
|||
requestHiddenOutputRestoreIfNeeded()
|
||||
}
|
||||
})
|
||||
hiddenOutputRestoreInFlight = trackedHiddenOutputRestore
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -4300,6 +4371,7 @@ export function connectPanePty(
|
|||
clearTerminalBellNotificationTimer()
|
||||
clearReattachIdleAgentCursorResetTimer()
|
||||
cleanupHiddenOutputRestoreDeferredRetry()
|
||||
cleanupHiddenOutputRestoreForegroundDeadline()
|
||||
unregisterBacklogRecovery?.()
|
||||
unregisterBacklogRecovery = null
|
||||
unregisterDocumentVisibilityRecovery?.()
|
||||
|
|
|
|||
|
|
@ -4597,7 +4597,16 @@
|
|||
"tintOpacity": "Tint Strength",
|
||||
"tintOpacityDescription": "Controls how strongly the tint is mixed into the sidebar."
|
||||
},
|
||||
"workspaceCardLayoutGuidance": "Use the workspace sidebar options menu > Card layout > Compact."
|
||||
"workspaceCardLayoutGuidance": "Managed from the workspace sidebar.",
|
||||
"interfaceDefaultFont": "Default font",
|
||||
"terminalDefaultFont": "Default font",
|
||||
"interfaceTitle": "Interface",
|
||||
"terminalTitle": "Terminal",
|
||||
"windowSidebarTitle": "Window & Sidebar",
|
||||
"windowSidebarSummary": "Sidebar, status bar, and file explorer",
|
||||
"statusBarCount": "{{value0}} indicators visible.",
|
||||
"gitIgnoredGlossary": "Files matched by .gitignore.",
|
||||
"statusBarDescription": "Choose which indicators appear in the status bar."
|
||||
},
|
||||
"AutoRenameBranchFromWorkSetting": {
|
||||
"1626524572": "Nautilus",
|
||||
|
|
@ -6248,7 +6257,9 @@
|
|||
"4415beb958": "disabled",
|
||||
"4e7d41a9f0": "enabled",
|
||||
"e90afcc44f": "off",
|
||||
"16c471ee03": "on"
|
||||
"16c471ee03": "on",
|
||||
"typographyAdvanced": "Typography",
|
||||
"dimUnfocusedPanes": "Dim unfocused panes."
|
||||
},
|
||||
"TerminalFontSizeSetting": {
|
||||
"9b5252c85a": "px",
|
||||
|
|
@ -6778,7 +6789,7 @@
|
|||
},
|
||||
"workspaceCardLayout": {
|
||||
"title": "Workspace Card Layout",
|
||||
"description": "Switch between compact and detailed workspace cards from the workspace sidebar options menu.",
|
||||
"description": "Workspace cards can use compact or detailed layouts.",
|
||||
"compact": "compact",
|
||||
"compactDisplay": "compact display",
|
||||
"workspaceCards": "workspace cards",
|
||||
|
|
@ -8360,6 +8371,9 @@
|
|||
"wslUnavailable": "WSL is not available on this machine.",
|
||||
"distroRequired": "Choose a WSL distro before projects can inherit WSL.",
|
||||
"wslDescription": "Detect and launch agents in {{value0}} via WSL for projects that do not override their runtime."
|
||||
},
|
||||
"AppearanceAdvancedDisclosure": {
|
||||
"advanced": "Advanced"
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
|
|
|
|||
|
|
@ -4594,10 +4594,19 @@
|
|||
"tintOpacity": "Intensidad del tinte",
|
||||
"tintOpacityDescription": "Controla con qué fuerza se mezcla el tinte en la barra lateral."
|
||||
},
|
||||
"workspaceCardLayoutGuidance": "Usa el menú de opciones de la barra lateral de espacios de trabajo > Diseño de tarjeta > Compacto.",
|
||||
"workspaceCardLayoutGuidance": "Gestionado desde la barra lateral del espacio de trabajo.",
|
||||
"872af9556e": "Bandeja del sistema",
|
||||
"2edf606c46": "Minimizar a la bandeja al cerrar",
|
||||
"b707773a0d": "Cuando está activado, cerrar la ventana mantiene Orca en ejecución en la bandeja del sistema en lugar de salir."
|
||||
"b707773a0d": "Cuando está activado, cerrar la ventana mantiene Orca en ejecución en la bandeja del sistema en lugar de salir.",
|
||||
"interfaceDefaultFont": "Fuente predeterminada",
|
||||
"terminalDefaultFont": "Fuente predeterminada",
|
||||
"interfaceTitle": "Interfaz",
|
||||
"terminalTitle": "Terminal",
|
||||
"windowSidebarTitle": "Ventana y barra lateral",
|
||||
"windowSidebarSummary": "Barra lateral, barra de estado y explorador de archivos",
|
||||
"statusBarCount": "Indicadores visibles: {{value0}}.",
|
||||
"gitIgnoredGlossary": "Archivos coincidentes con .gitignore.",
|
||||
"statusBarDescription": "Choose which indicators appear in the status bar."
|
||||
},
|
||||
"AutoRenameBranchFromWorkSetting": {
|
||||
"1626524572": "Nautilus",
|
||||
|
|
@ -6211,7 +6220,9 @@
|
|||
"4415beb958": "desactivado",
|
||||
"4e7d41a9f0": "activado",
|
||||
"e90afcc44f": "apagado",
|
||||
"16c471ee03": "en"
|
||||
"16c471ee03": "activado",
|
||||
"typographyAdvanced": "Tipografía",
|
||||
"dimUnfocusedPanes": "Atenuar paneles sin foco."
|
||||
},
|
||||
"TerminalFontSizeSetting": {
|
||||
"9b5252c85a": "píxeles",
|
||||
|
|
@ -6739,7 +6750,7 @@
|
|||
},
|
||||
"workspaceCardLayout": {
|
||||
"title": "Diseño de tarjetas de espacios de trabajo",
|
||||
"description": "Cambia entre tarjetas de espacios de trabajo compactas y detalladas desde el menú de opciones de la barra lateral de espacios de trabajo.",
|
||||
"description": "Las tarjetas de espacios de trabajo pueden usar diseños compactos o detallados.",
|
||||
"compact": "compacto",
|
||||
"compactDisplay": "vista compacta",
|
||||
"workspaceCards": "tarjetas de espacios de trabajo",
|
||||
|
|
@ -8360,6 +8371,9 @@
|
|||
"wslUnavailable": "WSL is not available on this machine.",
|
||||
"distroRequired": "Choose a WSL distro before projects can inherit WSL.",
|
||||
"wslDescription": "Detect and launch agents in {{value0}} via WSL for projects that do not override their runtime."
|
||||
},
|
||||
"AppearanceAdvancedDisclosure": {
|
||||
"advanced": "Avanzado"
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
|
|
|
|||
|
|
@ -4579,10 +4579,19 @@
|
|||
"tintOpacity": "色合いの強さ",
|
||||
"tintOpacityDescription": "サイドバーに色合いをどの程度強く混ぜるかを調整します。"
|
||||
},
|
||||
"workspaceCardLayoutGuidance": "ワークスペースサイドバーのオプションメニュー > カードレイアウト > コンパクト を使用します。",
|
||||
"workspaceCardLayoutGuidance": "ワークスペースサイドバーで管理されます。",
|
||||
"872af9556e": "システムトレイ",
|
||||
"2edf606c46": "閉じるときにトレイへ最小化",
|
||||
"b707773a0d": "有効にすると、ウィンドウを閉じてもOrcaは終了せず、システムトレイで実行を続けます。"
|
||||
"b707773a0d": "有効にすると、ウィンドウを閉じてもOrcaは終了せず、システムトレイで実行を続けます。",
|
||||
"interfaceDefaultFont": "デフォルトフォント",
|
||||
"terminalDefaultFont": "デフォルトフォント",
|
||||
"interfaceTitle": "インターフェイス",
|
||||
"terminalTitle": "ターミナル",
|
||||
"windowSidebarTitle": "ウィンドウとサイドバー",
|
||||
"windowSidebarSummary": "サイドバー、ステータスバー、ファイルエクスプローラー",
|
||||
"statusBarCount": "{{value0}} 個のインジケーターが表示中。",
|
||||
"gitIgnoredGlossary": ".gitignore に一致するファイル。",
|
||||
"statusBarDescription": "Choose which indicators appear in the status bar."
|
||||
},
|
||||
"AutoRenameBranchFromWorkSetting": {
|
||||
"1626524572": "Nautilus",
|
||||
|
|
@ -6233,7 +6242,9 @@
|
|||
"4415beb958": "無効",
|
||||
"4e7d41a9f0": "有効",
|
||||
"e90afcc44f": "オフ",
|
||||
"16c471ee03": "の上"
|
||||
"16c471ee03": "オン",
|
||||
"typographyAdvanced": "タイポグラフィ",
|
||||
"dimUnfocusedPanes": "フォーカスされていないペインを暗くします。"
|
||||
},
|
||||
"TerminalFontSizeSetting": {
|
||||
"9b5252c85a": "ピクセル",
|
||||
|
|
@ -6761,7 +6772,7 @@
|
|||
},
|
||||
"workspaceCardLayout": {
|
||||
"title": "ワークスペースカードのレイアウト",
|
||||
"description": "ワークスペースサイドバーのオプションメニューから、コンパクト表示と詳細表示のワークスペースカードを切り替えます。",
|
||||
"description": "ワークスペースカードはコンパクトまたは詳細レイアウトを使用できます。",
|
||||
"compact": "コンパクト",
|
||||
"compactDisplay": "コンパクト表示",
|
||||
"workspaceCards": "ワークスペースカード",
|
||||
|
|
@ -8360,6 +8371,9 @@
|
|||
"wslUnavailable": "WSL is not available on this machine.",
|
||||
"distroRequired": "Choose a WSL distro before projects can inherit WSL.",
|
||||
"wslDescription": "Detect and launch agents in {{value0}} via WSL for projects that do not override their runtime."
|
||||
},
|
||||
"AppearanceAdvancedDisclosure": {
|
||||
"advanced": "詳細設定"
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
|
|
|
|||
|
|
@ -4582,7 +4582,16 @@
|
|||
"tintOpacity": "색조 강도",
|
||||
"tintOpacityDescription": "사이드바에 색조를 얼마나 강하게 섞을지 조절합니다."
|
||||
},
|
||||
"workspaceCardLayoutGuidance": "워크스페이스 사이드바 옵션 메뉴 > 카드 레이아웃 > 컴팩트를 사용하세요."
|
||||
"workspaceCardLayoutGuidance": "워크스페이스 사이드바에서 관리됩니다.",
|
||||
"interfaceDefaultFont": "기본 글꼴",
|
||||
"terminalDefaultFont": "기본 글꼴",
|
||||
"interfaceTitle": "인터페이스",
|
||||
"terminalTitle": "터미널",
|
||||
"windowSidebarTitle": "창 및 사이드바",
|
||||
"windowSidebarSummary": "사이드바, 상태 표시줄 및 파일 탐색기",
|
||||
"statusBarCount": "{{value0}}개 표시기가 보입니다.",
|
||||
"gitIgnoredGlossary": ".gitignore와 일치하는 파일.",
|
||||
"statusBarDescription": "Choose which indicators appear in the status bar."
|
||||
},
|
||||
"AutoRenameBranchFromWorkSetting": {
|
||||
"1626524572": "Nautilus",
|
||||
|
|
@ -6196,7 +6205,9 @@
|
|||
"4415beb958": "비활성",
|
||||
"4e7d41a9f0": "활성화됨",
|
||||
"e90afcc44f": "끔",
|
||||
"16c471ee03": "켜짐"
|
||||
"16c471ee03": "켜짐",
|
||||
"typographyAdvanced": "타이포그래피",
|
||||
"dimUnfocusedPanes": "포커스되지 않은 창을 흐리게 표시합니다."
|
||||
},
|
||||
"TerminalFontSizeSetting": {
|
||||
"9b5252c85a": "px",
|
||||
|
|
@ -6726,7 +6737,7 @@
|
|||
"4d5b9427b5": "활성화하면 창을 닫아도 Orca가 종료되지 않고 시스템 트레이에서 계속 실행됩니다.",
|
||||
"workspaceCardLayout": {
|
||||
"title": "워크스페이스 카드 레이아웃",
|
||||
"description": "워크스페이스 사이드바 옵션 메뉴에서 워크스페이스 카드를 컴팩트 또는 상세 보기로 전환합니다.",
|
||||
"description": "워크스페이스 카드는 컴팩트 또는 상세 레이아웃을 사용할 수 있습니다.",
|
||||
"compact": "컴팩트",
|
||||
"compactDisplay": "컴팩트 보기",
|
||||
"workspaceCards": "워크스페이스 카드",
|
||||
|
|
@ -8360,6 +8371,9 @@
|
|||
"wslUnavailable": "WSL is not available on this machine.",
|
||||
"distroRequired": "Choose a WSL distro before projects can inherit WSL.",
|
||||
"wslDescription": "Detect and launch agents in {{value0}} via WSL for projects that do not override their runtime."
|
||||
},
|
||||
"AppearanceAdvancedDisclosure": {
|
||||
"advanced": "고급"
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
|
|
|
|||
|
|
@ -4579,10 +4579,19 @@
|
|||
"tintOpacity": "色调强度",
|
||||
"tintOpacityDescription": "控制色调混入边栏的强度。"
|
||||
},
|
||||
"workspaceCardLayoutGuidance": "使用工作区侧边栏选项菜单 > 卡片布局 > 紧凑。",
|
||||
"workspaceCardLayoutGuidance": "从工作区侧边栏管理。",
|
||||
"872af9556e": "系统托盘",
|
||||
"2edf606c46": "关闭时最小化到托盘",
|
||||
"b707773a0d": "启用后,关闭窗口会让 Orca 继续在系统托盘中运行,而不是退出。"
|
||||
"b707773a0d": "启用后,关闭窗口会让 Orca 继续在系统托盘中运行,而不是退出。",
|
||||
"interfaceDefaultFont": "默认字体",
|
||||
"terminalDefaultFont": "默认字体",
|
||||
"interfaceTitle": "界面",
|
||||
"terminalTitle": "终端",
|
||||
"windowSidebarTitle": "窗口和侧边栏",
|
||||
"windowSidebarSummary": "侧边栏、状态栏和文件浏览器",
|
||||
"statusBarCount": "显示 {{value0}} 个指示器。",
|
||||
"gitIgnoredGlossary": "与 .gitignore 匹配的文件。",
|
||||
"statusBarDescription": "Choose which indicators appear in the status bar."
|
||||
},
|
||||
"AutoRenameBranchFromWorkSetting": {
|
||||
"1626524572": "Nautilus",
|
||||
|
|
@ -6196,7 +6205,9 @@
|
|||
"4415beb958": "已禁用",
|
||||
"4e7d41a9f0": "已启用",
|
||||
"e90afcc44f": "关",
|
||||
"16c471ee03": "开"
|
||||
"16c471ee03": "开",
|
||||
"typographyAdvanced": "排版",
|
||||
"dimUnfocusedPanes": "调暗未聚焦的窗格。"
|
||||
},
|
||||
"TerminalFontSizeSetting": {
|
||||
"9b5252c85a": "像素",
|
||||
|
|
@ -6724,7 +6735,7 @@
|
|||
},
|
||||
"workspaceCardLayout": {
|
||||
"title": "工作区卡片布局",
|
||||
"description": "从工作区侧边栏选项菜单在紧凑和详细工作区卡片之间切换。",
|
||||
"description": "工作区卡片可以使用紧凑或详细布局。",
|
||||
"compact": "紧凑",
|
||||
"compactDisplay": "紧凑显示",
|
||||
"workspaceCards": "工作区卡片",
|
||||
|
|
@ -8360,6 +8371,9 @@
|
|||
"wslUnavailable": "WSL is not available on this machine.",
|
||||
"distroRequired": "Choose a WSL distro before projects can inherit WSL.",
|
||||
"wslDescription": "Detect and launch agents in {{value0}} via WSL for projects that do not override their runtime."
|
||||
},
|
||||
"AppearanceAdvancedDisclosure": {
|
||||
"advanced": "高级"
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
|
|
|
|||
|
|
@ -140,9 +140,7 @@ describe('agent process recognition', () => {
|
|||
agent: 'qwen-code',
|
||||
processName: 'qwen'
|
||||
})
|
||||
expect(
|
||||
recognizeAgentProcess(String.raw`C:\Users\dev\AppData\Roaming\npm\qwen.cmd`)
|
||||
).toEqual({
|
||||
expect(recognizeAgentProcess(String.raw`C:\Users\dev\AppData\Roaming\npm\qwen.cmd`)).toEqual({
|
||||
agent: 'qwen-code',
|
||||
processName: 'qwen'
|
||||
})
|
||||
|
|
|
|||
|
|
@ -850,11 +850,7 @@ describe('keybindings', () => {
|
|||
|
||||
// Ctrl+Shift+C on the same layout (terminal copy) must match too.
|
||||
expect(
|
||||
keybindingMatchesAction(
|
||||
'terminal.copySelection',
|
||||
{ ...cyrillicCtrlC, shift: true },
|
||||
'win32'
|
||||
)
|
||||
keybindingMatchesAction('terminal.copySelection', { ...cyrillicCtrlC, shift: true }, 'win32')
|
||||
).toBe(true)
|
||||
|
||||
// Greek layout: physical P produces 'π' (U+03C0); Ctrl+P must still match.
|
||||
|
|
|
|||
Loading…
Reference in New Issue