+ {interfaceMatches ? (
+
}
+ title={interfaceTitle}
+ summary={interfaceSummary}
+ open={isSectionOpen('interface')}
+ onToggle={() => toggleSection('interface')}
+ >
+
+
+ ) : null}
-
-
-
- updateSettings({ showTitlebarAppName: !settings.showTitlebarAppName })
- }
- />
-
-
-
- ) : null,
- isDesktopWindows && matchesSettingsSearch(searchQuery, systemTrayEntries) ? (
-
-
+ {/* 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. */}
-
-
-
- updateSettings({ minimizeToTrayOnClose: !settings.minimizeToTrayOnClose })
- }
- />
-
-
-
- ) : null,
- matchesSettingsSearch(searchQuery, getStatusBarEntries()) ? (
-
-
+ {terminalMatches ? (
+ }
+ title={terminalTitle}
+ summary={terminalSummary}
+ open={isSectionOpen('terminal')}
+ onToggle={() => toggleSection('terminal')}
+ >
+
+
+ ) : null}
-
- {visibleStatusBarToggles.map((toggle) => {
- const enabled = statusBarItems.includes(toggle.id)
- return (
-
- {
- 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}
- />
-
- )
- })}
-
-
- ) : null,
- matchesSettingsSearch(searchQuery, getSidebarEntries()) ? (
-
-
+ {windowMatches ? (
+ }
+ title={windowSidebarTitle}
+ summary={windowSidebarSummary}
+ open={isSectionOpen('window')}
+ onToggle={() => toggleSection('window')}
+ >
+
+
+ ) : null}
-
-
-
-
-
- {/* Why: this setting lives with the sidebar layout controls; Settings only
- points people to it so we do not create a second stateful control. */}
-
- Card layout > Compact.'
- )}
- control={null}
- />
-
-
-
-
- updateSettings({ showTasksButton: !(settings.showTasksButton !== false) })
- }
- />
-
-
-
-
- updateSettings({
- showAutomationsButton: !(settings.showAutomationsButton !== false)
- })
- }
- />
-
-
-
-
- updateSettings({ showMobileButton: !(settings.showMobileButton !== false) })
- }
- />
-
-
-
- ) : null,
- matchesSettingsSearch(searchQuery, getAppIconEntries()) ? (
-
+ {/* App icon stays at the bottom of Appearance as a small easter egg,
+ matching production — not buried inside Interface advanced. */}
+ {appIconMatches ? (
updateSettings({ appIcon })}
/>
-
- ) : null
- ].filter(Boolean)
-
- return (
-
- {visibleSections.map((section, index) => (
-
- {index > 0 ? : null}
- {section}
-
- ))}
+ ) : null}
)
}
diff --git a/src/renderer/src/components/settings/AppearanceSection.tsx b/src/renderer/src/components/settings/AppearanceSection.tsx
new file mode 100644
index 000000000..faa7dde2f
--- /dev/null
+++ b/src/renderer/src/components/settings/AppearanceSection.tsx
@@ -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 (
+
+
+
+ {icon}
+
+
+ {title}
+ {!open ? (
+ {summary}
+ ) : null}
+
+
+
+
+
+ )
+}
diff --git a/src/renderer/src/components/settings/AppearanceShortcutHintList.tsx b/src/renderer/src/components/settings/AppearanceShortcutHintList.tsx
new file mode 100644
index 000000000..8f9b33cb2
--- /dev/null
+++ b/src/renderer/src/components/settings/AppearanceShortcutHintList.tsx
@@ -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 (
+
+ {translate('auto.components.settings.AppearancePane.3057983501', 'Unassigned')}
+
+ )
+ }
+ const primaryCombo = combos[0]
+
+ return (
+
+
+
+ )
+}
diff --git a/src/renderer/src/components/settings/AppearanceWindowSidebarSection.tsx b/src/renderer/src/components/settings/AppearanceWindowSidebarSection.tsx
new file mode 100644
index 000000000..065056fbe
--- /dev/null
+++ b/src/renderer/src/components/settings/AppearanceWindowSidebarSection.tsx
@@ -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
) => 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 (
+
+
+
+
+
+
+
+
+ {showStatusBarControls ? (
+
+ {visibleStatusBarToggles.map((toggle) => {
+ const enabled = statusBarItems.includes(toggle.id)
+ return (
+
+ {
+ recordStatusBarToggleInteraction(toggle.id, recordFeatureInteraction)
+ toggleStatusBarItem(toggle.id)
+ }}
+ ariaLabel={toggle.title}
+ />
+
+ )
+ })}
+
+ ) : null}
+
+
+
+ {showAdvanced ? (
+
+
+ {showSidebarAdvanced ? (
+
+
+
+ {/* Why: this setting lives with the sidebar layout controls; Settings only
+ names that ownership so we do not create a second stateful control. */}
+
+
+ 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'
+ )
+ }
+ ]}
+ />
+ }
+ />
+
+
+
+
+ updateSettings({ showTasksButton: !(settings.showTasksButton !== false) })
+ }
+ />
+
+
+
+
+ updateSettings({
+ showAutomationsButton: !(settings.showAutomationsButton !== false)
+ })
+ }
+ />
+
+
+
+
+ updateSettings({ showMobileButton: !(settings.showMobileButton !== false) })
+ }
+ />
+
+
+
+ ) : null}
+
+ {showFileExplorerAdvanced ? (
+
+
+
+
+
+ updateSettings({
+ showGitIgnoredFiles: !(settings.showGitIgnoredFiles ?? true)
+ })
+ }
+ />
+
+
+
+ ) : null}
+
+
+ ) : null}
+
+ )
+}
diff --git a/src/renderer/src/components/settings/SettingsFormControls.tsx b/src/renderer/src/components/settings/SettingsFormControls.tsx
index d60a72c69..bc51edc5b 100644
--- a/src/renderer/src/components/settings/SettingsFormControls.tsx
+++ b/src/renderer/src/components/settings/SettingsFormControls.tsx
@@ -76,11 +76,19 @@ export function SettingsRow({
}: SettingsRowProps): React.JSX.Element {
return (
-
-
{label}
- {description ?
{description}
: null}
+
+
+ {label}
+
+ {description ? (
+
{description}
+ ) : null}
{control}
@@ -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 (
-
+
{title}
{description ?
{description}
: null}
diff --git a/src/renderer/src/components/settings/TerminalAdvancedTypographyControls.tsx b/src/renderer/src/components/settings/TerminalAdvancedTypographyControls.tsx
new file mode 100644
index 000000000..5acfee0d1
--- /dev/null
+++ b/src/renderer/src/components/settings/TerminalAdvancedTypographyControls.tsx
@@ -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
) => 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 (
+
+
+
+ updateSettings({ terminalFontWeight: normalizeTerminalFontWeight(value) })
+ }
+ />
+
+
+
+ updateSettings({ terminalLineHeight: clampNumber(value, 1, 3) })}
+ />
+
+
+
+ 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'
+ )
+ }
+ ]}
+ />
+ }
+ />
+
+ {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'
+ )}
+ .
+
+
+
+ )
+}
diff --git a/src/renderer/src/components/settings/TerminalAppearanceSection.ghostty.test.ts b/src/renderer/src/components/settings/TerminalAppearanceSection.ghostty.test.ts
index deb307211..d5f2c28f7 100644
--- a/src/renderer/src/components/settings/TerminalAppearanceSection.ghostty.test.ts
+++ b/src/renderer/src/components/settings/TerminalAppearanceSection.ghostty.test.ts
@@ -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,
diff --git a/src/renderer/src/components/settings/TerminalAppearanceSection.tsx b/src/renderer/src/components/settings/TerminalAppearanceSection.tsx
index e79067383..4503eb545 100644
--- a/src/renderer/src/components/settings/TerminalAppearanceSection.tsx
+++ b/src/renderer/src/components/settings/TerminalAppearanceSection.tsx
@@ -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(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()) ? (
-
- ) : null,
- matchesSettingsSearch(searchQuery, getTerminalCursorSearchEntries()) ? (
-
- ) : null,
- matchesSettingsSearch(searchQuery, getTerminalPaneAppearanceSearchEntries()) ? (
-
- ) : null,
- matchesSettingsSearch(searchQuery, getTerminalWindowSearchEntries()) ? (
-
- ) : null,
- showTerminalThemeCatalog ? (
-
- ) : 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 (
-
- {visibleSections.map((section, index) => (
-
- {index > 0 ?
: null}
- {section}
+ const advancedGroups = [
+ cursorMatches
+ ? {
+ key: 'cursor',
+ node: (
+
+ )
+ }
+ : null,
+ paneMatches
+ ? {
+ key: 'pane',
+ node: (
+
+ )
+ }
+ : null,
+ windowMatches
+ ? {
+ key: 'window',
+ node:
+ }
+ : null
+ ].filter((group): group is { key: string; node: React.JSX.Element } => group !== null)
+ const showAdvancedDisclosure = !isSearching || advancedGroups.length > 0
+ const previewAdvancedContent = showAdvancedDisclosure ? (
+
+ {advancedGroups.map((group, index) => (
+ 0 ? 'mt-2 border-t border-border/60 pt-4' : undefined}
+ >
+ {group.node}
))}
+
+ ) : null
+
+ return (
+
+ {/* 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 ? (
+
+ void ghostty.handleClick()}
+ >
+
+ {translate(
+ 'auto.components.settings.TerminalAppearanceSection.855a76343a',
+ 'Import from Ghostty'
+ )}
+
+ ) : null
+ }
+ />
+
+
+
+
+
+ updateSettings({ terminalFontFamily: value })}
+ onPreviewFontFamily={setPreviewFontFamily}
+ />
+ }
+ />
+
+
+
+ {showTypographyAdvancedDisclosure ? (
+
+ ) : null}
+
+ ) : null}
+
+ {showThemeCatalog ? (
+
+ ) : null}
+
-
+
+ {/* Why: Bar/Block/Underline options convey the meaning; helper text pruned. */}
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}
diff --git a/src/renderer/src/components/settings/TerminalFontSizeSetting.tsx b/src/renderer/src/components/settings/TerminalFontSizeSetting.tsx
index e3964394a..cc601aa99 100644
--- a/src/renderer/src/components/settings/TerminalFontSizeSetting.tsx
+++ b/src/renderer/src/components/settings/TerminalFontSizeSetting.tsx
@@ -8,10 +8,12 @@ import { translate } from '@/i18n/i18n'
export function TerminalFontSizeSetting({
settings,
- updateSettings
+ updateSettings,
+ forceVisible = false
}: {
settings: GlobalSettings
updateSettings: (updates: Partial) => void
+ forceVisible?: boolean
}): React.JSX.Element {
return (
+ {/* Why: helper text dropped per the copy audit — "Font Size" + px control
+ is self-evident; the search index keeps the longer description. */}
-
+
(preferredTarget ?? 'dark')
const themeOptions = getAvailableTerminalThemeOptions(settings)
@@ -78,25 +80,23 @@ export function TerminalThemeCatalogSection({
return (
+ ) : null
+ }
/>
- {showThemeImport ? (
-
-
-
-
- ) : null}
-
-
-
+
+
+ {advancedContent ?
{advancedContent}
: null}
+
-
-
+
- updateSettings({ terminalBackgroundOpacity: clampNumber(value, 0, 1) })
- }
- />
-
+ keywords={['opacity', 'transparency', 'background', 'alpha']}
+ >
+
+ updateSettings({ terminalBackgroundOpacity: clampNumber(value, 0, 1) })
+ }
+ />
+
-
-
-
-
- {translate(
- 'auto.components.settings.TerminalWindowSection.2b82242f43',
- 'Window Blur'
- )}
-
-
- {translate(
- 'auto.components.settings.TerminalWindowSection.97950bb087',
- 'Apply background blur to the terminal window. Requires restart.'
- )}
-
-
-
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'
- }`}
- >
-
-
-
-
- {blurPendingRestart ? (
-
-
-
+
+
+
+
{translate(
- 'auto.components.settings.TerminalWindowSection.c65bb9ce63',
- 'Restart required'
+ 'auto.components.settings.TerminalWindowSection.2b82242f43',
+ 'Window Blur'
)}
-
+
{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.'
)}
-
void handleRelaunch()}
+
+ 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'
+ }`}
>
-
- {relaunchingBlur
- ? translate(
- 'auto.components.settings.TerminalWindowSection.907131d741',
- 'Restarting…'
- )
- : translate(
- 'auto.components.settings.TerminalWindowSection.8abdab9f7c',
- 'Restart now'
- )}
-
+
+
- ) : null}
-
-
-
+
+
+ {translate(
+ 'auto.components.settings.TerminalWindowSection.c65bb9ce63',
+ 'Restart required'
+ )}
+
+
+ {translate(
+ 'auto.components.settings.TerminalWindowSection.53ce336e15',
+ 'Restart Orca to apply the window blur change.'
+ )}
+
+
+ void handleRelaunch()}
+ >
+
+ {relaunchingBlur
+ ? translate(
+ 'auto.components.settings.TerminalWindowSection.907131d741',
+ 'Restarting…'
+ )
+ : translate(
+ 'auto.components.settings.TerminalWindowSection.8abdab9f7c',
+ 'Restart now'
+ )}
+
+
+ ) : null}
+
+
+
updateSettings({ terminalPaddingX: Math.max(0, value) })}
- />
-
+ keywords={['padding', 'horizontal', 'spacing', 'margin']}
+ >
+
updateSettings({ terminalPaddingX: Math.max(0, value) })}
+ />
+
-
- updateSettings({ terminalPaddingY: Math.max(0, value) })}
- />
-
-
-
-
-
- {translate(
- 'auto.components.settings.TerminalWindowSection.3530908ef9',
- 'Hide Mouse While Typing'
- )}
-
-
- {translate(
- 'auto.components.settings.TerminalWindowSection.1d1920dc8a',
- 'Hide the mouse cursor when typing in the terminal.'
- )}
-
-
-
- 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']}
>
-
-
-
-
-
-
-
setColorOverridesExpanded((prev) => !prev)}
- className="flex items-center gap-2 text-sm font-medium"
- >
-
- ▶
-
- {translate(
- 'auto.components.settings.TerminalWindowSection.63f8d9336e',
- 'Color Overrides'
+
- updateSettings({ terminalPaddingY: Math.max(0, value) })}
+ />
+
+
+
+
+ {/* Why: helper text dropped per copy audit — near-verbatim restatement
+ of the label; the search index keeps the longer phrasing. */}
+
+ {translate(
+ 'auto.components.settings.TerminalWindowSection.3530908ef9',
+ 'Hide Mouse While Typing'
+ )}
+
+
+
+ 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'
}`}
>
-
- {COLOR_OVERRIDE_GROUPS.map((group) => (
-
-
{group.label}
-
- {group.keys.map((item) => (
-
- updateSettings({
- terminalColorOverrides: {
- ...settings.terminalColorOverrides,
- [item.key]: value || undefined
- }
- })
- }
- />
- ))}
+
+
+
+
+
+
+
setColorOverridesExpanded((prev) => !prev)}
+ className="flex items-center gap-2 text-sm font-medium"
+ >
+
+ ▶
+
+ {translate(
+ 'auto.components.settings.TerminalWindowSection.63f8d9336e',
+ 'Color Overrides'
+ )}
+
+
+
+ {COLOR_OVERRIDE_GROUPS.map((group) => (
+
+
{group.label}
+
+ {group.keys.map((item) => (
+
+ updateSettings({
+ terminalColorOverrides: {
+ ...settings.terminalColorOverrides,
+ [item.key]: value || undefined
+ }
+ })
+ }
+ />
+ ))}
+
-
- ))}
-
updateSettings({ terminalColorOverrides: undefined })}
- >
- {translate(
- 'auto.components.settings.TerminalWindowSection.03c855d15f',
- 'Reset all color overrides'
- )}
-
+ ))}
+
updateSettings({ terminalColorOverrides: undefined })}
+ >
+ {translate(
+ 'auto.components.settings.TerminalWindowSection.03c855d15f',
+ 'Reset all color overrides'
+ )}
+
+
-
-
+
+
)
}
diff --git a/src/renderer/src/components/settings/appearance-search.ts b/src/renderer/src/components/settings/appearance-search.ts
index b0d3d8eb8..7284b5db3 100644
--- a/src/renderer/src/components/settings/appearance-search.ts
+++ b/src/renderer/src/components/settings/appearance-search.ts
@@ -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(),
diff --git a/src/renderer/src/components/settings/appearance-sidebar-search.ts b/src/renderer/src/components/settings/appearance-sidebar-search.ts
index d723e3c4a..fd66f0f00 100644
--- a/src/renderer/src/components/settings/appearance-sidebar-search.ts
+++ b/src/renderer/src/components/settings/appearance-sidebar-search.ts
@@ -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(
diff --git a/src/renderer/src/components/settings/terminal-search.test.ts b/src/renderer/src/components/settings/terminal-search.test.ts
index 88041fdf8..60ff53fef 100644
--- a/src/renderer/src/components/settings/terminal-search.test.ts
+++ b/src/renderer/src/components/settings/terminal-search.test.ts
@@ -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'])(
diff --git a/src/renderer/src/components/settings/terminal-search.ts b/src/renderer/src/components/settings/terminal-search.ts
index 3b06d4a3b..783f7085d 100644
--- a/src/renderer/src/components/settings/terminal-search.ts
+++ b/src/renderer/src/components/settings/terminal-search.ts
@@ -34,6 +34,7 @@ import {
import { createLocalizedCatalog } from '@/i18n/localized-catalog'
export {
+ getTerminalAdvancedTypographySearchEntries,
getTerminalTypographySearchEntries,
getTerminalRenderingSearchEntries,
getTerminalCursorSearchEntries
diff --git a/src/renderer/src/components/settings/terminal-typography-search.ts b/src/renderer/src/components/settings/terminal-typography-search.ts
index ed04dcc07..02fe124d2 100644
--- a/src/renderer/src/components/settings/terminal-typography-search.ts
+++ b/src/renderer/src/components/settings/terminal-typography-search.ts
@@ -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'),
diff --git a/src/renderer/src/components/terminal-pane/pty-connection.test.ts b/src/renderer/src/components/terminal-pane/pty-connection.test.ts
index 5d7fec5c1..ef0430160 100644
--- a/src/renderer/src/components/terminal-pane/pty-connection.test.ts
+++ b/src/renderer/src/components/terminal-pane/pty-connection.test.ts
@@ -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')
diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts
index 64bbae244..7475a1d26 100644
--- a/src/renderer/src/components/terminal-pane/pty-connection.ts
+++ b/src/renderer/src/components/terminal-pane/pty-connection.ts
@@ -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 | null = null
let sshShellReadyFallbackTimer: ReturnType | null = null
@@ -2619,6 +2621,7 @@ export function connectPanePty(
let hiddenOutputRestoreRetryDeferred = false
let hiddenOutputRestoreScheduled = false
let hiddenOutputRestoreDeferredRetryTimer: ReturnType | null = null
+ let hiddenOutputRestoreForegroundDeadlineTimer: ReturnType | 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
+ 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?.()
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index 0e4ae0698..632f976a5 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -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": {
diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json
index 8a79c6e3a..281be816a 100644
--- a/src/renderer/src/i18n/locales/es.json
+++ b/src/renderer/src/i18n/locales/es.json
@@ -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": {
diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json
index 09040f48d..4e14b21d7 100644
--- a/src/renderer/src/i18n/locales/ja.json
+++ b/src/renderer/src/i18n/locales/ja.json
@@ -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": {
diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json
index 0dc37894a..64591eea9 100644
--- a/src/renderer/src/i18n/locales/ko.json
+++ b/src/renderer/src/i18n/locales/ko.json
@@ -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": {
diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json
index 089b4e141..aea5fd20e 100644
--- a/src/renderer/src/i18n/locales/zh.json
+++ b/src/renderer/src/i18n/locales/zh.json
@@ -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": {
diff --git a/src/shared/agent-process-recognition.test.ts b/src/shared/agent-process-recognition.test.ts
index 865d5105c..297e8e405 100644
--- a/src/shared/agent-process-recognition.test.ts
+++ b/src/shared/agent-process-recognition.test.ts
@@ -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'
})
diff --git a/src/shared/keybindings.test.ts b/src/shared/keybindings.test.ts
index 6dcfcbba4..4d724b630 100644
--- a/src/shared/keybindings.test.ts
+++ b/src/shared/keybindings.test.ts
@@ -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.