perf(terminal): keep unviewed restart survivors to the daemon's live window

Main now attaches every surviving terminal at startup; each mirror took the full
5,000-row depth with no viewer. A survivor nobody views keeps the 1,000 rows the
daemon itself holds, and grows to full depth from those rows once a view subscribes.
This commit is contained in:
Jinwoo-H
2026-10-29 16:39:23 -04:00
parent 9a6503aa80
commit 4ea934ca7f
7 changed files with 77 additions and 8 deletions
+7 -6
View File
@@ -23,7 +23,7 @@ import {
type TerminalViewAttributeResponder
} from './terminal-view-attribute-responder'
import { installDeviceAttributesResponder } from './startup-device-attributes-responder'
import type { TerminalSnapshot, TerminalModes } from './types'
import type { TerminalSnapshot } from './types'
import type { TerminalOscLinkRange } from '../../shared/terminal-osc-link-ranges'
import type { TerminalCursorContext } from '../../shared/terminal-composer-draft'
import { readTerminalCursorLineContext } from '../../shared/terminal-cursor-line-context'
@@ -241,7 +241,7 @@ export class HeadlessEmulator {
}
getSnapshot(opts: { scrollbackRows?: number } = {}): TerminalSnapshot {
const modes = this.getModes()
const modes = readTerminalModes(this.terminal)
// Absolute cursor restore avoids wrap-pending drift; carry the saved register for DECRC.
const serializedAnsi = serializeWithAbsoluteCursor(
this.serializer,
@@ -345,6 +345,11 @@ export class HeadlessEmulator {
this.restoredOscLinks = links?.slice() ?? []
}
/** Growing keeps every retained row; shrinking drops the oldest. */
setScrollback(rows: number): void {
this.terminal.options.scrollback = rows
}
clearScrollback(): void {
this.restoredOscLinks = []
this.terminal.clear()
@@ -354,8 +359,4 @@ export class HeadlessEmulator {
this.disposed = true
this.terminal.dispose()
}
private getModes(): TerminalModes {
return readTerminalModes(this.terminal)
}
}
@@ -13,6 +13,7 @@ import {
encodeTerminalStreamFrame,
encodeTerminalStreamJson
} from '../../shared/terminal-stream-protocol'
import { DAEMON_SESSION_SCROLLBACK_ROWS } from '../daemon/daemon-session-scrollback-window'
const WORKTREE_ID = 'repo-1::/tmp/wt'
const PTY_ID = `${WORKTREE_ID}@@1a2b3c4d`
@@ -24,7 +25,7 @@ type RuntimeInternals = {
state?: { connected?: boolean; connectionId?: string | null }
) => unknown
issuePtyHandle: (pty: unknown) => string
headlessTerminals: Map<string, unknown>
headlessTerminals: Map<string, { writeChain: Promise<unknown> }>
refreshPtyWorktreeRecordsWithControllerInventory: (
worktrees: [],
targetWorktreeId: string | null
@@ -263,6 +264,19 @@ function snapshotText(harness: ReturnType<typeof startMultiplex>): string {
.join('')
}
function numberedLines(from: number, to: number): string {
return Array.from(
{ length: to - from + 1 },
(_, i) => `L${String(from + i).padStart(4, '0')}`
).join('\r\n')
}
async function retainedLines(runtime: OrcaRuntimeService, ptyId: string): Promise<string[]> {
await internals(runtime).headlessTerminals.get(ptyId)?.writeChain
const snapshot = await runtime.serializeMainTerminalBuffer(ptyId, { scrollbackRows: 10_000 })
return `${snapshot?.scrollbackAnsi ?? ''}${snapshot?.data ?? ''}`.match(/L\d{4}/g) ?? []
}
describe('subscriber-driven daemon attach (never-activated tab)', () => {
it('attaches on first desktop multiplex subscribe and streams bytes (snapshot-null daemon)', async () => {
const { runtime, model, handle, mountSpy } = setupNeverAttachedDaemonSession({
@@ -431,6 +445,43 @@ describe('subscriber-driven daemon attach (never-activated tab)', () => {
expect(read.tail.join('\n')).toContain('worker still printing')
})
it('keeps unviewed survivors to the daemon window and grows a viewed one from it', async () => {
const runtime = new OrcaRuntimeService()
const model = createDaemonProviderModel({ snapshotCapable: false })
model.bind(runtime)
runtime.setPtyController(model.controller as never)
const ptyIds = Array.from({ length: 10 }, (_, i) => `${WORKTREE_ID}@@0000000${i}`)
const records = ptyIds.map((ptyId) => {
model.sessions.set(ptyId, { cols: 137, rows: 41, attached: false, screen: '' })
return internals(runtime).recordPtyWorktree(ptyId, WORKTREE_ID, { connected: true })
})
await internals(runtime).refreshPtyWorktreeRecordsWithControllerInventory([], null)
await vi.waitFor(() => expect(model.attachCalls).toHaveLength(10))
for (const ptyId of ptyIds) {
expect(model.emitData(ptyId, numberedLines(1, 3000))).toBe(true)
}
const retained = await Promise.all(ptyIds.map((ptyId) => retainedLines(runtime, ptyId)))
const windowRows = DAEMON_SESSION_SCROLLBACK_ROWS + 41
expect(retained.map((lines) => lines.length)).toEqual(ptyIds.map(() => windowRows))
expect(retained[0].at(-1)).toBe('L3000')
// The first view paints exactly the rows the mirror kept, then the mirror grows past the window.
const harness = startMultiplex(runtime)
await vi.waitFor(() => expect(harness.handlers.has(0)).toBe(true))
sendSubscribe(harness, 1, internals(runtime).issuePtyHandle(records[0]))
await waitForSubscribed(harness, 1)
expect(snapshotText(harness).match(/L\d{4}/g)).toEqual(retained[0])
model.emitData(ptyIds[0], `\r\n${numberedLines(3001, 6000)}`)
model.emitData(ptyIds[1], `\r\n${numberedLines(3001, 6000)}`)
await vi.waitFor(() => expect(outputText(harness)).toContain('L6000'))
expect(await retainedLines(runtime, ptyIds[0])).toEqual([
...retained[0],
...numberedLines(3001, 6000).split('\r\n')
])
expect(await retainedLines(runtime, ptyIds[1])).toHaveLength(windowRows)
})
it('does not subscriber-attach or provider-read a session this app already spawned', async () => {
const { runtime, model, handle } = setupNeverAttachedDaemonSession({
snapshotCapable: true,
@@ -41,6 +41,10 @@ export abstract class OrcaRuntimeWithNotifyRemoteTerminalViewPresenceChanged ext
): Promise<WriteSettlement | undefined>
protected notifyRemoteTerminalViewPresenceChanged(ptyId: string): void {
// A viewed survivor's mirror grows to full depth from the rows it kept.
if (this.hasTerminalViewer(ptyId) && this.unviewedSurvivorMirrorPtys.delete(ptyId)) {
this.headlessTerminals.get(ptyId)?.emulator.setScrollback(this.headlessTerminalScrollbackRows)
}
try {
this.onRemoteTerminalViewPresenceChanged?.(ptyId)
} catch (err) {
@@ -190,6 +190,12 @@ export abstract class OrcaRuntimeWithRefreshPtyWorktreeRecordsWithControllerInve
this.restoredOrchestrationAuthorityByPtyId.delete(session.id)
}
pty.controllerTitle = session.title?.trim() || null
if (
!this.hasTerminalViewer(session.id) &&
this.mainDaemonAttachments.isKnownUnattachedLocal(session.id)
) {
this.unviewedSurvivorMirrorPtys.add(session.id)
}
this.mainDaemonAttachments.reconcile(session.id)
}
// Do not serialize relay probes or let one throw abort inventory cleanup.
@@ -162,6 +162,7 @@ export abstract class OrcaRuntimeWithOnPtyExit extends OrcaRuntimeWithOnClientDi
this.mobileSubscribers.delete(ptyId)
this.terminalViewSubscribers.clearSubscribers(ptyId)
this.mainDaemonAttachments.forget(ptyId)
this.unviewedSurvivorMirrorPtys.delete(ptyId)
this.mobileDisplayModes.delete(ptyId)
this.resizeListeners.delete(ptyId)
this.lastRendererSizes.delete(ptyId)
@@ -53,6 +53,9 @@ export abstract class OrcaRuntimeWithFitOverrideListeners extends OrcaRuntimeWit
protected providerSnapshotPreferredPtys = new Set<string>()
// Survivors main attached with no view hold the daemon's live window, not a full-depth copy.
protected unviewedSurvivorMirrorPtys = new Set<string>()
protected providerModeTrackersByPtyId = new Map<string, TerminalKittyKeyboardModeTracker>()
protected providerModeSnapshotScansByPtyId = new Map<
@@ -1,6 +1,7 @@
import { OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer } from './headless-terminal-hydration'
import type { RuntimeHeadlessTerminal } from '../../runtime-terminal-state-records'
import { HeadlessEmulator } from '../../../daemon/headless-emulator'
import { DAEMON_SESSION_SCROLLBACK_ROWS } from '../../../daemon/daemon-session-scrollback-window'
import { shouldForwardHeadlessTerminalQueryReply } from '../../headless-terminal-query-reply-policy'
import { isNativeWindowsConptyPty } from '../../terminal-model-query-authority'
import { getTerminalViewAttributes } from '../../terminal-view-attribute-store'
@@ -41,7 +42,9 @@ export abstract class OrcaRuntimeWithCreatePtyHeadlessTerminalState extends Orca
const emulator = new HeadlessEmulator({
cols: dims.cols,
rows: dims.rows,
scrollback: this.headlessTerminalScrollbackRows,
scrollback: this.unviewedSurvivorMirrorPtys.has(ptyId)
? Math.min(DAEMON_SESSION_SCROLLBACK_ROWS, this.headlessTerminalScrollbackRows)
: this.headlessTerminalScrollbackRows,
pathFlavor,
remotePosixFileUriAuthority:
!!this.ptysById.get(ptyId)?.connectionId && pathFlavor !== 'win32',