mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 16:03:26 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [] })
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "ライト モードでペイン間の分割分割線を制御します。",
|
||||
|
||||
@@ -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": "라이트 모드에서 창 사이의 분할 구분선을 제어합니다.",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user