Migrate terminal scrollback setting from bytes to rows

Transition terminal scrollback configuration from a byte-based (MB)
limit to a row-based count to align with standard terminal emulator
behavior.

- Introduce a shared scrollback policy to handle normalization and
  safe migration of legacy byte presets to row equivalents.
- Update persistence logic to automatically convert and clean up legacy
  settings on startup and write-back.
- Refactor the advanced settings interface and localization strings
  to let users configure scrollback in terms of rows (up to 50k).
- Live-apply row limit changes directly to mounted xterm instances
  without recreating panes or restarting PTY sessions.
This commit is contained in:
Jinjing
2026-06-27 23:40:28 -07:00
parent 360ce673e7
commit 54403d82fc
27 changed files with 479 additions and 89 deletions
@@ -81,7 +81,7 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
terminalClipboardOnSelect: false,
terminalAllowOsc52Clipboard: false,
setupScriptLaunchMode: 'split-vertical',
terminalScrollbackBytes: 10_000_000,
terminalScrollbackRows: 5_000,
localAccountRuntime: 'host',
localAccountWslDistro: null,
openLinksInApp: false,
+1 -1
View File
@@ -85,7 +85,7 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
terminalClipboardOnSelect: false,
terminalAllowOsc52Clipboard: false,
setupScriptLaunchMode: 'split-vertical',
terminalScrollbackBytes: 10_000_000,
terminalScrollbackRows: 5_000,
localAccountRuntime: 'host',
localAccountWslDistro: null,
openLinksInApp: false,
+21
View File
@@ -285,6 +285,27 @@ describe('registerSettingsHandlers', () => {
)
})
it('normalizes terminal scrollback row updates and drops legacy byte updates', async () => {
store.getSettings.mockReturnValue({ terminalScrollbackRows: 5_000 })
store.updateSettings.mockReturnValue({ terminalScrollbackRows: 50_000 })
registerSettingsHandlers(store as never)
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
_event: unknown,
args: unknown
) => Promise<unknown>
await handler(settingsInvokeEvent, {
terminalScrollbackRows: 75_000,
terminalScrollbackBytes: 250_000_000
})
expect(store.updateSettings).toHaveBeenCalledWith(
{ terminalScrollbackRows: 50_000 },
{ notifyListeners: true, originWebContentsId: 1 }
)
})
it('normalizes custom terminal themes from renderer settings IPC', async () => {
store.getSettings.mockReturnValue({ terminalCustomThemes: [] })
store.updateSettings.mockReturnValue({ terminalCustomThemes: [] })
+18 -1
View File
@@ -17,6 +17,7 @@ import { normalizeAppIconId } from '../../shared/app-icon'
import { normalizeUiLanguage } from '../../shared/ui-language'
import { applyAppIcon } from '../app-icon'
import { normalizeTerminalCustomThemes } from '../../shared/terminal-custom-themes'
import { normalizeDesktopTerminalScrollbackRows } from '../../shared/terminal-scrollback-policy'
import { prepareLocalWorktreeRootsForRepos } from '../worktree-root-preparation'
// Why: the whitelist is the source-of-truth for which keys we emit on. Casting
@@ -24,6 +25,17 @@ import { prepareLocalWorktreeRootsForRepos } from '../worktree-root-preparation'
// check stay O(1) without re-coercing the readonly tuple on every call.
const SETTINGS_CHANGED_WHITELIST_SET = new Set<string>(SETTINGS_CHANGED_WHITELIST)
type LegacyTerminalScrollbackSettingsUpdate = Partial<GlobalSettings> & {
terminalScrollbackBytes?: unknown
}
function sanitizeRendererSettingsUpdate(args: Partial<GlobalSettings>): Partial<GlobalSettings> {
const { terminalScrollbackBytes: _legacyScrollbackBytes, ...sanitizedArgs } =
args as LegacyTerminalScrollbackSettingsUpdate
void _legacyScrollbackBytes
return sanitizedArgs
}
// Why: fields that appear in the View > Appearance submenu need the menu
// rebuilt after any update so the checkbox `checked` state stays in sync
// with the persisted value. Electron doesn't reactively re-render menu
@@ -54,7 +66,7 @@ export function registerSettingsHandlers(
})
ipcMain.handle('settings:set', async (event, args: Partial<GlobalSettings>) => {
const sanitizedArgs = { ...args }
const sanitizedArgs = sanitizeRendererSettingsUpdate(args)
// Why: Floating Workspace grants are trusted only when written by the
// main-process directory picker, never by renderer-provided settings IPC.
delete sanitizedArgs.floatingTerminalTrustedCwds
@@ -77,6 +89,11 @@ export function registerSettingsHandlers(
if ('terminalCustomThemes' in args) {
sanitizedArgs.terminalCustomThemes = normalizeTerminalCustomThemes(args.terminalCustomThemes)
}
if ('terminalScrollbackRows' in args) {
sanitizedArgs.terminalScrollbackRows = normalizeDesktopTerminalScrollbackRows(
args.terminalScrollbackRows
)
}
if ('uiLanguage' in args) {
sanitizedArgs.uiLanguage = normalizeUiLanguage(args.uiLanguage)
}
+96
View File
@@ -3813,6 +3813,102 @@ describe('Store', () => {
expect(listener).not.toHaveBeenCalled()
})
it('migrates missing terminal scrollback rows to the row default and writes back rows only', async () => {
writeDataFile({ settings: {} })
const store = await createStore()
expect(store.getSettings().terminalScrollbackRows).toBe(5_000)
store.flush()
const persisted = readDataFile() as { settings?: Record<string, unknown> }
expect(persisted.settings?.terminalScrollbackRows).toBe(5_000)
expect(persisted.settings).not.toHaveProperty('terminalScrollbackBytes')
})
it('migrates legacy terminal scrollback byte presets by intent', async () => {
writeDataFile({
settings: {
terminalScrollbackBytes: 25_000_000
}
})
const store = await createStore()
expect(store.getSettings().terminalScrollbackRows).toBe(10_000)
store.flush()
const persisted = readDataFile() as { settings?: Record<string, unknown> }
expect(persisted.settings?.terminalScrollbackRows).toBe(10_000)
expect(persisted.settings).not.toHaveProperty('terminalScrollbackBytes')
})
it('lets persisted terminal scrollback rows win over legacy bytes', async () => {
writeDataFile({
settings: {
terminalScrollbackRows: 25_000,
terminalScrollbackBytes: 100_000_000
}
})
const store = await createStore()
expect(store.getSettings().terminalScrollbackRows).toBe(25_000)
store.flush()
const persisted = readDataFile() as { settings?: Record<string, unknown> }
expect(persisted.settings?.terminalScrollbackRows).toBe(25_000)
expect(persisted.settings).not.toHaveProperty('terminalScrollbackBytes')
})
it('normalizes invalid and clamped terminal scrollback rows on load', async () => {
writeDataFile({
settings: {
terminalScrollbackRows: '50000'
}
})
const invalidStore = await createStore()
expect(invalidStore.getSettings().terminalScrollbackRows).toBe(5_000)
invalidStore.flush()
writeDataFile({
settings: {
terminalScrollbackRows: 75_000
}
})
const clampedStore = await createStore()
expect(clampedStore.getSettings().terminalScrollbackRows).toBe(50_000)
})
it('normalizes terminal scrollback row updates and ignores stale byte updates', async () => {
const store = await createStore()
const listener = vi.fn()
store.onSettingsChanged(listener)
const updated = store.updateSettings(
{
terminalScrollbackRows: 75_000,
terminalScrollbackBytes: 250_000_000
} as never,
{ notifyListeners: true }
)
expect(updated.terminalScrollbackRows).toBe(50_000)
expect(listener).toHaveBeenCalledWith(
{ terminalScrollbackRows: 50_000 },
expect.objectContaining({ terminalScrollbackRows: 50_000 }),
undefined
)
store.updateSettings({ terminalScrollbackBytes: 10_000_000 } as never)
store.flush()
const persisted = readDataFile() as { settings?: Record<string, unknown> }
expect(persisted.settings?.terminalScrollbackRows).toBe(50_000)
expect(persisted.settings).not.toHaveProperty('terminalScrollbackBytes')
})
it('normalizes disabled TUI agents on load and update', async () => {
writeFileSync(
join(testState.dir, 'orca-data.json'),
+56 -2
View File
@@ -132,6 +132,10 @@ import { normalizeTerminalShortcutPolicy } from '../shared/keybindings'
import { normalizeSourceControlGroupOrder } from '../shared/source-control-group-order'
import { normalizeAppIconId } from '../shared/app-icon'
import { normalizeTerminalCustomThemes } from '../shared/terminal-custom-themes'
import {
legacyTerminalScrollbackBytesToRows,
normalizeDesktopTerminalScrollbackRows
} from '../shared/terminal-scrollback-policy'
import {
compareFeatureInteractionUsageBuckets,
getFeatureInteractionCategory,
@@ -392,6 +396,46 @@ function buildWorkspaceDirHistoryForUpdate(
return next
}
type LegacyTerminalScrollbackSettings = {
terminalScrollbackRows?: unknown
terminalScrollbackBytes?: unknown
}
function readLegacyTerminalScrollbackSettings(settings: unknown): LegacyTerminalScrollbackSettings {
return settings && typeof settings === 'object'
? (settings as LegacyTerminalScrollbackSettings)
: {}
}
function stripLegacyTerminalScrollbackBytes(
settings: Partial<GlobalSettings> | undefined
): Partial<GlobalSettings> {
const { terminalScrollbackBytes: _legacyScrollbackBytes, ...rest } = (settings ??
{}) as Partial<GlobalSettings> & { terminalScrollbackBytes?: unknown }
void _legacyScrollbackBytes
return rest
}
function migrateTerminalScrollbackRows(settings: unknown): {
rows: number
needsSave: boolean
} {
const legacySettings = readLegacyTerminalScrollbackSettings(settings)
const hasRows = Object.prototype.hasOwnProperty.call(legacySettings, 'terminalScrollbackRows')
const hasLegacyBytes = Object.prototype.hasOwnProperty.call(
legacySettings,
'terminalScrollbackBytes'
)
const rows = hasRows
? normalizeDesktopTerminalScrollbackRows(legacySettings.terminalScrollbackRows)
: legacyTerminalScrollbackBytesToRows(legacySettings.terminalScrollbackBytes)
return {
rows,
needsSave: !hasRows || hasLegacyBytes || legacySettings.terminalScrollbackRows !== rows
}
}
function getWorkspaceLayoutHistoryKey(layout: OrcaWorkspaceLayout): string {
return `${normalizeRuntimePathForComparison(layout.path)}:${layout.nestWorkspaces}`
}
@@ -2522,6 +2566,10 @@ export class Store {
// Merge with defaults in case new fields were added
const homeDir = homedir()
const defaults = getDefaultPersistedState(homeDir)
const migratedTerminalScrollback = migrateTerminalScrollbackRows(parsed.settings)
if (migratedTerminalScrollback.needsSave) {
this.loadNeedsSave = true
}
const rawSourceControlAi = parsed.settings?.sourceControlAi
const rawSourceControlAiMissing = rawSourceControlAi === undefined
const rawSourceControlAiActionsMissing =
@@ -2744,7 +2792,7 @@ export class Store {
),
settings: {
...defaults.settings,
...parsed.settings,
...stripLegacyTerminalScrollbackBytes(parsed.settings),
// Why: v1.3.42 renamed the cosmetic sidekick setting to pet. Carry
// the old persisted flag forward once so enabled users don't lose it.
experimentalPet:
@@ -2780,6 +2828,7 @@ export class Store {
floatingTerminalCwd: migratedFloatingTerminalCwd,
floatingTerminalTrustedCwds: migratedFloatingTerminalTrustedCwds,
floatingTerminalCwdMigratedToAppWorkspace: true,
terminalScrollbackRows: migratedTerminalScrollback.rows,
terminalQuickCommands: normalizeTerminalQuickCommands(
parsed.settings?.terminalQuickCommands
),
@@ -4599,7 +4648,7 @@ export class Store {
updates: Partial<GlobalSettings>,
options: { notifyListeners?: boolean; originWebContentsId?: number } = {}
): GlobalSettings {
const sanitizedUpdates = { ...updates }
const sanitizedUpdates = stripLegacyTerminalScrollbackBytes(updates)
// Why: coerce strictly to boolean here (not at the IPC edge) so every write
// path is covered and a non-bool renderer payload can never persist a
// truthy non-bool that later reads as "tray-minimize on".
@@ -4627,6 +4676,11 @@ export class Store {
updates.terminalCustomThemes
)
}
if ('terminalScrollbackRows' in updates) {
sanitizedUpdates.terminalScrollbackRows = normalizeDesktopTerminalScrollbackRows(
updates.terminalScrollbackRows
)
}
if ('visibleTaskProviders' in updates || 'defaultTaskSource' in updates) {
const taskProviderSettings = normalizeTaskProviderSettings({
visibleTaskProviders:
@@ -14,7 +14,7 @@ import { useSystemPrefersDark } from '@/components/terminal-pane/use-system-pref
import { isMacUserAgent, isWindowsUserAgent } from '@/components/terminal-pane/pane-helpers'
import { applyDocumentTheme } from '@/lib/document-theme'
import { useConfirmationDialog } from '@/components/confirmation-dialog'
import { SCROLLBACK_PRESETS_MB, getFallbackTerminalFonts } from './SettingsConstants'
import { SCROLLBACK_PRESETS_ROWS, getFallbackTerminalFonts } from './SettingsConstants'
import { DEFAULT_APP_FONT_FAMILY, getDefaultVoiceSettings } from '../../../../shared/constants'
import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
import { GeneralPane } from './GeneralPane'
@@ -299,7 +299,7 @@ function Settings(): React.JSX.Element {
// sidebar. We trim platform-only entries on other platforms so search never
// reveals controls that the renderer will intentionally hide.
const [scrollbackMode, setScrollbackMode] = useState<'preset' | 'custom'>('preset')
const [prevScrollbackBytes, setPrevScrollbackBytes] = useState(settings?.terminalScrollbackBytes)
const [prevScrollbackRows, setPrevScrollbackRows] = useState(settings?.terminalScrollbackRows)
// Why: Appearance owns terminal visual controls, but the Ghostty import flow
// still needs Settings-level state so the modal survives section remounts.
const ghostty = useGhosttyImport(updateSettings, settings)
@@ -583,14 +583,15 @@ function Settings(): React.JSX.Element {
clearSettingsTarget()
}, [clearSettingsTarget, settings, settingsNavigationTarget])
// Why: only recompute scrollback mode when the byte value actually changes,
// Why: only recompute scrollback mode when the row value actually changes,
// not on every unrelated settings mutation.
if (settings?.terminalScrollbackBytes !== prevScrollbackBytes) {
setPrevScrollbackBytes(settings?.terminalScrollbackBytes)
if (settings?.terminalScrollbackRows !== prevScrollbackRows) {
setPrevScrollbackRows(settings?.terminalScrollbackRows)
if (settings) {
const scrollbackMb = Math.max(1, Math.round(settings.terminalScrollbackBytes / 1_000_000))
setScrollbackMode(
SCROLLBACK_PRESETS_MB.includes(scrollbackMb as (typeof SCROLLBACK_PRESETS_MB)[number])
SCROLLBACK_PRESETS_ROWS.includes(
settings.terminalScrollbackRows as (typeof SCROLLBACK_PRESETS_ROWS)[number]
)
? 'preset'
: 'custom'
)
@@ -1,10 +1,11 @@
import type { OrcaHooks } from '../../../../shared/types'
import { getDefaultRepoHookSettings } from '../../../../shared/constants'
import { DESKTOP_TERMINAL_SCROLLBACK_ROW_PRESETS } from '../../../../shared/terminal-scrollback-policy'
export type HookName = keyof OrcaHooks['scripts']
export const DEFAULT_REPO_HOOK_SETTINGS = getDefaultRepoHookSettings()
export const MAX_THEME_RESULTS = 80
export const SCROLLBACK_PRESETS_MB = [10, 25, 50, 100, 250] as const
export const SCROLLBACK_PRESETS_ROWS = DESKTOP_TERMINAL_SCROLLBACK_ROW_PRESETS
export const ZOOM_STEP = 0.5
export const ZOOM_MIN = -3
export const ZOOM_MAX = 5
@@ -1,13 +1,17 @@
import type { GlobalSettings } from '../../../../shared/types'
import {
DESKTOP_TERMINAL_SCROLLBACK_ROWS_MAX,
DESKTOP_TERMINAL_SCROLLBACK_ROWS_MIN,
normalizeDesktopTerminalScrollbackRows
} from '../../../../shared/terminal-scrollback-policy'
import { Input } from '../ui/input'
import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group'
import { clampNumber } from '@/lib/terminal-theme'
import {
SettingsRow,
SettingsSegmentedControl,
SettingsSubsectionHeader
} from './SettingsFormControls'
import { SCROLLBACK_PRESETS_MB } from './SettingsConstants'
import { SCROLLBACK_PRESETS_ROWS } from './SettingsConstants'
import { SearchableSetting } from './SearchableSetting'
import { matchesSettingsSearch } from './settings-search'
import { getTerminalWindowsPowershellImplementationSearchEntry } from './terminal-windows-search'
@@ -25,6 +29,10 @@ type TerminalAdvancedSectionProps = {
isMac: boolean
}
function formatScrollbackRowsLabel(rows: number): string {
return rows % 1_000 === 0 ? `${rows / 1_000}k` : String(rows)
}
export function TerminalAdvancedSection({
settings,
updateSettings,
@@ -35,12 +43,12 @@ export function TerminalAdvancedSection({
pwshAvailable,
isMac
}: TerminalAdvancedSectionProps): React.JSX.Element {
const scrollbackMb = Math.max(1, Math.round(settings.terminalScrollbackBytes / 1_000_000))
const isPreset = SCROLLBACK_PRESETS_MB.includes(
scrollbackMb as (typeof SCROLLBACK_PRESETS_MB)[number]
const scrollbackRows = normalizeDesktopTerminalScrollbackRows(settings.terminalScrollbackRows)
const isPreset = SCROLLBACK_PRESETS_ROWS.includes(
scrollbackRows as (typeof SCROLLBACK_PRESETS_ROWS)[number]
)
const scrollbackToggleValue =
scrollbackMode === 'custom' ? 'custom' : isPreset ? `${scrollbackMb}` : 'custom'
scrollbackMode === 'custom' ? 'custom' : isPreset ? `${scrollbackRows}` : 'custom'
const powerShellImplementation = settings.terminalWindowsPowerShellImplementation ?? 'auto'
return (
@@ -55,19 +63,19 @@ export function TerminalAdvancedSection({
<div className="divide-y divide-border/40">
<SearchableSetting
title={translate('auto.components.settings.TerminalPane.9df53f7c14', 'Scrollback Size')}
title={translate('auto.components.settings.TerminalPane.9df53f7c14', 'Scrollback Rows')}
description={translate(
'auto.components.settings.TerminalPane.c3810b2b42',
'Maximum terminal scrollback buffer size.'
'Retained desktop terminal rows.'
)}
keywords={['terminal', 'scrollback', 'buffer', 'memory']}
keywords={['terminal', 'scrollback', 'rows', 'buffer', 'memory']}
>
<SettingsRow
alignTop={scrollbackMode === 'custom'}
label={translate('auto.components.settings.TerminalPane.9df53f7c14', 'Scrollback Size')}
label={translate('auto.components.settings.TerminalPane.9df53f7c14', 'Scrollback Rows')}
description={translate(
'auto.components.settings.TerminalPane.81d86b2dd2',
'Maximum terminal scrollback buffer size for new terminal panes.'
'Retained desktop terminal rows for new and open panes.'
)}
control={
<div className="flex flex-col items-end gap-2">
@@ -85,25 +93,25 @@ export function TerminalAdvancedSection({
setScrollbackMode('preset')
updateSettings({
terminalScrollbackBytes: Number(value) * 1_000_000
terminalScrollbackRows: normalizeDesktopTerminalScrollbackRows(Number(value))
})
}}
variant="outline"
size="sm"
className="h-8 flex-wrap justify-end"
>
{SCROLLBACK_PRESETS_MB.map((preset) => (
{SCROLLBACK_PRESETS_ROWS.map((preset) => (
<ToggleGroupItem
key={preset}
value={`${preset}`}
className="h-8 px-3 text-xs"
aria-label={translate(
'auto.components.settings.TerminalPane.5336c096af',
'{{value0}} megabytes',
'{{value0}} rows',
{ value0: preset }
)}
>
{preset} {translate('auto.components.settings.TerminalPane.12e06178fa', 'MB')}
{formatScrollbackRowsLabel(preset)}
</ToggleGroupItem>
))}
<ToggleGroupItem
@@ -121,22 +129,22 @@ export function TerminalAdvancedSection({
<div className="flex items-center gap-2">
<Input
type="number"
min={1}
max={256}
step={1}
value={scrollbackMb}
min={DESKTOP_TERMINAL_SCROLLBACK_ROWS_MIN}
max={DESKTOP_TERMINAL_SCROLLBACK_ROWS_MAX}
step={100}
value={scrollbackRows}
onChange={(e) => {
const value = Number(e.target.value)
if (Number.isFinite(value)) {
updateSettings({
terminalScrollbackBytes: clampNumber(value, 1, 256) * 1_000_000
terminalScrollbackRows: normalizeDesktopTerminalScrollbackRows(value)
})
}
}}
className="number-input-clean w-24 tabular-nums"
/>
<span className="text-xs text-muted-foreground">
{translate('auto.components.settings.TerminalPane.12e06178fa', 'MB')}
{translate('auto.components.settings.TerminalPane.12e06178fa', 'rows')}
</span>
</div>
) : null}
@@ -280,7 +280,7 @@ describe('TerminalPane PowerShell version setting', () => {
it('shows the PowerShell 7+ download link when pwsh is unavailable', () => {
const element = TerminalPane({
settings: {
terminalScrollbackBytes: 10_000_000,
terminalScrollbackRows: 5_000,
terminalWindowsShell: 'powershell.exe',
terminalWindowsPowerShellImplementation: 'powershell.exe',
terminalWordSeparator: ''
@@ -302,7 +302,7 @@ describe('TerminalPane PowerShell version setting', () => {
it('does not show WSL as a Windows default shell option when available', () => {
const element = TerminalPane({
settings: {
terminalScrollbackBytes: 10_000_000,
terminalScrollbackRows: 5_000,
terminalWindowsShell: 'powershell.exe',
terminalWindowsPowerShellImplementation: 'auto',
terminalWordSeparator: ''
@@ -325,7 +325,7 @@ describe('TerminalPane PowerShell version setting', () => {
it('shows Windows shell controls for a remote Windows host on a non-Windows client', () => {
const element = TerminalPane({
settings: {
terminalScrollbackBytes: 10_000_000,
terminalScrollbackRows: 5_000,
terminalWindowsShell: 'powershell.exe',
terminalWindowsPowerShellImplementation: 'auto',
terminalWordSeparator: ''
@@ -349,7 +349,7 @@ describe('TerminalPane PowerShell version setting', () => {
it('hides WSL as a Windows default shell option when unavailable', () => {
const element = TerminalPane({
settings: {
terminalScrollbackBytes: 10_000_000,
terminalScrollbackRows: 5_000,
terminalWindowsShell: 'powershell.exe',
terminalWindowsPowerShellImplementation: 'auto',
terminalWordSeparator: ''
@@ -368,7 +368,7 @@ describe('TerminalPane PowerShell version setting', () => {
it('does not show WSL distro choices for a persisted legacy WSL shell', () => {
const element = TerminalPane({
settings: {
terminalScrollbackBytes: 10_000_000,
terminalScrollbackRows: 5_000,
terminalWindowsShell: 'wsl.exe',
terminalWindowsWslDistro: 'Debian',
terminalWindowsPowerShellImplementation: 'auto',
@@ -395,7 +395,7 @@ describe('TerminalPane PowerShell version setting', () => {
it('shows Git Bash as a Windows default shell option when bash.exe is detected', () => {
const element = TerminalPane({
settings: {
terminalScrollbackBytes: 10_000_000,
terminalScrollbackRows: 5_000,
terminalWindowsShell: 'powershell.exe',
terminalWindowsPowerShellImplementation: 'auto',
terminalWordSeparator: ''
@@ -415,7 +415,7 @@ describe('TerminalPane PowerShell version setting', () => {
it('hides Git Bash as a Windows default shell option when not detected', () => {
const element = TerminalPane({
settings: {
terminalScrollbackBytes: 10_000_000,
terminalScrollbackRows: 5_000,
terminalWindowsShell: 'powershell.exe',
terminalWindowsPowerShellImplementation: 'auto',
terminalWordSeparator: ''
@@ -4,10 +4,10 @@ import { createLocalizedCatalog } from '@/i18n/localized-catalog'
export const getTerminalAdvancedSearchEntries = createLocalizedCatalog(() => [
{
title: translate('auto.components.settings.terminal.search.7674e758e1', 'Scrollback Size'),
title: translate('auto.components.settings.terminal.search.7674e758e1', 'Scrollback Rows'),
description: translate(
'auto.components.settings.terminal.search.f7d56b6281',
'Maximum terminal scrollback buffer size.'
'Retained desktop terminal rows.'
),
keywords: [
...translateSearchKeyword('auto.components.settings.terminal.search.f66a7cf715', 'terminal'),
@@ -15,6 +15,7 @@ export const getTerminalAdvancedSearchEntries = createLocalizedCatalog(() => [
'auto.components.settings.terminal.search.cde233f5da',
'scrollback'
),
...translateSearchKeyword('auto.components.settings.terminal.search.rows', 'rows'),
...translateSearchKeyword('auto.components.settings.terminal.search.fffdff40a7', 'buffer'),
...translateSearchKeyword('auto.components.settings.terminal.search.56fff3d113', 'memory')
]
@@ -72,6 +72,15 @@ describe('getTerminalPaneSearchEntries', () => {
expect(entriesLinux.some((entry) => entry.title === 'Manage Sessions')).toBe(true)
})
it('indexes terminal scrollback as rows rather than MB size', () => {
const entries = getTerminalPaneSearchEntries({ isWindows: false, isMac: false })
const scrollbackEntry = entries.find((entry) => entry.title === 'Scrollback Rows')
expect(scrollbackEntry).toBeDefined()
expect(matchesSettingsSearch('rows', [scrollbackEntry!])).toBe(true)
expect(entries.some((entry) => entry.title === 'Scrollback Size')).toBe(false)
})
it('includes the OSC 52 clipboard setting on all platforms', () => {
const entriesWindows = getTerminalPaneSearchEntries({ isWindows: true, isMac: false })
const entriesMac = getTerminalPaneSearchEntries({ isWindows: false, isMac: true })
@@ -13,7 +13,7 @@ const baseSettings: GlobalSettings = {
terminalGpuAcceleration: 'auto',
terminalCursorStyle: 'bar',
terminalCursorBlink: true,
terminalScrollbackBytes: 10_000_000,
terminalScrollbackRows: 5_000,
terminalBackgroundOpacity: 1,
terminalInactivePaneOpacity: 1,
terminalPaddingX: 0,
@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import {
applyTerminalScrollbackRowsToMountedPanes,
mapRestoredPaneTitlesByPaneId,
shouldDetachPaneTransportOnUnmount,
splitPaneWithOneShotStartup,
@@ -79,6 +80,36 @@ describe('splitPaneWithOneShotStartup', () => {
})
})
describe('applyTerminalScrollbackRowsToMountedPanes', () => {
it('updates mounted pane xterm scrollback options only when needed', () => {
const firstOptions = { scrollback: 1_000 }
const secondOptions = { scrollback: 5_000 }
const firstTerminal = { options: firstOptions }
let secondWrites = 0
const secondTerminal = {
options: {
get scrollback() {
return secondOptions.scrollback
},
set scrollback(value: number | undefined) {
secondWrites += 1
secondOptions.scrollback = value ?? 0
}
}
}
const manager = {
getPanes: vi.fn(() => [{ terminal: firstTerminal }, { terminal: secondTerminal }])
}
applyTerminalScrollbackRowsToMountedPanes(manager, 5_000)
expect(firstTerminal.options.scrollback).toBe(5_000)
expect(secondOptions.scrollback).toBe(5_000)
expect(secondWrites).toBe(0)
expect(manager.getPanes).toHaveBeenCalledTimes(1)
})
})
describe('shouldDetachPaneTransportOnUnmount', () => {
it('detaches when the tab still owns the transport PTY', () => {
expect(
@@ -9,6 +9,7 @@ import {
normalizeTerminalScrollSensitivity,
resolveTerminalCursorInactiveStyle
} from '@/lib/pane-manager/pane-terminal-options'
import { normalizeDesktopTerminalScrollbackRows } from '../../../../shared/terminal-scrollback-policy'
import { normalizeTerminalTuiMouseWheelMultiplier } from '@/lib/pane-manager/pane-terminal-mouse-wheel'
import { buildWindowsPtyCompatibilityOptions } from '@/lib/pane-manager/windows-pty-compatibility'
import { useAppStore } from '@/store'
@@ -109,6 +110,21 @@ export function recordRuntimeCreatedTerminalPaneSplit(
return recordCreatedTerminalPaneSplit(createdPane, args)
}
type TerminalScrollbackPaneManager = {
getPanes(): { terminal: Pick<Terminal, 'options'> }[]
}
export function applyTerminalScrollbackRowsToMountedPanes(
manager: TerminalScrollbackPaneManager,
rows: number
): void {
for (const pane of manager.getPanes()) {
if (pane.terminal.options.scrollback !== rows) {
pane.terminal.options.scrollback = rows
}
}
}
function extractUncHost(value: string | undefined): string | null {
const match = /^(?:\\\\|\/\/)([^\\/]+)/.exec(value ?? '')
return match?.[1] || null
@@ -419,6 +435,9 @@ export function useTerminalPaneLifecycle({
setPaneCount,
setPaneLayoutRevision
}: UseTerminalPaneLifecycleDeps): void {
const terminalScrollbackRows = normalizeDesktopTerminalScrollbackRows(
settings?.terminalScrollbackRows
)
const systemPrefersDarkRef = useRef(systemPrefersDark)
systemPrefersDarkRef.current = systemPrefersDark
const linkProviderDisposablesRef = useRef(new Map<number, IDisposable>())
@@ -1150,12 +1169,8 @@ export function useTerminalPaneLifecycle({
fontFamily: buildFontFamily(currentSettings?.terminalFontFamily ?? ''),
fontWeight: terminalFontWeights.fontWeight,
fontWeightBold: terminalFontWeights.fontWeightBold,
scrollback: Math.min(
50_000,
Math.max(
1000,
Math.round((currentSettings?.terminalScrollbackBytes ?? 10_000_000) / 200)
)
scrollback: normalizeDesktopTerminalScrollbackRows(
currentSettings?.terminalScrollbackRows
),
cursorStyle,
cursorInactiveStyle: resolveTerminalCursorInactiveStyle(cursorStyle),
@@ -1528,6 +1543,16 @@ export function useTerminalPaneLifecycle({
managerRef.current?.setTerminalGpuAcceleration(settings?.terminalGpuAcceleration ?? 'auto')
}, [settings?.terminalGpuAcceleration, managerRef])
useEffect(() => {
const manager = managerRef.current
if (!manager) {
return
}
// Why: live row retention changes are xterm option updates only; they must
// not recreate panes, replay snapshots, refit, resize, or signal the PTY.
applyTerminalScrollbackRowsToMountedPanes(manager, terminalScrollbackRows)
}, [managerRef, terminalScrollbackRows])
useEffect(() => {
const manager = managerRef.current
if (!manager) {
+8 -7
View File
@@ -6267,12 +6267,12 @@
"3d88af864d": "Choose whether the PowerShell shell option launches Windows PowerShell or PowerShell 7+ for new terminal panes.",
"8a956cc91e": "Characters treated as word boundaries for double-click selection.",
"4bebcc2b2c": "Word Separators",
"12e06178fa": "MB",
"12e06178fa": "rows",
"907b0b9d3e": "Custom",
"5336c096af": "{{value0}} megabytes",
"81d86b2dd2": "Maximum terminal scrollback buffer size for new terminal panes.",
"9df53f7c14": "Scrollback Size",
"c3810b2b42": "Maximum terminal scrollback buffer size.",
"5336c096af": "{{value0}} rows",
"81d86b2dd2": "Retained desktop terminal rows for new and open panes.",
"9df53f7c14": "Scrollback Rows",
"c3810b2b42": "Retained desktop terminal rows.",
"267d020745": "Scrollback, word boundaries, and platform-specific terminal behaviors.",
"5e5f06c82c": "Advanced",
"003df129fe": "Split Horizontally",
@@ -7911,8 +7911,9 @@
"957a0203fc": "Word Separators",
"56fff3d113": "memory",
"fffdff40a7": "buffer",
"f7d56b6281": "Maximum terminal scrollback buffer size.",
"7674e758e1": "Scrollback Size",
"rows": "rows",
"f7d56b6281": "Retained desktop terminal rows.",
"7674e758e1": "Scrollback Rows",
"411229c636": "light",
"781f49d942": "divider",
"77d9f9cd55": "Controls the split divider line between panes in light mode.",
+8 -7
View File
@@ -6230,12 +6230,12 @@
"3d88af864d": "Elija si la opción de shell de PowerShell inicia Windows PowerShell o PowerShell 7+ para nuevos paneles de terminal.",
"8a956cc91e": "Caracteres tratados como límites de palabras para la selección con doble clic.",
"4bebcc2b2c": "Separadores de palabras",
"12e06178fa": "MEGABYTE",
"12e06178fa": "filas",
"907b0b9d3e": "Costumbre",
"5336c096af": "{{value0}} megabytes",
"81d86b2dd2": "Tamaño máximo del búfer de desplazamiento hacia atrás del terminal para nuevos paneles de terminal.",
"9df53f7c14": "Tamaño de desplazamiento hacia atrás",
"c3810b2b42": "Tamaño máximo del búfer de desplazamiento hacia atrás del terminal.",
"5336c096af": "{{value0}} filas",
"81d86b2dd2": "Filas de terminal de escritorio retenidas para paneles nuevos y abiertos.",
"9df53f7c14": "Filas de desplazamiento hacia atrás",
"c3810b2b42": "Filas de terminal de escritorio retenidas.",
"267d020745": "Desplazamiento hacia atrás, límites de palabras y comportamientos de terminal específicos de la plataforma.",
"5e5f06c82c": "Avanzado",
"003df129fe": "Dividir horizontalmente",
@@ -7860,8 +7860,9 @@
"957a0203fc": "Separadores de palabras",
"56fff3d113": "memoria",
"fffdff40a7": "buffer",
"f7d56b6281": "Tamaño máximo del búfer de desplazamiento hacia atrás del terminal.",
"7674e758e1": "Tamaño de desplazamiento hacia atrás",
"rows": "filas",
"f7d56b6281": "Filas de terminal de escritorio retenidas.",
"7674e758e1": "Filas de desplazamiento hacia atrás",
"411229c636": "luz",
"781f49d942": "divisor",
"77d9f9cd55": "Controla la línea divisoria dividida entre paneles en modo claro.",
+8 -7
View File
@@ -6252,12 +6252,12 @@
"3d88af864d": "PowerShell シェル オプションで、新規 terminal ペインに対して Windows PowerShell を起動するか PowerShell 7+ を起動するかを選択します。",
"8a956cc91e": "文字はダブルクリック選択の単語境界として扱われます。",
"4bebcc2b2c": "単語の区切り文字",
"12e06178fa": "MB",
"12e06178fa": "行",
"907b0b9d3e": "カスタム",
"5336c096af": "{{value0}}メガバイト",
"81d86b2dd2": "新規 terminal ペインの最大 terminal スクロールバック バッファ サイズ。",
"9df53f7c14": "スクロールバックのサイズ",
"c3810b2b42": "terminal スクロールバック バッファの最大サイズ。",
"5336c096af": "{{value0}} 行",
"81d86b2dd2": "新規および開いているペインで保持するデスクトップ terminal 行数。",
"9df53f7c14": "スクロールバック行数",
"c3810b2b42": "保持するデスクトップ terminal 行数。",
"267d020745": "スクロールバック、単語境界、およびプラットフォーム固有の terminal 動作。",
"5e5f06c82c": "詳細設定",
"003df129fe": "水平方向に分割",
@@ -7896,8 +7896,9 @@
"957a0203fc": "単語の区切り文字",
"56fff3d113": "メモリ",
"fffdff40a7": "バッファ",
"f7d56b6281": "terminal スクロールバック バッファの最大サイズ。",
"7674e758e1": "スクロールバックのサイズ",
"rows": "行",
"f7d56b6281": "保持するデスクトップ terminal 行数。",
"7674e758e1": "スクロールバック行数",
"411229c636": "ライト",
"781f49d942": "ディバイダー",
"77d9f9cd55": "ライト モードでペイン間の分割分割線を制御します。",
+8 -7
View File
@@ -6215,12 +6215,12 @@
"3d88af864d": "PowerShell 셸 옵션이 새 terminal 패널에 대해 Windows PowerShell을 시작할지 아니면 PowerShell 7+를 시작할지 선택합니다.",
"8a956cc91e": "두 번 클릭 선택 시 단어 경계로 처리되는 문자입니다.",
"4bebcc2b2c": "단어 구분 기호",
"12e06178fa": "MB",
"12e06178fa": "행",
"907b0b9d3e": "사용자 지정",
"5336c096af": "{{value0}} 메가바이트",
"81d86b2dd2": "새 terminal 패널의 최대 terminal 스크롤백 버퍼 크기입니다.",
"9df53f7c14": "스크롤백 크기",
"c3810b2b42": "최대 terminal 스크롤백 버퍼 크기.",
"5336c096af": "{{value0}} 행",
"81d86b2dd2": "새 패널과 열린 패널에 보관할 데스크톱 terminal 행입니다.",
"9df53f7c14": "스크롤백 행",
"c3810b2b42": "보관할 데스크톱 terminal 행입니다.",
"267d020745": "스크롤백, 단어 경계 및 플랫폼별 terminal 동작.",
"5e5f06c82c": "고급",
"003df129fe": "수평으로 분할",
@@ -7845,8 +7845,9 @@
"957a0203fc": "단어 구분 기호",
"56fff3d113": "메모리",
"fffdff40a7": "버퍼",
"f7d56b6281": "최대 terminal 스크롤백 버퍼 크기입니다.",
"7674e758e1": "스크롤백 크기",
"rows": "행",
"f7d56b6281": "보관할 데스크톱 terminal 행입니다.",
"7674e758e1": "스크롤백 행",
"411229c636": "라이트",
"781f49d942": "분할기",
"77d9f9cd55": "라이트 모드에서 창 사이의 분할 구분선을 제어합니다.",
+8 -7
View File
@@ -6215,12 +6215,12 @@
"3d88af864d": "选择 PowerShell shell 选项是为新终端窗格启动 Windows PowerShell 还是 PowerShell 7+。",
"8a956cc91e": "双击选择时将字符视为单词边界。",
"4bebcc2b2c": "单词分隔符",
"12e06178fa": "MB",
"12e06178fa": "行",
"907b0b9d3e": "自定义",
"5336c096af": "{{value0}} 兆字节",
"81d86b2dd2": "新终端窗格的最大终端回滚缓冲区大小。",
"9df53f7c14": "回滚大小",
"c3810b2b42": "最大终端回滚缓冲区大小。",
"5336c096af": "{{value0}} 行",
"81d86b2dd2": "为新窗格和已打开窗格保留的桌面终端行数。",
"9df53f7c14": "回滚行数",
"c3810b2b42": "保留的桌面终端行数。",
"267d020745": "回滚、字边界和特定于平台的终端行为。",
"5e5f06c82c": "高级",
"003df129fe": "水平分割",
@@ -7845,8 +7845,9 @@
"957a0203fc": "单词分隔符",
"56fff3d113": "记忆",
"fffdff40a7": "缓冲",
"f7d56b6281": "最大终端回滚缓冲区大小。",
"7674e758e1": "回滚大小",
"rows": "行",
"f7d56b6281": "保留的桌面终端行数。",
"7674e758e1": "回滚行数",
"411229c636": "浅色",
"781f49d942": "分隔线",
"77d9f9cd55": "控制浅色模式下窗格之间的分割分隔线。",
@@ -88,6 +88,10 @@ describe('buildDefaultTerminalOptions', () => {
expect(buildDefaultTerminalOptions().scrollbar?.width).toBe(7)
})
it('uses the shared desktop scrollback row default', () => {
expect(buildDefaultTerminalOptions().scrollback).toBe(5_000)
})
it('slightly increases default terminal wheel scrolling while preserving fast scroll', () => {
const options = buildDefaultTerminalOptions()
@@ -1,4 +1,5 @@
import type { ITerminalOptions } from '@xterm/xterm'
import { DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT } from '../../../../shared/terminal-scrollback-policy'
type TerminalCursorStyle = NonNullable<ITerminalOptions['cursorStyle']>
type TerminalCursorInactiveStyle = NonNullable<ITerminalOptions['cursorInactiveStyle']>
@@ -40,7 +41,7 @@ export function buildDefaultTerminalOptions(): ITerminalOptions {
'"SF Mono", "Menlo", "Monaco", "Cascadia Mono", "Consolas", "DejaVu Sans Mono", "Liberation Mono", "Symbols Nerd Font Mono", "MesloLGS Nerd Font", "JetBrainsMono Nerd Font", "Hack Nerd Font", monospace',
fontWeight: '300',
fontWeightBold: '500',
scrollback: 10000,
scrollback: DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT,
// Why: Orca's default terminal cells are taller than many users' baseline
// terminal, so a small multiplier keeps row-per-wheel movement familiar.
scrollSensitivity: DEFAULT_TERMINAL_SCROLL_SENSITIVITY,
+13 -1
View File
@@ -21,6 +21,7 @@ import {
} from '../../../../shared/tui-agent-launch-defaults'
import { bumpProviderRuntimeSessionGeneration } from '@/lib/provider-runtime-context'
import { normalizeUiLanguage } from '../../../../shared/ui-language'
import { normalizeDesktopTerminalScrollbackRows } from '../../../../shared/terminal-scrollback-policy'
import { translate } from '@/i18n/i18n'
export type SettingsSlice = SettingsSearchState & {
@@ -30,6 +31,10 @@ export type SettingsSlice = SettingsSearchState & {
switchRuntimeEnvironment: (environmentId: string | null) => Promise<boolean>
}
type LegacyTerminalScrollbackSettingsUpdate = Partial<GlobalSettings> & {
terminalScrollbackBytes?: unknown
}
function normalizeRuntimeEnvironmentId(value: string | null | undefined): string | null {
const trimmed = value?.trim()
return trimmed ? trimmed : null
@@ -76,7 +81,9 @@ export const createSettingsSlice: StateCreator<AppState, [], [], SettingsSlice>
updateSettings: async (updates) => {
try {
const sanitizedUpdates = { ...updates }
const { terminalScrollbackBytes: _legacyScrollbackBytes, ...sanitizedUpdates } =
updates as LegacyTerminalScrollbackSettingsUpdate
void _legacyScrollbackBytes
if ('terminalQuickCommands' in updates) {
sanitizedUpdates.terminalQuickCommands = normalizeTerminalQuickCommands(
updates.terminalQuickCommands
@@ -123,6 +130,11 @@ export const createSettingsSlice: StateCreator<AppState, [], [], SettingsSlice>
if ('uiLanguage' in updates) {
sanitizedUpdates.uiLanguage = normalizeUiLanguage(updates.uiLanguage)
}
if ('terminalScrollbackRows' in updates) {
sanitizedUpdates.terminalScrollbackRows = normalizeDesktopTerminalScrollbackRows(
updates.terminalScrollbackRows
)
}
const nextSettings = await window.api.settings.set(sanitizedUpdates)
set((s) => ({ settings: (nextSettings as GlobalSettings | undefined) ?? s.settings }))
} catch (err) {
+2 -1
View File
@@ -29,6 +29,7 @@ import {
DEFAULT_LEFT_SIDEBAR_TINT_OPACITY
} from './left-sidebar-appearance'
import { DEFAULT_SOURCE_CONTROL_GROUP_ORDER } from './source-control-group-order'
import { DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT } from './terminal-scrollback-policy'
export { DEFAULT_STATUS_BAR_ITEMS } from './status-bar-defaults'
export {
@@ -259,7 +260,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
terminalAllowOsc52Clipboard: false,
claudeAgentTeamsMode: 'off',
setupScriptLaunchMode: 'new-tab',
terminalScrollbackBytes: 10_000_000,
terminalScrollbackRows: DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT,
httpProxyUrl: '',
httpProxyBypassRules: '',
electronHttp1CompatibilityMode: false,
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest'
import {
DESKTOP_TERMINAL_SCROLLBACK_ROW_PRESETS,
DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT,
DESKTOP_TERMINAL_SCROLLBACK_ROWS_MAX,
DESKTOP_TERMINAL_SCROLLBACK_ROWS_MIN,
legacyTerminalScrollbackBytesToRows,
normalizeDesktopTerminalScrollbackRows,
normalizeDesktopTerminalSnapshotRows
} from './terminal-scrollback-policy'
describe('terminal scrollback policy', () => {
it('exports the desktop row defaults and presets', () => {
expect(DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT).toBe(5_000)
expect(DESKTOP_TERMINAL_SCROLLBACK_ROWS_MIN).toBe(1_000)
expect(DESKTOP_TERMINAL_SCROLLBACK_ROWS_MAX).toBe(50_000)
expect(DESKTOP_TERMINAL_SCROLLBACK_ROW_PRESETS).toEqual([5_000, 10_000, 25_000, 50_000])
})
it('normalizes persisted desktop rows without string coercion', () => {
expect(normalizeDesktopTerminalScrollbackRows(undefined)).toBe(5_000)
expect(normalizeDesktopTerminalScrollbackRows('25000')).toBe(5_000)
expect(normalizeDesktopTerminalScrollbackRows(Number.NaN)).toBe(5_000)
expect(normalizeDesktopTerminalScrollbackRows(500.9)).toBe(1_000)
expect(normalizeDesktopTerminalScrollbackRows(25_000.9)).toBe(25_000)
expect(normalizeDesktopTerminalScrollbackRows(100_000)).toBe(50_000)
})
it('normalizes snapshot rows while preserving visible-screen-only zero', () => {
expect(normalizeDesktopTerminalSnapshotRows(undefined)).toBeUndefined()
expect(normalizeDesktopTerminalSnapshotRows('0')).toBeUndefined()
expect(normalizeDesktopTerminalSnapshotRows(0)).toBe(0)
expect(normalizeDesktopTerminalSnapshotRows(-1)).toBe(0)
expect(normalizeDesktopTerminalSnapshotRows(25_000.9)).toBe(25_000)
expect(normalizeDesktopTerminalSnapshotRows(100_000)).toBe(50_000)
})
it('migrates legacy decimal MB buckets by intent, not byte-to-row math', () => {
expect(legacyTerminalScrollbackBytesToRows(undefined)).toBe(5_000)
expect(legacyTerminalScrollbackBytesToRows(0)).toBe(5_000)
expect(legacyTerminalScrollbackBytesToRows(1_000_000)).toBe(1_000)
expect(legacyTerminalScrollbackBytesToRows(10_000_000)).toBe(5_000)
expect(legacyTerminalScrollbackBytesToRows(25_000_000)).toBe(10_000)
expect(legacyTerminalScrollbackBytesToRows(50_000_000)).toBe(25_000)
expect(legacyTerminalScrollbackBytesToRows(100_000_000)).toBe(50_000)
expect(legacyTerminalScrollbackBytesToRows(250_000_000)).toBe(50_000)
})
})
+55
View File
@@ -0,0 +1,55 @@
export const DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT = 5_000
export const DESKTOP_TERMINAL_SCROLLBACK_ROWS_MIN = 1_000
export const DESKTOP_TERMINAL_SCROLLBACK_ROWS_MAX = 50_000
export const DESKTOP_TERMINAL_SCROLLBACK_ROW_PRESETS = [5_000, 10_000, 25_000, 50_000] as const
export const LEGACY_TERMINAL_SCROLLBACK_BYTES_1_MB = 1_000_000
export const LEGACY_TERMINAL_SCROLLBACK_BYTES_10_MB = 10_000_000
export const LEGACY_TERMINAL_SCROLLBACK_BYTES_25_MB = 25_000_000
export const LEGACY_TERMINAL_SCROLLBACK_BYTES_50_MB = 50_000_000
export const LEGACY_TERMINAL_SCROLLBACK_BYTES_100_MB = 100_000_000
export const LEGACY_TERMINAL_SCROLLBACK_BUCKET_5K_MAX_BYTES = 17_500_000
export const LEGACY_TERMINAL_SCROLLBACK_BUCKET_10K_MAX_BYTES = 37_500_000
export const LEGACY_TERMINAL_SCROLLBACK_BUCKET_25K_MAX_BYTES = 75_000_000
function isFiniteNumber(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value)
}
function clampRows(value: number, min: number): number {
return Math.min(DESKTOP_TERMINAL_SCROLLBACK_ROWS_MAX, Math.max(min, Math.floor(value)))
}
export function normalizeDesktopTerminalScrollbackRows(value: unknown): number {
if (!isFiniteNumber(value)) {
return DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT
}
return clampRows(value, DESKTOP_TERMINAL_SCROLLBACK_ROWS_MIN)
}
export function normalizeDesktopTerminalSnapshotRows(value: unknown): number | undefined {
if (!isFiniteNumber(value)) {
return undefined
}
return clampRows(value, 0)
}
export function legacyTerminalScrollbackBytesToRows(bytes: unknown): number {
if (!isFiniteNumber(bytes) || bytes <= 0) {
return DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT
}
if (bytes <= LEGACY_TERMINAL_SCROLLBACK_BYTES_1_MB) {
return DESKTOP_TERMINAL_SCROLLBACK_ROWS_MIN
}
if (bytes < LEGACY_TERMINAL_SCROLLBACK_BUCKET_5K_MAX_BYTES) {
return DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT
}
if (bytes < LEGACY_TERMINAL_SCROLLBACK_BUCKET_10K_MAX_BYTES) {
return 10_000
}
if (bytes < LEGACY_TERMINAL_SCROLLBACK_BUCKET_25K_MAX_BYTES) {
return 25_000
}
return DESKTOP_TERMINAL_SCROLLBACK_ROWS_MAX
}
+1 -1
View File
@@ -2527,7 +2527,7 @@ export type GlobalSettings = {
* background "Setup" tab so the user's main terminal stays immediately
* usable without the setup output crowding the initial pane. */
setupScriptLaunchMode: SetupScriptLaunchMode
terminalScrollbackBytes: number
terminalScrollbackRows: number
/** Optional app-level proxy for Electron networking and locally spawned PTYs.
* Empty preserves system proxy settings plus inherited proxy env behavior. */
httpProxyUrl?: string