mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 09:27:29 +00:00
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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user