Fix terminal read for blank TUI tails (#5050)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo Hong
2026-06-09 15:53:39 -07:00
committed by GitHub
co-authored by Orca
parent 9c199a5b85
commit 7d666fafab
4 changed files with 264 additions and 2 deletions
+91 -2
View File
@@ -3825,6 +3825,59 @@ export class OrcaRuntimeService {
return rendererSnapshot ? { ...rendererSnapshot, source: 'renderer' } : null
}
private async withVisibleSnapshotFallback(
ptyId: string,
read: RuntimeTerminalRead,
opts: { cursor?: number; limit?: number } = {}
): Promise<RuntimeTerminalRead> {
if (!shouldFallbackToVisibleTerminalSnapshot(read, opts)) {
return read
}
const lines = await this.readRendererVisibleSnapshotLines(ptyId)
if (lines.length === 0) {
return read
}
return buildVisibleSnapshotReadFallback(read, lines, opts.limit)
}
private async readRendererVisibleSnapshotLines(ptyId: string): Promise<string[]> {
const controller = this.ptyController
if (!controller?.serializeBuffer) {
return []
}
if (controller.hasRendererSerializer && !controller.hasRendererSerializer(ptyId)) {
return []
}
try {
// Why: raw PTY tails can be whitespace-only while a full-screen TUI is
// visibly nonblank in renderer xterm. Ask the renderer for the active
// screen instead of reusing the headless transcript path.
const snapshot = await controller.serializeBuffer(ptyId, {
scrollbackRows: 0,
altScreenForcesZeroRows: false
})
if (!snapshot || snapshot.data.length === 0) {
return []
}
const emulator = new HeadlessEmulator({
cols: snapshot.cols,
rows: snapshot.rows,
scrollback: 0
})
try {
await emulator.write(snapshot.data)
return emulator
.getVisibleLines()
.map((line) => line.trimEnd())
.filter((line) => line.trim().length > 0)
} finally {
emulator.dispose()
}
} catch {
return []
}
}
private async serializeHeadlessTerminalBuffer(
ptyId: string,
opts: { scrollbackRows?: number; includeEmpty?: boolean } = {}
@@ -5930,7 +5983,8 @@ export class OrcaRuntimeService {
): Promise<RuntimeTerminalRead> {
const pty = this.getLivePtyForHandle(handle)
if (pty) {
return this.readPtyTerminal(handle, pty.pty, opts)
const read = this.readPtyTerminal(handle, pty.pty, opts)
return this.withVisibleSnapshotFallback(pty.pty.ptyId, read, opts)
}
const { leaf } = this.getLiveLeafForHandle(handle)
@@ -5944,7 +5998,7 @@ export class OrcaRuntimeService {
cursor: opts.cursor,
limit: opts.limit
})
return read
return leaf.ptyId ? this.withVisibleSnapshotFallback(leaf.ptyId, read, opts) : read
}
async sendTerminal(
@@ -15388,6 +15442,41 @@ function readTerminalTail(args: {
}
}
function shouldFallbackToVisibleTerminalSnapshot(
read: RuntimeTerminalRead,
opts: { cursor?: number; limit?: number }
): boolean {
if (typeof opts.cursor === 'number') {
return false
}
if (read.tail.length === 0) {
return false
}
const hasSubstantialBlankTail =
read.limited === true || read.truncated || read.tail.length >= DEFAULT_TERMINAL_READ_LIMIT
return hasSubstantialBlankTail && read.tail.every((line) => line.trim().length === 0)
}
function buildVisibleSnapshotReadFallback(
read: RuntimeTerminalRead,
visibleLines: string[],
limit: number | undefined
): RuntimeTerminalRead {
const lineLimit = terminalReadLimit(limit, DEFAULT_TERMINAL_READ_LIMIT)
const lineBoundedTail = visibleLines.slice(-lineLimit)
const charBoundedTail = trimTerminalPreviewToCharacterBudget(
lineBoundedTail,
MAX_TERMINAL_PREVIEW_CHARS
)
return {
...read,
tail: charBoundedTail.tail,
limited:
read.limited || lineBoundedTail.length < visibleLines.length || charBoundedTail.limited,
returnedLineCount: charBoundedTail.tail.length
}
}
function getTerminalState(leaf: RuntimeLeafRecord): RuntimeTerminalState {
if (leaf.connected) {
return 'running'