Files
orca/src/shared/codex-composer-ready-scanner.ts
T
Neil ac3a25ca37 Wait for Codex's live composer before pasting linked issue drafts (#25779)
* Wait for Codex's live composer before pasting launch drafts

* Keep timed-out Codex draft delivery consumed across remounts

* Document the checked terminal fixtures used by the remount test

* Distinguish Codex's reserved footer row from early multiline input

* Complete transcript baselines and suppress hidden terminal strings

* Align watcher scan budget with linked-directory reconciliation

* Drop unused snapshots after main removed the readiness census
2026-10-05 23:38:28 -07:00

97 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createTerminalEscapeScanner } from './terminal-escape-scanner'
/** The fullscreen loading frame has a composer too; only the live chat paints its footer. */
export function createCodexComposerReadyScanner(): {
observe: (data: string) => { ready: boolean }
} {
let bracketedPaste = false
let altScreen = false
let leftAltScreen = false
let synchronized = false
let sawSynchronizedFrame = false
let promptSeen = false
let promptRow: number | null = null
let footerRow: number | null = null
let hintRow: number | null = null
let row: number | null = null
let cursorShown = false
const paintedRows = new Set<number>()
const resetLayout = (): void => {
promptSeen = false
promptRow = null
footerRow = null
hintRow = null
row = null
cursorShown = false
paintedRows.clear()
}
const scanner = createTerminalEscapeScanner({
onCsi: (privateMarker, params, final) => {
if (privateMarker === '?' && (final === 'h' || final === 'l')) {
const set = final === 'h'
for (const mode of params.split(';')) {
if (mode === '2004') {
bracketedPaste = set
if (!set) {
resetLayout()
}
} else if (mode === '1049') {
altScreen = set
leftAltScreen = !set
resetLayout()
} else if (mode === '2026') {
synchronized = set
sawSynchronizedFrame = true
} else if (mode === '25') {
cursorShown = set
}
}
} else if (final === 'H' || final === 'f') {
row = Math.max(1, Number(params.split(';')[0] || '1'))
} else if (final === 'J' && params === '2') {
resetLayout()
}
},
onText: (text) => {
for (const char of text) {
if (row !== null && !/\s/.test(char) && paintedRows.size < 128) {
paintedRows.add(row)
}
if (char === '\n' && row !== null) {
row += 1
} else if (char === '›' && (bracketedPaste || altScreen)) {
promptSeen = true
promptRow = row
} else if (char === '·' && promptRow !== null && row !== null && row > promptRow) {
footerRow = row
} else if (char === '?' && promptRow !== null && row !== null && row > promptRow) {
hintRow = Math.max(hintRow ?? row, row)
}
}
}
})
return {
observe: (data) => {
scanner.observe(data)
const liveFullscreenComposer =
altScreen &&
!synchronized &&
cursorShown &&
promptRow !== null &&
row !== null &&
row >= promptRow &&
hintRow !== null &&
// Single-item status lines have no separator; their reserved row is below the input padding.
((footerRow !== null && footerRow === hintRow - 1 && row < footerRow) ||
(row + 2 < hintRow && paintedRows.has(hintRow - 1)))
// Older inline builds do not paint synchronized fullscreen frames.
const legacyComposer = promptSeen && !leftAltScreen && (!altScreen || !sawSynchronizedFrame)
return {
ready: bracketedPaste && (liveFullscreenComposer || legacyComposer)
}
}
}
}