mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 16:02:42 +00:00
fix(terminal): remove scrollback demotion (C1 slice C)
The scrollback demotion feature for eviction-exempt hidden worktrees is no longer needed. Retention budget limits are now sufficient without this additional bound. Remove the terminal-hidden-scrollback-demotion module, the selectScrollbackDemotedTerminalWorktrees function, and related per-pane demotion logic.
This commit is contained in:
@@ -94,13 +94,8 @@ import {
|
||||
import {
|
||||
TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS,
|
||||
selectRetentionForceParkedTerminalWorktrees,
|
||||
selectScrollbackDemotedTerminalWorktrees,
|
||||
type TerminalWorktreeRetentionCandidate
|
||||
} from './terminal-pane/terminal-hidden-worktree-retention'
|
||||
import {
|
||||
resetTerminalScrollbackDemotion,
|
||||
setScrollbackDemotedTerminalWorktrees
|
||||
} from './terminal-pane/terminal-hidden-scrollback-demotion'
|
||||
import { captureTerminalShutdownBuffersBestEffort } from './terminal-pane/shutdown-buffer-captures'
|
||||
import { getTerminalParkingPolicyOverrides } from './terminal-pane/terminal-parking-e2e-overrides'
|
||||
import { useColdParkedTerminalPresentation } from './terminal-pane/use-cold-parked-terminal-presentation'
|
||||
@@ -302,9 +297,6 @@ function Terminal(): React.JSX.Element | null {
|
||||
const terminalRetentionBudgetEnabled = useAppStore(
|
||||
(s) => s.settings?.terminalHiddenWorktreeRetentionBudget !== false
|
||||
)
|
||||
const terminalScrollbackDemotionEnabled = useAppStore(
|
||||
(s) => s.settings?.terminalHiddenScrollbackDemotion !== false
|
||||
)
|
||||
const terminalTitleSnapshotAuthorityEnabled = useAppStore((s) =>
|
||||
isMainTerminalSideEffectAuthorityForPty({
|
||||
settings: s.settings,
|
||||
@@ -1072,18 +1064,6 @@ function Terminal(): React.JSX.Element | null {
|
||||
setForceParkedTerminalWorktreeIds((current) =>
|
||||
haveSameWorktreeIds(current, forceParkedWorktreeIds) ? current : forceParkedWorktreeIds
|
||||
)
|
||||
// C1 slice C: eviction-exempt worktrees stay mounted, so past the TTL their
|
||||
// hidden panes demote to the minimum scrollback tier instead of unmounting.
|
||||
setScrollbackDemotedTerminalWorktrees(
|
||||
selectScrollbackDemotedTerminalWorktrees({
|
||||
worktrees: retentionBudgetCandidates,
|
||||
parkingEnabled: terminalParkingEnabled,
|
||||
retentionBudgetEnabled: terminalRetentionBudgetEnabled,
|
||||
demotionEnabled: terminalScrollbackDemotionEnabled,
|
||||
nowMs,
|
||||
...overrides
|
||||
})
|
||||
)
|
||||
const retentionTtlEligibleIds = new Set(
|
||||
retentionBudgetCandidates
|
||||
.filter((candidate) => !candidate.ordinaryParkingCovers && !candidate.hasPendingSpawnWork)
|
||||
@@ -1105,10 +1085,8 @@ function Terminal(): React.JSX.Element | null {
|
||||
parkCooldownUntilMs: candidate.parkCooldownUntilMs,
|
||||
nowMs,
|
||||
...overrides,
|
||||
// Why: only retention candidates wake at the eviction TTL; everyone else keeps the ordinary
|
||||
// deadlines. Demotion (slice C) needs the wakeup too — it no longer requires slice B's switch.
|
||||
...((terminalRetentionBudgetEnabled || terminalScrollbackDemotionEnabled) &&
|
||||
retentionTtlEligibleIds.has(candidate.worktreeId)
|
||||
// Why: only retention candidates wake at the eviction TTL; everyone else keeps the ordinary deadlines.
|
||||
...(terminalRetentionBudgetEnabled && retentionTtlEligibleIds.has(candidate.worktreeId)
|
||||
? {
|
||||
retentionTtlMs: overrides.retentionTtlMs ?? TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS
|
||||
}
|
||||
@@ -1133,7 +1111,6 @@ function Terminal(): React.JSX.Element | null {
|
||||
terminalParkingEnabled,
|
||||
terminalParkingRevision,
|
||||
terminalRetentionBudgetEnabled,
|
||||
terminalScrollbackDemotionEnabled,
|
||||
terminalSshParkingEnabled,
|
||||
workspaceSurfaces
|
||||
])
|
||||
@@ -1373,14 +1350,8 @@ function Terminal(): React.JSX.Element | null {
|
||||
workspaceSessionReady,
|
||||
workspaceSurfaces
|
||||
])
|
||||
// Why: on host unmount no reconciliation effect runs again, so dispose every remaining parked watcher and drop the demotion verdicts nothing would recompute.
|
||||
useEffect(
|
||||
() => () => {
|
||||
disposeAllParkedTerminalWatchers()
|
||||
resetTerminalScrollbackDemotion()
|
||||
},
|
||||
[]
|
||||
)
|
||||
// Why: on host unmount no reconciliation effect runs again, so dispose every remaining parked watcher.
|
||||
useEffect(() => () => disposeAllParkedTerminalWatchers(), [])
|
||||
// Auto-create first tab when worktree activates
|
||||
useEffect(() => {
|
||||
if (!workspaceSessionReady) {
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
TERMINAL_DEMOTED_SCROLLBACK_ROWS,
|
||||
demotedTerminalScrollbackRows,
|
||||
isTerminalWorktreeScrollbackDemoted,
|
||||
resetTerminalScrollbackDemotion,
|
||||
resolveTerminalMountScrollbackRows,
|
||||
setScrollbackDemotedTerminalWorktrees,
|
||||
subscribeTerminalScrollbackDemotion
|
||||
} from './terminal-hidden-scrollback-demotion'
|
||||
|
||||
afterEach(() => {
|
||||
setScrollbackDemotedTerminalWorktrees(new Set())
|
||||
})
|
||||
|
||||
describe('setScrollbackDemotedTerminalWorktrees', () => {
|
||||
it('replaces the demoted set', () => {
|
||||
setScrollbackDemotedTerminalWorktrees(new Set(['wt-1', 'wt-2']))
|
||||
expect(isTerminalWorktreeScrollbackDemoted('wt-1')).toBe(true)
|
||||
setScrollbackDemotedTerminalWorktrees(new Set(['wt-2']))
|
||||
expect(isTerminalWorktreeScrollbackDemoted('wt-1')).toBe(false)
|
||||
expect(isTerminalWorktreeScrollbackDemoted('wt-2')).toBe(true)
|
||||
})
|
||||
|
||||
it('does not notify when the set content is unchanged (flip-loop damping)', () => {
|
||||
setScrollbackDemotedTerminalWorktrees(new Set(['wt-1']))
|
||||
const listener = vi.fn()
|
||||
const unsubscribe = subscribeTerminalScrollbackDemotion(listener)
|
||||
setScrollbackDemotedTerminalWorktrees(new Set(['wt-1']))
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
setScrollbackDemotedTerminalWorktrees(new Set(['wt-1', 'wt-2']))
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
unsubscribe()
|
||||
})
|
||||
})
|
||||
|
||||
describe('resetTerminalScrollbackDemotion', () => {
|
||||
// Why: module state outlives the host, so a stale verdict would demote a pane
|
||||
// that remounts before any effect recomputes it.
|
||||
it('clears every verdict and notifies subscribers', () => {
|
||||
setScrollbackDemotedTerminalWorktrees(new Set(['wt-1', 'wt-2']))
|
||||
const listener = vi.fn()
|
||||
const unsubscribe = subscribeTerminalScrollbackDemotion(listener)
|
||||
resetTerminalScrollbackDemotion()
|
||||
expect(isTerminalWorktreeScrollbackDemoted('wt-1')).toBe(false)
|
||||
expect(isTerminalWorktreeScrollbackDemoted('wt-2')).toBe(false)
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
unsubscribe()
|
||||
})
|
||||
})
|
||||
|
||||
describe('demotedTerminalScrollbackRows', () => {
|
||||
it('never exceeds the configured rows', () => {
|
||||
expect(demotedTerminalScrollbackRows(50_000)).toBe(TERMINAL_DEMOTED_SCROLLBACK_ROWS)
|
||||
expect(demotedTerminalScrollbackRows(500)).toBe(500)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveTerminalMountScrollbackRows', () => {
|
||||
// Why: pane births (splits) under an already-demoted worktree bypass the
|
||||
// post-mount demotion effect entirely — the mount options must demote.
|
||||
it('demotes new xterms while the worktree verdict is set, restores after clear', () => {
|
||||
setScrollbackDemotedTerminalWorktrees(new Set(['wt-demoted']))
|
||||
expect(resolveTerminalMountScrollbackRows('wt-demoted', 50_000)).toBe(
|
||||
TERMINAL_DEMOTED_SCROLLBACK_ROWS
|
||||
)
|
||||
expect(resolveTerminalMountScrollbackRows('wt-other', 50_000)).toBe(50_000)
|
||||
resetTerminalScrollbackDemotion()
|
||||
expect(resolveTerminalMountScrollbackRows('wt-demoted', 50_000)).toBe(50_000)
|
||||
})
|
||||
})
|
||||
@@ -1,72 +0,0 @@
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import { DESKTOP_TERMINAL_SCROLLBACK_ROWS_MIN } from '../../../../shared/terminal-scrollback-policy'
|
||||
|
||||
/** Demoted hidden panes keep the minimum retention tier (~1.3MB V8 heap vs
|
||||
* ~19MB at 50k rows, measured); trimmed history is gone by design — reveal
|
||||
* restores the configured cap for future output only. */
|
||||
export const TERMINAL_DEMOTED_SCROLLBACK_ROWS = DESKTOP_TERMINAL_SCROLLBACK_ROWS_MIN
|
||||
|
||||
// Why module state (same pattern as parked watchers): the verdict is computed
|
||||
// in Terminal.tsx but applied by each mounted pane's lifecycle hook — prop
|
||||
// threading would cross three component layers for a per-worktree boolean.
|
||||
const demotedWorktreeIds = new Set<string>()
|
||||
const listeners = new Set<() => void>()
|
||||
|
||||
export function setScrollbackDemotedTerminalWorktrees(next: ReadonlySet<string>): void {
|
||||
if (
|
||||
next.size === demotedWorktreeIds.size &&
|
||||
[...next].every((id) => demotedWorktreeIds.has(id))
|
||||
) {
|
||||
return
|
||||
}
|
||||
demotedWorktreeIds.clear()
|
||||
for (const id of next) {
|
||||
demotedWorktreeIds.add(id)
|
||||
}
|
||||
for (const listener of listeners) {
|
||||
listener()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear every verdict — the demotion host is gone, so nothing recomputes them.
|
||||
* Without this a pane remounting later reads a stale demotion (React runs the
|
||||
* pane's scrollback effect BEFORE the host effect that would clear it) and
|
||||
* xterm trims its restore replay to 1 000 rows, unrecoverably.
|
||||
*/
|
||||
export function resetTerminalScrollbackDemotion(): void {
|
||||
setScrollbackDemotedTerminalWorktrees(new Set())
|
||||
}
|
||||
|
||||
export function isTerminalWorktreeScrollbackDemoted(worktreeId: string): boolean {
|
||||
return demotedWorktreeIds.has(worktreeId)
|
||||
}
|
||||
|
||||
export function subscribeTerminalScrollbackDemotion(listener: () => void): () => void {
|
||||
listeners.add(listener)
|
||||
return () => listeners.delete(listener)
|
||||
}
|
||||
|
||||
/** Live per-worktree demotion verdict for a mounted pane host. */
|
||||
export function useTerminalWorktreeScrollbackDemoted(worktreeId: string): boolean {
|
||||
return useSyncExternalStore(subscribeTerminalScrollbackDemotion, () =>
|
||||
demotedWorktreeIds.has(worktreeId)
|
||||
)
|
||||
}
|
||||
|
||||
export function demotedTerminalScrollbackRows(configuredRows: number): number {
|
||||
return Math.min(TERMINAL_DEMOTED_SCROLLBACK_ROWS, configuredRows)
|
||||
}
|
||||
|
||||
/** Scrollback for a NEW xterm in this worktree. Pane births under an
|
||||
* already-demoted worktree (splits, layout replays) must take the demoted
|
||||
* tier at create — the post-mount demotion effect only re-runs when the
|
||||
* verdict or setting flips, so it never covers them. */
|
||||
export function resolveTerminalMountScrollbackRows(
|
||||
worktreeId: string,
|
||||
configuredRows: number
|
||||
): number {
|
||||
return demotedWorktreeIds.has(worktreeId)
|
||||
? demotedTerminalScrollbackRows(configuredRows)
|
||||
: configuredRows
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS,
|
||||
isEvictionExemptTerminalPty,
|
||||
selectRetentionForceParkedTerminalWorktrees,
|
||||
selectScrollbackDemotedTerminalWorktrees,
|
||||
type TerminalWorktreeRetentionCandidate
|
||||
} from './terminal-hidden-worktree-retention'
|
||||
|
||||
@@ -175,157 +174,3 @@ describe('selectRetentionForceParkedTerminalWorktrees', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('selectScrollbackDemotedTerminalWorktrees', () => {
|
||||
const nowMs = 5_000_000
|
||||
const aged = nowMs - TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS
|
||||
const base = {
|
||||
parkingEnabled: true,
|
||||
retentionBudgetEnabled: true,
|
||||
demotionEnabled: true,
|
||||
nowMs
|
||||
}
|
||||
|
||||
function exemptCandidate(
|
||||
worktreeId: string,
|
||||
hiddenSinceMs: number | null,
|
||||
partial: Partial<TerminalWorktreeRetentionCandidate> = {}
|
||||
): TerminalWorktreeRetentionCandidate {
|
||||
return {
|
||||
worktreeId,
|
||||
hiddenSinceMs,
|
||||
isVisible: false,
|
||||
shouldMeasureHiddenWorktree: false,
|
||||
hasActivityTerminalPortal: false,
|
||||
ordinaryParkingCovers: false,
|
||||
hasEvictionExemptTab: true,
|
||||
hasPendingSpawnWork: false,
|
||||
...partial
|
||||
}
|
||||
}
|
||||
|
||||
it('demotes only eviction-exempt worktrees hidden past the TTL', () => {
|
||||
const selected = selectScrollbackDemotedTerminalWorktrees({
|
||||
...base,
|
||||
worktrees: [
|
||||
exemptCandidate('wt-aged', aged),
|
||||
exemptCandidate('wt-recent', nowMs - 60_000),
|
||||
exemptCandidate('wt-not-exempt', aged, { hasEvictionExemptTab: false }),
|
||||
exemptCandidate('wt-visible', aged, { isVisible: true }),
|
||||
exemptCandidate('wt-portal', aged, { hasActivityTerminalPortal: true }),
|
||||
exemptCandidate('wt-pending', aged, { hasPendingSpawnWork: true }),
|
||||
exemptCandidate('wt-unhidden', null)
|
||||
]
|
||||
})
|
||||
expect(selected).toEqual(new Set(['wt-aged']))
|
||||
})
|
||||
|
||||
it('returns empty when the master or demotion switch is off', () => {
|
||||
const worktrees = [exemptCandidate('wt-aged', aged)]
|
||||
expect(
|
||||
selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees, parkingEnabled: false })
|
||||
).toEqual(new Set())
|
||||
expect(
|
||||
selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees, demotionEnabled: false })
|
||||
).toEqual(new Set())
|
||||
})
|
||||
|
||||
it('demotes independently of the slice-B retention-budget switch', () => {
|
||||
const worktrees = [exemptCandidate('wt-aged', aged)]
|
||||
expect(
|
||||
selectScrollbackDemotedTerminalWorktrees({
|
||||
...base,
|
||||
worktrees,
|
||||
retentionBudgetEnabled: false
|
||||
})
|
||||
).toEqual(new Set(['wt-aged']))
|
||||
})
|
||||
|
||||
it('demotes an exempt worktree as soon as it force-parks under the count budget, before the TTL', () => {
|
||||
// Why: after a force-park the exempt tabs' panes are the only ones left
|
||||
// mounted in that worktree, so they take the demoted scrollback tier.
|
||||
const underTtl = nowMs - TERMINAL_WORKTREE_PARK_DELAY_MS
|
||||
const worktrees = [
|
||||
exemptCandidate('wt-a', underTtl - 3),
|
||||
exemptCandidate('wt-b', underTtl - 2),
|
||||
exemptCandidate('wt-c', underTtl - 1),
|
||||
exemptCandidate('wt-d', underTtl)
|
||||
]
|
||||
expect(
|
||||
selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees, retentionLimit: 2 })
|
||||
).toEqual(new Set(['wt-a', 'wt-b']))
|
||||
})
|
||||
|
||||
it('demotes un-parkable non-exempt worktrees the force-park lever spared, past the TTL', () => {
|
||||
// wt-solo is the only candidate → last-active exempt from force-park; its
|
||||
// panes stay fully mounted forever unless demotion bounds them.
|
||||
const solo = [exemptCandidate('wt-solo', aged, { hasEvictionExemptTab: false })]
|
||||
expect(selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees: solo })).toEqual(
|
||||
new Set(['wt-solo'])
|
||||
)
|
||||
// With slice B off nothing force-parks; every un-parkable worktree past
|
||||
// the TTL demotes (C bounds independently).
|
||||
const pair = [
|
||||
exemptCandidate('wt-a', aged - 1, { hasEvictionExemptTab: false }),
|
||||
exemptCandidate('wt-b', aged, { hasEvictionExemptTab: false })
|
||||
]
|
||||
expect(
|
||||
selectScrollbackDemotedTerminalWorktrees({
|
||||
...base,
|
||||
worktrees: pair,
|
||||
retentionBudgetEnabled: false
|
||||
})
|
||||
).toEqual(new Set(['wt-a', 'wt-b']))
|
||||
// With slice B on, wt-a force-parks (unmounted — no demotion needed) and
|
||||
// only the spared last-active wt-b demotes.
|
||||
expect(selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees: pair })).toEqual(
|
||||
new Set(['wt-b'])
|
||||
)
|
||||
// Ordinary-parking-covered worktrees are bounded by the warm cap already.
|
||||
const covered = [
|
||||
exemptCandidate('wt-covered', aged, {
|
||||
hasEvictionExemptTab: false,
|
||||
ordinaryParkingCovers: true
|
||||
})
|
||||
]
|
||||
expect(selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees: covered })).toEqual(
|
||||
new Set()
|
||||
)
|
||||
})
|
||||
|
||||
it('is idempotent and only grows as time advances (flip-loop dwell)', () => {
|
||||
const worktrees = [
|
||||
exemptCandidate('wt-exempt', aged),
|
||||
exemptCandidate('wt-force-parked', aged, { hasEvictionExemptTab: false }),
|
||||
exemptCandidate('wt-last-active', nowMs - 60_000, { hasEvictionExemptTab: false })
|
||||
]
|
||||
const first = selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees })
|
||||
expect(selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees })).toEqual(first)
|
||||
// Why: with unchanged inputs, a later evaluation may only ADD members —
|
||||
// a verdict that oscillates with time is the React-#185 ingredient.
|
||||
for (const laterMs of [nowMs + 1_000, nowMs + TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS]) {
|
||||
const later = selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees, nowMs: laterMs })
|
||||
for (const id of first) {
|
||||
expect(later.has(id)).toBe(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('honors the retentionTtlMs override and stays time-monotone', () => {
|
||||
const worktrees = [exemptCandidate('wt-1', nowMs - 500)]
|
||||
expect(
|
||||
selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees, retentionTtlMs: 400 })
|
||||
).toEqual(new Set(['wt-1']))
|
||||
expect(
|
||||
selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees, retentionTtlMs: 600 })
|
||||
).toEqual(new Set())
|
||||
expect(
|
||||
selectScrollbackDemotedTerminalWorktrees({
|
||||
...base,
|
||||
worktrees,
|
||||
retentionTtlMs: 600,
|
||||
nowMs: nowMs + 200
|
||||
})
|
||||
).toEqual(new Set(['wt-1']))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,10 +20,10 @@ export const TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS = 45 * 60_000
|
||||
// Why: an eviction-exempt pty is a live local one a remount could not reattach
|
||||
// (daemon-fail-open separator-less ids, ptys minted under another worktree) — a
|
||||
// fresh spawn would orphan the live shell. Its TAB keeps its mounted pane when
|
||||
// the worktree force-parks (per-tab exclusion, mirroring Activity portals) and
|
||||
// relies on scrollback demotion instead. Per-PTY, not per-tab: the coverage
|
||||
// veto that makes a worktree a retention candidate walks every split pane, so
|
||||
// the exemption must too (see isEvictionExemptTerminalTab).
|
||||
// the worktree force-parks (per-tab exclusion, mirroring Activity portals).
|
||||
// Per-PTY, not per-tab: the coverage veto that makes a worktree a retention
|
||||
// candidate walks every split pane, so the exemption must too (see
|
||||
// isEvictionExemptTerminalTab).
|
||||
export function isEvictionExemptTerminalPty(
|
||||
ptyId: string | null | undefined,
|
||||
worktreeId: string
|
||||
@@ -98,66 +98,3 @@ export function selectRetentionForceParkedTerminalWorktrees(
|
||||
hotRetainLimit: args.retentionLimit ?? TERMINAL_HIDDEN_WORKTREE_RETENTION_LIMIT
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrollback demotion targets the hidden panes that stay mounted past the
|
||||
* retention deadlines: (a) eviction-exempt tabs' worktrees (their panes
|
||||
* survive force-park via the per-tab exclusion) demote past the TTL — or
|
||||
* immediately when their worktree force-parks under the count budget, since
|
||||
* the exempt panes are then the only ones left mounted; (b) un-parkable
|
||||
* worktrees the force-park lever spared (the last-active exemption, or slice
|
||||
* B switched off) demote past the TTL instead of holding full scrollback
|
||||
* forever. Time-monotone for fixed inputs: the force-parked and past-TTL sets
|
||||
* only ever grow with nowMs and the last-active pick is time-independent, so
|
||||
* membership only grows until a reveal resets hiddenSince — no oscillation
|
||||
* surface.
|
||||
*
|
||||
* Demotion is WORKTREE-scoped by design: one eviction-exempt leaf demotes
|
||||
* every still-mounted pane in the worktree, including parkable siblings a
|
||||
* mixed split tab keeps mounted (exemption is tab-granular). Coarser than
|
||||
* per-pane, deliberately — do not "fix" to per-pane demotion without
|
||||
* revisiting the retention-budget contract in the C1 PR body.
|
||||
*/
|
||||
export function selectScrollbackDemotedTerminalWorktrees(
|
||||
args: {
|
||||
worktrees: readonly TerminalWorktreeRetentionCandidate[]
|
||||
parkingEnabled: boolean
|
||||
retentionBudgetEnabled: boolean
|
||||
demotionEnabled: boolean
|
||||
nowMs: number
|
||||
} & TerminalColdParkPolicyOverrides
|
||||
): Set<string> {
|
||||
// Why parkingEnabled still gates: it is the master kill switch — parking
|
||||
// globally off means nothing parks OR demotes. Slice B's budget switch does
|
||||
// not gate slice C; each slice reverts independently (approved contract).
|
||||
if (!args.parkingEnabled || !args.demotionEnabled) {
|
||||
return new Set()
|
||||
}
|
||||
const ttlMs = args.retentionTtlMs ?? TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS
|
||||
const forceParked = selectRetentionForceParkedTerminalWorktrees(args)
|
||||
const demoted = new Set<string>()
|
||||
for (const worktree of args.worktrees) {
|
||||
if (
|
||||
worktree.hiddenSinceMs === null ||
|
||||
worktree.isVisible ||
|
||||
worktree.shouldMeasureHiddenWorktree ||
|
||||
worktree.hasActivityTerminalPortal ||
|
||||
worktree.hasPendingSpawnWork
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const pastTtl = args.nowMs - worktree.hiddenSinceMs >= ttlMs
|
||||
if (worktree.hasEvictionExemptTab) {
|
||||
if (pastTtl || forceParked.has(worktree.worktreeId)) {
|
||||
demoted.add(worktree.worktreeId)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Force-park candidates the lever spared stay fully mounted, so demotion
|
||||
// is their only bound; force-parked ones already unmounted.
|
||||
if (!worktree.ordinaryParkingCovers && pastTtl && !forceParked.has(worktree.worktreeId)) {
|
||||
demoted.add(worktree.worktreeId)
|
||||
}
|
||||
}
|
||||
return demoted
|
||||
}
|
||||
|
||||
@@ -15,11 +15,6 @@ import {
|
||||
resolveTerminalCursorInactiveStyle
|
||||
} from '@/lib/pane-manager/pane-terminal-options'
|
||||
import { normalizeDesktopTerminalScrollbackRows } from '../../../../shared/terminal-scrollback-policy'
|
||||
import {
|
||||
demotedTerminalScrollbackRows,
|
||||
resolveTerminalMountScrollbackRows,
|
||||
useTerminalWorktreeScrollbackDemoted
|
||||
} from './terminal-hidden-scrollback-demotion'
|
||||
import {
|
||||
configureTerminalOutputBacklogCap,
|
||||
writeTerminalOutput
|
||||
@@ -574,8 +569,6 @@ export function useTerminalPaneLifecycle({
|
||||
const terminalScrollbackRows = normalizeDesktopTerminalScrollbackRows(
|
||||
settings?.terminalScrollbackRows
|
||||
)
|
||||
// C1 slice C: hidden eviction-exempt worktrees drop to the minimum scrollback tier past the retention TTL.
|
||||
const scrollbackDemoted = useTerminalWorktreeScrollbackDemoted(worktreeId)
|
||||
// Why here: backlog cap scales with the scrollback setting; set it where the setting is read to stay in lockstep.
|
||||
configureTerminalOutputBacklogCap(settings?.terminalScrollbackRows)
|
||||
const systemPrefersDarkRef = useRef(systemPrefersDark)
|
||||
@@ -1334,12 +1327,8 @@ export function useTerminalPaneLifecycle({
|
||||
fontFamily: buildFontFamily(currentSettings?.terminalFontFamily ?? ''),
|
||||
fontWeight: terminalFontWeights.fontWeight,
|
||||
fontWeightBold: terminalFontWeights.fontWeightBold,
|
||||
// Why resolveTerminalMountScrollbackRows: pane births while the
|
||||
// worktree is already demoted (C1 slice C) must not resurrect the
|
||||
// full tier — the post-mount demotion effect doesn't re-run for them.
|
||||
scrollback: resolveTerminalMountScrollbackRows(
|
||||
worktreeId,
|
||||
normalizeDesktopTerminalScrollbackRows(currentSettings?.terminalScrollbackRows)
|
||||
scrollback: normalizeDesktopTerminalScrollbackRows(
|
||||
currentSettings?.terminalScrollbackRows
|
||||
),
|
||||
cursorStyle,
|
||||
cursorInactiveStyle: resolveTerminalCursorInactiveStyle(cursorStyle),
|
||||
@@ -1768,13 +1757,8 @@ export function useTerminalPaneLifecycle({
|
||||
return
|
||||
}
|
||||
// Why: live row-retention changes are xterm option updates only — must not recreate/replay/refit/resize/signal the PTY.
|
||||
applyTerminalScrollbackRowsToMountedPanes(
|
||||
manager,
|
||||
scrollbackDemoted
|
||||
? demotedTerminalScrollbackRows(terminalScrollbackRows)
|
||||
: terminalScrollbackRows
|
||||
)
|
||||
}, [managerRef, scrollbackDemoted, terminalScrollbackRows])
|
||||
applyTerminalScrollbackRowsToMountedPanes(manager, terminalScrollbackRows)
|
||||
}, [managerRef, terminalScrollbackRows])
|
||||
|
||||
useEffect(() => {
|
||||
const manager = managerRef.current
|
||||
|
||||
@@ -304,7 +304,6 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
|
||||
// settings objects (which omit them) keep the default-on behavior.
|
||||
terminalSshViewParking: true,
|
||||
terminalHiddenWorktreeRetentionBudget: true,
|
||||
terminalHiddenScrollbackDemotion: true,
|
||||
terminalMainSideEffectAuthority: true,
|
||||
terminalHiddenDeliveryGate: true,
|
||||
terminalModelQueryAuthority: true,
|
||||
|
||||
@@ -2852,8 +2852,6 @@ export type GlobalSettings = {
|
||||
terminalSshViewParking?: boolean
|
||||
/** Kill switch for the hidden-worktree retention budget (C1): force-parks the least-recently-hidden un-parkable worktrees beyond a count budget or TTL. */
|
||||
terminalHiddenWorktreeRetentionBudget?: boolean
|
||||
/** Kill switch for scrollback demotion (C1): eviction-exempt hidden worktrees drop to the minimum scrollback tier past the retention TTL. */
|
||||
terminalHiddenScrollbackDemotion?: boolean
|
||||
/** Kill switch for main-process PTY side-effect authority; on (default) = title/bell/agent facts via pty:sideEffect channel, not renderer byte parsing. */
|
||||
terminalMainSideEffectAuthority?: boolean
|
||||
/** Kill switch for main's hidden-delivery gate (Phase 4): drops PTY bytes to hidden views after model ingestion; requires terminalMainSideEffectAuthority. */
|
||||
|
||||
Reference in New Issue
Block a user