Keep Codex terminal transcripts readable during redraws (#5858)

This commit is contained in:
Brennan Benson 2026-06-19 19:49:54 -07:00 committed by GitHub
parent 3478efca08
commit d6cb38b45a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 715 additions and 33 deletions

View File

@ -7440,6 +7440,9 @@ describe('OrcaRuntimeService', () => {
const cursorRedraw = appendNormalizedToTailBuffer([], '', 'Working 10%\x1b[3D25%')
expect(cursorRedraw.partialLine).toBe('Working 25%')
const eraseLineKeepsCursor = appendNormalizedToTailBuffer([], '', 'ABC\x1b[2KXY\n')
expect(eraseLineKeepsCursor.lines).toEqual([' XY'])
const eraseWithoutCarriageReturn = appendNormalizedToTailBuffer(
[],
'Downloading 10%',
@ -7462,6 +7465,160 @@ describe('OrcaRuntimeService', () => {
expect(read.latestCursor).toBe('1')
})
it('retains ANSI-only status redraws instead of appending every frame', async () => {
const runtime = new OrcaRuntimeService(store)
syncSinglePty(runtime)
const [terminal] = (await runtime.listTerminals()).terminals
runtime.onPtyData('pty-1', '• Working', 100)
const initialPreview = await runtime.readTerminal(terminal.handle)
expect(initialPreview.tail).toEqual(['• Working'])
runtime.onPtyData('pty-1', '\x1b[2K\x1b[1G• Working.', 101)
const redrawPreview = await runtime.readTerminal(terminal.handle)
expect(redrawPreview.tail).toEqual(['• Working.'])
expect(redrawPreview.tail.join('\n')).not.toContain('• Working• Working')
runtime.onPtyData('pty-1', '\x1b[2K\x1b[1G• Working..', 102)
const latestPreview = await runtime.readTerminal(terminal.handle)
expect(latestPreview.tail).toEqual(['• Working..'])
expect(latestPreview.latestCursor).toBe('0')
const cursorReadBeforeNewline = await runtime.readTerminal(terminal.handle, { cursor: 0 })
expect(cursorReadBeforeNewline.tail).toEqual([])
expect(cursorReadBeforeNewline.nextCursor).toBe('0')
runtime.onPtyData('pty-1', '\n', 103)
const read = await runtime.readTerminal(terminal.handle)
expect(read.tail).toEqual(['• Working..'])
expect(read.latestCursor).toBe('1')
expect(read.tail.join('\n')).not.toContain('• Working• Working')
const cursorReadAfterNewline = await runtime.readTerminal(terminal.handle, { cursor: 0 })
expect(cursorReadAfterNewline.tail).toEqual(['• Working..'])
expect(cursorReadAfterNewline.nextCursor).toBe('1')
})
it('retains multi-line ANSI footer redraws instead of appending old frames', async () => {
const runtime = new OrcaRuntimeService(store)
syncSinglePty(runtime)
const [terminal] = (await runtime.listTerminals()).terminals
runtime.onPtyData('pty-1', '• Working\nTool call\n', 100)
runtime.onPtyData(
'pty-1',
'\x1b[2A\x1b[2K\x1b[1G• Working.\n\x1b[2K\x1b[1GTool call finished\n',
101
)
const read = await runtime.readTerminal(terminal.handle)
expect(read.tail).toEqual(['• Working.', 'Tool call finished'])
expect(read.latestCursor).toBe('4')
expect(read.tail.join('\n')).not.toContain('• Working\nTool call\n• Working.')
expect(read.tail.join('\n')).not.toContain('2A')
expect(read.tail.join('\n')).not.toContain('2K')
})
it('keeps the cursor column when erasing full lines in multi-line redraws', async () => {
const runtime = new OrcaRuntimeService(store)
syncSinglePty(runtime)
const [terminal] = (await runtime.listTerminals()).terminals
runtime.onPtyData('pty-1', 'ABC\nxyz', 100)
runtime.onPtyData('pty-1', '\x1b[1A\x1b[2KXY\n', 101)
const read = await runtime.readTerminal(terminal.handle)
expect(read.tail).toEqual([' XY'])
expect(read.tail.join('\n')).not.toContain('ABCXY')
expect(read.tail.join('\n')).not.toContain('2K')
})
it('retains split multi-line ANSI footer redraw state across chunks', async () => {
const runtime = new OrcaRuntimeService(store)
syncSinglePty(runtime)
const [terminal] = (await runtime.listTerminals()).terminals
runtime.onPtyData('pty-1', '• Working\nTool call\n', 100)
const beforeRedraw = await runtime.readTerminal(terminal.handle, { cursor: 0 })
expect(beforeRedraw.tail).toEqual(['• Working', 'Tool call'])
expect(beforeRedraw.nextCursor).toBe('2')
runtime.onPtyData('pty-1', '\x1b[2A', 101)
const betweenChunks = await runtime.readTerminal(terminal.handle)
expect(betweenChunks.tail).toEqual(['• Working'])
expect(betweenChunks.latestCursor).toBe('2')
runtime.onPtyData('pty-1', '\x1b[2K\x1b[1G• Working.\n\x1b[2K\x1b[1GTool call finished\n', 102)
const read = await runtime.readTerminal(terminal.handle)
expect(read.tail).toEqual(['• Working.', 'Tool call finished'])
expect(read.latestCursor).toBe('4')
const cursorRead = await runtime.readTerminal(terminal.handle, {
cursor: Number(beforeRedraw.nextCursor)
})
expect(cursorRead.tail).toEqual(['• Working.', 'Tool call finished'])
expect(cursorRead.oldestCursor).toBe('2')
expect(cursorRead.nextCursor).toBe('4')
})
it('does not let stale lower rows hide earlier corrected footer rows from cursor reads', async () => {
const runtime = new OrcaRuntimeService(store)
syncSinglePty(runtime)
const [terminal] = (await runtime.listTerminals()).terminals
runtime.onPtyData('pty-1', 'A\nB\nC\n', 100)
const beforeRedraw = await runtime.readTerminal(terminal.handle, { cursor: 0 })
expect(beforeRedraw.tail).toEqual(['A', 'B', 'C'])
expect(beforeRedraw.nextCursor).toBe('3')
runtime.onPtyData('pty-1', '\x1b[2A\x1b[2K\x1b[1GB2\n', 101)
const read = await runtime.readTerminal(terminal.handle)
expect(read.tail).toEqual(['A', 'B2'])
expect(read.latestCursor).toBe('4')
const cursorRead = await runtime.readTerminal(terminal.handle, {
cursor: Number(beforeRedraw.nextCursor)
})
expect(cursorRead.tail).toEqual(['B2'])
expect(cursorRead.oldestCursor).toBe('2')
expect(cursorRead.nextCursor).toBe('4')
})
it('retains multi-line ANSI redraws when the last footer row stays partial', async () => {
const runtime = new OrcaRuntimeService(store)
syncSinglePty(runtime)
const [terminal] = (await runtime.listTerminals()).terminals
runtime.onPtyData('pty-1', '• Working\nTool call\n', 100)
const beforeRedraw = await runtime.readTerminal(terminal.handle, { cursor: 0 })
expect(beforeRedraw.nextCursor).toBe('2')
runtime.onPtyData(
'pty-1',
'\x1b[2A\x1b[2K\x1b[1G• Working.\n\x1b[2K\x1b[1GTool call still running',
101
)
const read = await runtime.readTerminal(terminal.handle)
expect(read.tail).toEqual(['• Working.', 'Tool call still running'])
expect(read.latestCursor).toBe('3')
const cursorRead = await runtime.readTerminal(terminal.handle, {
cursor: Number(beforeRedraw.nextCursor)
})
expect(cursorRead.tail).toEqual(['• Working.'])
expect(cursorRead.oldestCursor).toBe('2')
expect(cursorRead.nextCursor).toBe('3')
runtime.onPtyData('pty-1', '\n', 102)
const completedPartialRead = await runtime.readTerminal(terminal.handle, { cursor: 3 })
expect(completedPartialRead.tail).toEqual(['Tool call still running'])
expect(completedPartialRead.nextCursor).toBe('4')
})
it('does not retain split ANSI controls as visible terminal preview text', async () => {
const runtime = new OrcaRuntimeService(store)
syncSinglePty(runtime)
@ -7501,6 +7658,133 @@ describe('OrcaRuntimeService', () => {
expect(pty?.lastOscTitle).toBe('x'.repeat(MAX_OSC_TITLE_CHARS))
})
it('applies ANSI split redraw controls without leaking raw params', async () => {
const runtime = new OrcaRuntimeService(store)
syncSinglePty(runtime)
const [terminal] = (await runtime.listTerminals()).terminals
runtime.onPtyData('pty-1', 'Frame old', 100)
runtime.onPtyData('pty-1', '\x1b[', 101)
const pendingEraseRead = await runtime.readTerminal(terminal.handle)
expect(pendingEraseRead.tail).toEqual(['Frame old'])
expect(pendingEraseRead.tail.join('\n')).not.toContain('[')
runtime.onPtyData('pty-1', '2K\x1b[', 102)
const pendingColumnRead = await runtime.readTerminal(terminal.handle)
expect(pendingColumnRead.tail.join('\n')).not.toContain('2K')
expect(pendingColumnRead.tail.join('\n')).not.toContain('1G')
runtime.onPtyData('pty-1', '1GFrame new\n', 103)
const read = await runtime.readTerminal(terminal.handle)
const retained = read.tail.join('\n')
expect(read.tail).toEqual(['Frame new'])
expect(retained).not.toContain('2K')
expect(retained).not.toContain('1G')
})
it('retains same-line redraw cursor position across split full-line erase chunks', async () => {
const runtime = new OrcaRuntimeService(store)
syncSinglePty(runtime)
const [terminal] = (await runtime.listTerminals()).terminals
runtime.onPtyData('pty-1', 'ABC', 100)
runtime.onPtyData('pty-1', '\x1b[2K', 101)
const erasedRead = await runtime.readTerminal(terminal.handle)
expect(erasedRead.tail).toEqual([])
runtime.onPtyData('pty-1', 'XY\n', 102)
const read = await runtime.readTerminal(terminal.handle)
expect(read.tail).toEqual([' XY'])
expect(read.tail.join('\n')).not.toContain('ABCXY')
expect(read.tail.join('\n')).not.toContain('2K')
})
it('keeps huge ANSI cursor movement params bounded in retained previews', async () => {
const runtime = new OrcaRuntimeService(store)
syncSinglePty(runtime)
const [terminal] = (await runtime.listTerminals()).terminals
const infinityParam = '9'.repeat(400)
runtime.onPtyData('pty-1', `A\x1b[1000000000CZ\n`, 100)
runtime.onPtyData('pty-1', `B\x1b[${infinityParam}GQ\n`, 101)
for (let index = 0; index < 50; index += 1) {
runtime.onPtyData('pty-1', `R${index}\x1b[2K\x1b[999999CZ\n`, 102 + index)
}
const read = await runtime.readTerminal(terminal.handle, { cursor: 0, limit: 60 })
expect(read.tail).toHaveLength(52)
for (const line of read.tail) {
expect(line.length).toBeLessThan(5000)
expect(line).not.toContain('1000000000')
expect(line).not.toContain(infinityParam)
expect(line).not.toContain('999999')
}
expect(read.tail[0]?.startsWith('A')).toBe(true)
expect(read.tail[0]?.endsWith('Z')).toBe(true)
expect(read.tail[1]?.startsWith('B')).toBe(true)
expect(read.tail[1]?.endsWith('Q')).toBe(true)
expect(read.tail.at(-1)?.endsWith('Z')).toBe(true)
})
it('bounds retained work for many newline-separated huge ANSI cursor movements', async () => {
const runtime = new OrcaRuntimeService(store)
syncSinglePty(runtime)
const [terminal] = (await runtime.listTerminals()).terminals
runtime.onPtyData('pty-1', '\x1b[4000GZ\n'.repeat(3000), 100)
const read = await runtime.readTerminal(terminal.handle, { cursor: 0, limit: 2000 })
expect(read.latestCursor).toBe('3000')
expect(read.oldestCursor).not.toBe('0')
expect(read.tail.length).toBeLessThan(100)
for (const line of read.tail) {
expect(line.length).toBeLessThanOrEqual(4000)
expect(line.endsWith('Z')).toBe(true)
expect(line).not.toContain('4000G')
}
})
it('applies ANSI erase-from-start line controls in retained previews', async () => {
const runtime = new OrcaRuntimeService(store)
syncSinglePty(runtime)
const [terminal] = (await runtime.listTerminals()).terminals
runtime.onPtyData('pty-1', 'ABCDE\x1b[3D\x1b[1KXY\n', 100)
const read = await runtime.readTerminal(terminal.handle)
expect(read.tail).toEqual([' XYE'])
expect(read.tail.join('\n')).not.toContain('ABC')
expect(read.tail.join('\n')).not.toContain('1K')
})
it('applies ANSI stripping for private or intermediate CSI line controls', async () => {
const runtime = new OrcaRuntimeService(store)
syncSinglePty(runtime)
const [terminal] = (await runtime.listTerminals()).terminals
runtime.onPtyData('pty-1', 'ABCDE\x1b[?99DXY\n', 100)
runtime.onPtyData('pty-1', 'ABCDE\x1b[1$DXY\n', 101)
const read = await runtime.readTerminal(terminal.handle)
expect(read.tail).toEqual(['ABCDEXY', 'ABCDEXY'])
expect(read.tail.join('\n')).not.toContain('?99D')
expect(read.tail.join('\n')).not.toContain('1$D')
})
it('applies ANSI stripping for unsupported erase-line modes', async () => {
const runtime = new OrcaRuntimeService(store)
syncSinglePty(runtime)
const [terminal] = (await runtime.listTerminals()).terminals
runtime.onPtyData('pty-1', 'Old\x1b[3KNew\n', 100)
const read = await runtime.readTerminal(terminal.handle)
expect(read.tail).toEqual(['OldNew'])
expect(read.tail.join('\n')).not.toContain('3K')
})
it('does not retain split ST-terminated string controls as preview text', async () => {
const runtime = new OrcaRuntimeService(store)
syncSinglePty(runtime)

View File

@ -824,6 +824,7 @@ type RuntimeLeafRecord = RuntimeSyncedLeaf & {
tailBuffer: string[]
tailPartialLine: string
tailPendingAnsi: string
tailRedrawCursor: RetainedTailRedrawCursor | null
tailTruncated: boolean
tailLinesTotal: number
preview: string
@ -885,6 +886,7 @@ type RuntimePtyWorktreeRecord = {
tailBuffer: string[]
tailPartialLine: string
tailPendingAnsi: string
tailRedrawCursor: RetainedTailRedrawCursor | null
tailTruncated: boolean
tailLinesTotal: number
preview: string
@ -2556,6 +2558,7 @@ export class OrcaRuntimeService {
tailBuffer: tailSource?.tailBuffer ?? [],
tailPartialLine: tailSource?.tailPartialLine ?? '',
tailPendingAnsi: tailSource?.tailPendingAnsi ?? '',
tailRedrawCursor: tailSource?.tailRedrawCursor ?? null,
tailTruncated: tailSource?.tailTruncated ?? false,
tailLinesTotal: tailSource?.tailLinesTotal ?? 0,
preview: tailSource?.preview ?? '',
@ -4737,6 +4740,7 @@ export class OrcaRuntimeService {
lines: pty.tailBuffer,
partialLine: pty.tailPartialLine,
pendingAnsi: pty.tailPendingAnsi,
redrawCursor: pty.tailRedrawCursor,
truncated: pty.tailTruncated,
linesTotal: pty.tailLinesTotal
}
@ -4751,11 +4755,13 @@ export class OrcaRuntimeService {
const nextTail = appendNormalizedToTailBuffer(
pty.tailBuffer,
pty.tailPartialLine,
normalized.text
normalized.text,
pty.tailRedrawCursor
)
ptyTailAfter = nextTail
pty.tailBuffer = nextTail.lines
pty.tailPartialLine = nextTail.partialLine
pty.tailRedrawCursor = nextTail.redrawCursor
pty.tailTruncated = pty.tailTruncated || nextTail.truncated
pty.tailLinesTotal += nextTail.newCompleteLines
pty.preview = buildPreview(pty.tailBuffer, pty.tailPartialLine)
@ -4794,6 +4800,7 @@ export class OrcaRuntimeService {
leaf.tailBuffer,
leaf.tailPartialLine,
leaf.tailPendingAnsi,
leaf.tailRedrawCursor,
leaf.tailTruncated,
leaf.tailLinesTotal,
ptyTailBefore
@ -4804,6 +4811,7 @@ export class OrcaRuntimeService {
leaf.tailBuffer = pty.tailBuffer
leaf.tailPartialLine = pty.tailPartialLine
leaf.tailPendingAnsi = pty.tailPendingAnsi
leaf.tailRedrawCursor = pty.tailRedrawCursor
leaf.tailTruncated = pty.tailTruncated
leaf.tailLinesTotal = pty.tailLinesTotal
leaf.preview = pty.preview
@ -4813,10 +4821,12 @@ export class OrcaRuntimeService {
const nextTail = appendNormalizedToTailBuffer(
leaf.tailBuffer,
leaf.tailPartialLine,
normalized.text
normalized.text,
leaf.tailRedrawCursor
)
leaf.tailBuffer = nextTail.lines
leaf.tailPartialLine = nextTail.partialLine
leaf.tailRedrawCursor = nextTail.redrawCursor
leaf.tailTruncated = leaf.tailTruncated || nextTail.truncated
leaf.tailLinesTotal += nextTail.newCompleteLines
leaf.preview = buildPreview(leaf.tailBuffer, leaf.tailPartialLine)
@ -16112,6 +16122,7 @@ export class OrcaRuntimeService {
tailBuffer: [],
tailPartialLine: '',
tailPendingAnsi: '',
tailRedrawCursor: null,
tailTruncated: false,
tailLinesTotal: 0,
preview: state.preview ?? ''
@ -16231,6 +16242,7 @@ export class OrcaRuntimeService {
pty.tailBuffer = []
pty.tailPartialLine = ''
pty.tailPendingAnsi = ''
pty.tailRedrawCursor = null
pty.tailTruncated = false
pty.tailLinesTotal = 0
}
@ -20651,10 +20663,12 @@ function buildTerminalWaitText(lines: string[], partialLine: string, preview: st
export function appendNormalizedToTailBuffer(
previousLines: string[],
previousPartialLine: string,
normalizedChunk: string
normalizedChunk: string,
previousRedrawCursor: RetainedTailRedrawCursor | null = null
): {
lines: string[]
partialLine: string
redrawCursor: RetainedTailRedrawCursor | null
truncated: boolean
newCompleteLines: number
} {
@ -20662,6 +20676,7 @@ export function appendNormalizedToTailBuffer(
return {
lines: previousLines,
partialLine: previousPartialLine,
redrawCursor: previousRedrawCursor,
truncated: false,
newCompleteLines: 0
}
@ -20671,28 +20686,38 @@ export function appendNormalizedToTailBuffer(
// larger line transcript for pagination, but keep partial-line work bounded.
const previousPartialWasCapped = previousPartialLine.length > MAX_TAIL_PARTIAL_CHARS
const boundedPreviousPartialLine = previousPartialLine.slice(-MAX_TAIL_PARTIAL_CHARS)
// Why: status UIs redraw a single line with CR/backspace/ANSI erase
// controls. Terminal previews are text, not a full screen model, so retain
// the latest visible redraw segment instead of appending every spinner frame.
const completedLines: string[] = []
const combined = `${boundedPreviousPartialLine}${normalizedChunk}`
let lineStart = 0
for (let index = 0; index < combined.length; index += 1) {
if (combined.charCodeAt(index) !== 0x0a) {
continue
}
completedLines.push(
trimTerminalLineRight(applyTerminalLineControls(combined.slice(lineStart, index)))
const combinedChunk = `${boundedPreviousPartialLine}${normalizedChunk}`
if (previousRedrawCursor || containsTerminalVerticalLineControl(combinedChunk)) {
return appendNormalizedToMultilineTailBuffer(
previousLines,
boundedPreviousPartialLine,
normalizedChunk,
previousPartialWasCapped,
previousRedrawCursor
)
lineStart = index + 1
}
const nextPartialLine = trimTerminalLineRight(
applyTerminalLineControls(combined.slice(lineStart))
)
// Why: status UIs redraw a single line with CR/backspace/ANSI erase controls.
// Terminal previews are text, not a full screen model, so retain the latest
// visible redraw segment instead of appending every spinner frame.
const segments = splitRetainedTerminalTailSegments(combinedChunk)
const pieces = processTerminalTailCompleteSegments(segments.completeSegments)
const partialResult = applyTerminalLineControls(segments.partialSegment)
const nextPartialLine = trimTerminalLineRight(partialResult.text)
const retainedPartialLine = nextPartialLine.slice(-MAX_TAIL_PARTIAL_CHARS)
const newCompleteLines = completedLines.length
let nextLines = newCompleteLines > 0 ? [...previousLines, ...completedLines] : previousLines
let truncated = previousPartialWasCapped || nextPartialLine.length > MAX_TAIL_PARTIAL_CHARS
const newCompleteLines = segments.completeLineCount
const omittedNewCompleteLines = newCompleteLines - pieces.length
let nextLines =
newCompleteLines > 0
? [
...(omittedNewCompleteLines > 0 ? [] : previousLines),
...pieces.map((line) => line.replace(/[ \t]+$/g, ''))
]
: previousLines
let truncated =
previousPartialWasCapped ||
omittedNewCompleteLines > 0 ||
nextPartialLine.length > MAX_TAIL_PARTIAL_CHARS
if (nextLines.length > MAX_TAIL_LINES) {
nextLines = nextLines.slice(nextLines.length - MAX_TAIL_LINES)
@ -20716,9 +20741,18 @@ export function appendNormalizedToTailBuffer(
}
}
const redrawCursor =
!partialResult.hadControl || partialResult.cursorColumn === nextPartialLine.length
? null
: {
rowFromEnd: 0,
column: partialResult.cursorColumn
}
return {
lines: nextLines,
partialLine: retainedPartialLine,
redrawCursor,
truncated,
newCompleteLines
}
@ -20736,16 +20770,303 @@ function trimTerminalLineRight(line: string): string {
return end === line.length ? line : line.slice(0, end)
}
function applyTerminalLineControls(line: string): string {
function appendNormalizedToMultilineTailBuffer(
previousLines: string[],
boundedPreviousPartialLine: string,
normalizedChunk: string,
previousPartialWasCapped: boolean,
previousRedrawCursor: RetainedTailRedrawCursor | null
): {
lines: string[]
partialLine: string
redrawCursor: RetainedTailRedrawCursor | null
truncated: boolean
newCompleteLines: number
} {
const rows: RetainedTerminalRow[] = [
...previousLines.map((line) => ({ text: line, completed: true })),
{ text: boundedPreviousPartialLine, completed: false }
]
let cursorRow = previousRedrawCursor
? Math.max(0, rows.length - 1 - previousRedrawCursor.rowFromEnd)
: rows.length - 1
let cursorColumn = previousRedrawCursor?.column ?? boundedPreviousPartialLine.length
let newCompleteLines = 0
let truncated = previousPartialWasCapped
const ensureCursorRow = (): void => {
while (cursorRow >= rows.length) {
rows.push({ text: '', completed: false })
}
}
const trimRows = (): void => {
const maxRows = MAX_TAIL_LINES + 1
if (rows.length <= maxRows) {
return
}
const removeCount = rows.length - maxRows
rows.splice(0, removeCount)
cursorRow = Math.max(0, cursorRow - removeCount)
truncated = true
}
const moveCursorToColumn = (nextColumn: number): void => {
cursorColumn = clampTerminalPreviewCursor(nextColumn)
}
const markCursorRowRewritten = (): void => {
ensureCursorRow()
rows[cursorRow]!.completed = false
}
const writeChar = (char: string): void => {
ensureCursorRow()
markCursorRowRewritten()
const row = rows[cursorRow]!
if (cursorColumn > row.text.length) {
row.text = `${row.text}${' '.repeat(cursorColumn - row.text.length)}`
}
row.text =
cursorColumn >= row.text.length
? `${row.text}${char}`
: `${row.text.slice(0, cursorColumn)}${char}${row.text.slice(cursorColumn + 1)}`
cursorColumn += 1
}
const eraseLine = (mode: number): void => {
ensureCursorRow()
markCursorRowRewritten()
const row = rows[cursorRow]!
if (mode === 0) {
row.text = row.text.slice(0, cursorColumn)
} else if (mode === 1) {
const deleteCount = Math.min(cursorColumn + 1, row.text.length)
row.text = `${' '.repeat(deleteCount)}${row.text.slice(deleteCount)}`
} else if (mode === 2) {
row.text = ''
}
}
for (let index = 0; index < normalizedChunk.length; index += 1) {
const char = normalizedChunk[index]
if (char === '\n') {
ensureCursorRow()
rows[cursorRow]!.completed = true
newCompleteLines += 1
cursorRow += 1
cursorColumn = 0
ensureCursorRow()
trimRows()
continue
}
if (char === '\r') {
cursorColumn = 0
continue
}
if (char === '\u0008') {
cursorColumn = Math.max(0, cursorColumn - 1)
continue
}
if (char === '\u001b') {
const parsed = parseAnsiControlSequence(normalizedChunk, index)
if (!parsed) {
continue
}
index = parsed.endIndex
if (parsed.kind !== 'csi' || !hasCanonicalNumericCsiParams(parsed.params)) {
continue
}
const firstParam = parsed.firstParam ?? 1
if (parsed.final === 'A') {
cursorRow = Math.max(0, cursorRow - firstParam)
rows.splice(cursorRow + 1)
} else if (parsed.final === 'K') {
eraseLine(parsed.firstParam ?? 0)
} else if (parsed.final === 'G' || parsed.final === '`') {
moveCursorToColumn(firstParam - 1)
} else if (parsed.final === 'D') {
cursorColumn = Math.max(0, cursorColumn - firstParam)
} else if (parsed.final === 'C') {
moveCursorToColumn(cursorColumn + firstParam)
}
continue
}
writeChar(char)
}
return finalizeRetainedTerminalRows(rows, cursorRow, cursorColumn, truncated, newCompleteLines)
}
type RetainedTailRedrawCursor = {
rowFromEnd: number
column: number
}
type RetainedTerminalRow = {
text: string
completed: boolean
}
function finalizeRetainedTerminalRows(
rows: RetainedTerminalRow[],
cursorRow: number,
cursorColumn: number,
initialTruncated: boolean,
newCompleteLines: number
): {
lines: string[]
partialLine: string
redrawCursor: RetainedTailRedrawCursor | null
truncated: boolean
newCompleteLines: number
} {
let truncated = initialTruncated
let retainedRows = rows.map((row) => ({ ...row, text: row.text.replace(/[ \t]+$/g, '') }))
if (retainedRows.length > MAX_TAIL_LINES + 1) {
const removeCount = retainedRows.length - (MAX_TAIL_LINES + 1)
retainedRows = retainedRows.slice(removeCount)
cursorRow = Math.max(0, cursorRow - removeCount)
truncated = true
}
let totalChars = retainedRows.reduce((sum, row) => sum + row.text.length, 0)
let trimStartIndex = 0
while (trimStartIndex < retainedRows.length - 1 && totalChars > MAX_TAIL_CHARS) {
totalChars -= retainedRows[trimStartIndex]!.text.length
trimStartIndex += 1
}
if (trimStartIndex > 0) {
retainedRows = retainedRows.slice(trimStartIndex)
cursorRow = Math.max(0, cursorRow - trimStartIndex)
truncated = true
}
while (
retainedRows.length > 1 &&
cursorRow < retainedRows.length - 1 &&
retainedRows.at(-1)?.completed === false &&
retainedRows.at(-1)?.text.length === 0
) {
retainedRows.pop()
}
const lastRow = retainedRows.at(-1)
let partialLine = lastRow && !lastRow.completed ? lastRow.text : ''
let lines = (lastRow && !lastRow.completed ? retainedRows.slice(0, -1) : retainedRows).map(
(row) => row.text
)
if (partialLine.length > MAX_TAIL_PARTIAL_CHARS) {
partialLine = partialLine.slice(-MAX_TAIL_PARTIAL_CHARS)
truncated = true
}
if (lines.length > MAX_TAIL_LINES) {
lines = lines.slice(lines.length - MAX_TAIL_LINES)
truncated = true
}
const outputRowCount = lines.length + 1
const defaultCursorRow = outputRowCount - 1
const defaultCursorColumn = partialLine.length
const redrawCursor =
cursorRow === defaultCursorRow && cursorColumn === defaultCursorColumn
? null
: {
rowFromEnd: Math.max(0, outputRowCount - 1 - cursorRow),
column: clampTerminalPreviewCursor(cursorColumn)
}
return {
lines,
partialLine,
redrawCursor,
truncated,
newCompleteLines
}
}
function splitRetainedTerminalTailSegments(value: string): {
completeSegments: string[]
partialSegment: string
completeLineCount: number
} {
let completeLineCount = 0
for (let index = 0; index < value.length; index += 1) {
if (value[index] === '\n') {
completeLineCount += 1
}
}
const retainedCompleteCount = Math.min(completeLineCount, MAX_TAIL_LINES)
const omittedCompleteCount = completeLineCount - retainedCompleteCount
let startIndex = 0
if (omittedCompleteCount > 0) {
let seen = 0
for (let index = 0; index < value.length; index += 1) {
if (value[index] !== '\n') {
continue
}
seen += 1
if (seen === omittedCompleteCount) {
startIndex = index + 1
break
}
}
}
const completeSegments: string[] = []
let segmentStart = startIndex
for (let index = startIndex; index < value.length; index += 1) {
if (value[index] !== '\n') {
continue
}
completeSegments.push(value.slice(segmentStart, index))
segmentStart = index + 1
}
return {
completeSegments,
partialSegment: value.slice(segmentStart),
completeLineCount
}
}
function processTerminalTailCompleteSegments(segments: string[]): string[] {
const processed: string[] = []
let totalChars = 0
for (let index = segments.length - 1; index >= 0; index -= 1) {
const line = applyTerminalLineControls(segments[index]!).text
processed.push(line)
totalChars += line.length
if (totalChars > MAX_TAIL_CHARS) {
break
}
}
processed.reverse()
return processed
}
function applyTerminalLineControls(line: string): {
text: string
cursorColumn: number
hadControl: boolean
} {
const carriageIndex = line.lastIndexOf('\r')
const latestRedraw = carriageIndex >= 0 ? line.slice(carriageIndex + 1) : line
if (!latestRedraw.includes('\u0008') && !latestRedraw.includes('\u001b')) {
return latestRedraw
return {
text: latestRedraw,
cursorColumn: latestRedraw.length,
hadControl: carriageIndex >= 0
}
}
const chars: string[] = []
let cursor = 0
const moveCursorTo = (nextCursor: number): void => {
cursor = clampTerminalPreviewCursor(nextCursor)
}
const writeChar = (char: string): void => {
if (cursor > chars.length) {
const oldLength = chars.length
chars.length = cursor
chars.fill(' ', oldLength, cursor)
}
if (cursor >= chars.length) {
chars.push(char)
} else {
@ -20768,36 +21089,45 @@ function applyTerminalLineControls(line: string): string {
if (parsed.kind !== 'csi') {
continue
}
if (!hasCanonicalNumericCsiParams(parsed.params)) {
continue
}
if (parsed.final === 'K') {
const mode = parsed.firstParam ?? 0
if (mode === 0) {
chars.length = cursor
} else if (mode === 1) {
chars.splice(0, cursor)
cursor = 0
} else if (mode === 2 || mode === 3) {
const deleteCount = Math.min(cursor + 1, chars.length)
chars.fill(' ', 0, deleteCount)
} else if (mode === 2) {
chars.length = 0
cursor = 0
}
} else if (parsed.final === 'G' || parsed.final === '`') {
cursor = Math.min(chars.length, Math.max(0, (parsed.firstParam ?? 1) - 1))
moveCursorTo((parsed.firstParam ?? 1) - 1)
} else if (parsed.final === 'D') {
cursor = Math.max(0, cursor - (parsed.firstParam ?? 1))
} else if (parsed.final === 'C') {
cursor = Math.min(chars.length, cursor + (parsed.firstParam ?? 1))
moveCursorTo(cursor + (parsed.firstParam ?? 1))
}
} else {
writeChar(char)
}
}
return chars.join('')
return { text: chars.join(''), cursorColumn: cursor, hadControl: true }
}
function clampTerminalPreviewCursor(nextCursor: number): number {
if (!Number.isFinite(nextCursor)) {
return MAX_TAIL_PARTIAL_CHARS
}
return Math.min(MAX_TAIL_PARTIAL_CHARS, Math.max(0, Math.floor(nextCursor)))
}
function parseAnsiControlSequence(
value: string,
escapeIndex: number
):
| { kind: 'csi'; final: string; firstParam: number | null; endIndex: number }
| { kind: 'csi'; final: string; params: string; firstParam: number | null; endIndex: number }
| {
kind: 'other'
endIndex: number
@ -20811,10 +21141,11 @@ function parseAnsiControlSequence(
continue
}
const params = value.slice(escapeIndex + 2, index)
const firstParamMatch = /^\??(\d+)/.exec(params)
const firstParamMatch = /^(\d+)/.exec(params)
return {
kind: 'csi',
final: value[index] ?? '',
params,
firstParam: firstParamMatch ? Number(firstParamMatch[1]) : null,
endIndex: index
}
@ -20847,16 +21178,43 @@ function isStTerminatedStringControlIntroducer(introducer: string | undefined):
return introducer === 'P' || introducer === 'X' || introducer === '^' || introducer === '_'
}
function hasCanonicalNumericCsiParams(params: string): boolean {
return /^[0-9;]*$/.test(params)
}
function containsTerminalVerticalLineControl(value: string): boolean {
for (let index = 0; index < value.length; index += 1) {
if (value[index] !== '\u001b') {
continue
}
const parsed = parseAnsiControlSequence(value, index)
if (!parsed) {
return false
}
index = parsed.endIndex
if (
parsed.kind === 'csi' &&
parsed.final === 'A' &&
hasCanonicalNumericCsiParams(parsed.params)
) {
return true
}
}
return false
}
function tailStateMatches(
lines: string[],
partialLine: string,
pendingAnsi: string,
redrawCursor: RetainedTailRedrawCursor | null,
truncated: boolean,
linesTotal: number,
snapshot: {
lines: string[]
partialLine: string
pendingAnsi: string
redrawCursor: RetainedTailRedrawCursor | null
truncated: boolean
linesTotal: number
}
@ -20864,6 +21222,7 @@ function tailStateMatches(
if (
partialLine !== snapshot.partialLine ||
pendingAnsi !== snapshot.pendingAnsi ||
!tailRedrawCursorsMatch(redrawCursor, snapshot.redrawCursor) ||
truncated !== snapshot.truncated ||
linesTotal !== snapshot.linesTotal ||
lines.length !== snapshot.lines.length
@ -20881,6 +21240,19 @@ function tailStateMatches(
return true
}
function tailRedrawCursorsMatch(
left: RetainedTailRedrawCursor | null,
right: RetainedTailRedrawCursor | null
): boolean {
if (left === right) {
return true
}
if (!left || !right) {
return false
}
return left.rowFromEnd === right.rowFromEnd && left.column === right.column
}
function buildTailLines(lines: string[], partialLine: string): string[] {
return partialLine.length > 0 ? [...lines, partialLine] : lines
}
@ -21514,6 +21886,11 @@ function normalizeTerminalChunk(
pendingAnsi: trimPendingAnsiControl(combined.slice(index))
}
}
if (parsed.kind === 'csi' && isTerminalPreviewLineControl(parsed)) {
// Why: Codex can redraw status text with ANSI controls but no CR; keep
// those controls so the tail buffer overwrites the previous frame.
parts.push(combined.slice(index, parsed.endIndex + 1))
}
index = parsed.endIndex
textStart = index + 1
continue
@ -21580,6 +21957,27 @@ function trimPendingAnsiControl(value: string): string {
return `${introducer}${value.slice(-suffixBudget)}`
}
function isTerminalPreviewLineControl(parsed: {
final: string
params: string
firstParam: number | null
}): boolean {
if (!hasCanonicalNumericCsiParams(parsed.params)) {
return false
}
if (parsed.final === 'K') {
const mode = parsed.firstParam ?? 0
return mode === 0 || mode === 1 || mode === 2
}
return (
parsed.final === 'A' ||
parsed.final === 'G' ||
parsed.final === '`' ||
parsed.final === 'D' ||
parsed.final === 'C'
)
}
function maxTimestamp(left: number | null, right: number | null): number | null {
if (left === null) {
return right