Avoid clearing terminal scrollback for metadata-only eager replays (#7012)

Previously, the terminal pane would always perform a clear screen on PTY
attach before replaying the eager buffer. This cleared restored scrollback
unconditionally, even when there was no active terminal content to replay
(e.g., when the buffer was empty or only contained window titles and
agent status metadata).

Now, eager buffers are parsed to determine if they contain display-affecting
content. The attach-time clear is skipped for empty or metadata-only
replays, preserving the restored scrollback history. Trailing partial
control sequences are also trimmed to prevent swallowing live terminal output.
This commit is contained in:
Jinjing
2026-07-01 02:57:14 -07:00
committed by GitHub
parent 16a14ec0cf
commit 252e5f6af5
7 changed files with 487 additions and 53 deletions
@@ -141,7 +141,7 @@ type StoreState = {
type ConnectCallbacks = {
onData?: (data: string, meta?: { seq?: number; rawLength?: number }) => void
onReplayData?: (data: string) => void
onReplayData?: (data: string, meta?: { clearBeforeReplay?: boolean }) => void
onError?: (msg: string) => void
}
@@ -7981,6 +7981,69 @@ describe('connectPanePty', () => {
disposable.dispose()
})
it('does not clear restored scrollback when eager metadata replay opts out', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedReplayCallback: {
current: ((data: string, meta?: { clearBeforeReplay?: boolean }) => void) | null
} = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedReplayCallback.current = callbacks.onReplayData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const pane = createPane(1)
pane.terminal.write = vi.fn((_data: string, callback?: () => void) => {
callback?.()
})
const manager = createManager(1)
const deps = createDeps()
const disposable = connectPanePty(pane as never, manager as never, deps as never)
await flushAsyncTicks(6)
capturedReplayCallback.current?.('\x1b]0;Restored title\x07', { clearBeforeReplay: false })
await flushAsyncTicks(6)
expect(pane.terminal.write).not.toHaveBeenCalledWith(
'\x1b[2J\x1b[3J\x1b[H',
expect.any(Function)
)
expect(pane.terminal.write).toHaveBeenCalledWith(
'\x1b]0;Restored title\x07',
expect.any(Function)
)
disposable.dispose()
})
it('does not write a clear or reset for empty eager metadata replay', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedReplayCallback: {
current: ((data: string, meta?: { clearBeforeReplay?: boolean }) => void) | null
} = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedReplayCallback.current = callbacks.onReplayData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const pane = createPane(1)
pane.terminal.write = vi.fn((_data: string, callback?: () => void) => {
callback?.()
})
const manager = createManager(1)
const deps = createDeps()
const disposable = connectPanePty(pane as never, manager as never, deps as never)
await flushAsyncTicks(6)
capturedReplayCallback.current?.('', { clearBeforeReplay: false })
await flushAsyncTicks(6)
expect(pane.terminal.write).not.toHaveBeenCalled()
disposable.dispose()
})
it('coalesces remote replay payloads that overlap before parsing starts', async () => {
const { connectPanePty } = await import('./pty-connection')
enableActiveRuntimeEnvironment()
@@ -3263,18 +3263,27 @@ export function connectPanePty(
}
let replayWriteQueue = Promise.resolve()
let pendingReplayData: string | null = null
type PendingReplayData = {
data: string
clearBeforeReplay: boolean
}
let pendingReplayData: PendingReplayData | null = null
let replayDrainQueued = false
const drainReplayDataQueue = async (): Promise<void> => {
while (pendingReplayData !== null) {
const data = pendingReplayData
const { data, clearBeforeReplay } = pendingReplayData
pendingReplayData = null
// Relay replay buffer holds the last 100 KB of output, which may
// overlap with content already rendered in xterm before the
// disconnect. Clear first to prevent duplication on SSH reconnect.
await writeReplayDataAsync('\x1b[2J\x1b[3J\x1b[H')
// Relay replay buffers may overlap with content already rendered in
// xterm. Local eager replay decides this earlier so metadata-only frames
// can keep restored scrollback while still using the replay guard.
if (clearBeforeReplay) {
await writeReplayDataAsync('\x1b[2J\x1b[3J\x1b[H')
}
await writeReplayDataAsync(data)
await writeReplayDataAsync(POST_REPLAY_REATTACH_RESET)
if (clearBeforeReplay || data.length > 0) {
await writeReplayDataAsync(POST_REPLAY_REATTACH_RESET)
}
if (disposed) {
pendingReplayData = null
return
@@ -3285,8 +3294,11 @@ export function connectPanePty(
manager.rebuildPaneWebgl(pane.id)
}
}
const replayDataCallback = (data: string): void => {
pendingReplayData = data
const replayDataCallback = (data: string, meta: { clearBeforeReplay?: boolean } = {}): void => {
pendingReplayData = {
data,
clearBeforeReplay: meta.clearBeforeReplay !== false
}
if (replayDrainQueued) {
return
}
@@ -3297,7 +3309,9 @@ export function connectPanePty(
.finally(() => {
replayDrainQueued = false
if (pendingReplayData !== null) {
replayDataCallback(pendingReplayData)
replayDataCallback(pendingReplayData.data, {
clearBeforeReplay: pendingReplayData.clearBeforeReplay
})
}
})
}
@@ -38,7 +38,7 @@ type PtyCallbacks = {
onConnect?: () => void
onDisconnect?: () => void
onData?: (data: string, meta?: PtyDataMeta) => void
onReplayData?: (data: string) => void
onReplayData?: (data: string, meta?: { clearBeforeReplay?: boolean }) => void
onStatus?: (shell: string) => void
onError?: (message: string, errors?: string[]) => void
onExit?: (code: number) => void
@@ -715,7 +715,7 @@ describe('createIpcPtyTransport', () => {
expect(onDataCallback).not.toHaveBeenCalledWith(bufferedPayload)
})
it('clears before replaying eager-buffered output so hidden automation terminals do not open blank', async () => {
it('replays display-bearing eager-buffered output with default clear semantics', async () => {
const { createIpcPtyTransport, registerEagerPtyBuffer } = await import('./pty-transport')
const bufferedPayload = '\x1b[?1049hAutomation agent is running'
@@ -735,11 +735,121 @@ describe('createIpcPtyTransport', () => {
}
})
const clear = '\x1b[2J\x1b[3J\x1b[H'
expect(onReplayData.mock.calls.map(([data]) => data)).toEqual([clear, bufferedPayload])
expect(onReplayData.mock.calls).toEqual([[bufferedPayload]])
})
it('routes the attach-time clear sequence through onReplayData for non-alternate-screen sessions', async () => {
it('does not clear before replaying title-only eager-buffered output', async () => {
const { createIpcPtyTransport, registerEagerPtyBuffer } = await import('./pty-transport')
const bufferedPayload = '\x1b]0;Restored title\x07'
registerEagerPtyBuffer('pty-title-only', vi.fn())
onData?.({
id: 'pty-title-only',
data: bufferedPayload
})
const onTitleChange = vi.fn()
const transport = createIpcPtyTransport({ onTitleChange })
const onReplayData = vi.fn()
transport.attach({
existingPtyId: 'pty-title-only',
callbacks: {
onReplayData
}
})
// Why: title/control frames restore metadata but do not redraw a terminal
// frame; clearing before them would erase the persisted scrollback.
const clear = '\x1b[2J\x1b[3J\x1b[H'
expect(onReplayData.mock.calls).toEqual([[bufferedPayload, { clearBeforeReplay: false }]])
expect(onReplayData).not.toHaveBeenCalledWith(clear)
expect(onTitleChange).toHaveBeenCalledWith('Restored title', 'Restored title')
})
it('does not write an unterminated title-only eager buffer into replay', async () => {
const { createIpcPtyTransport, registerEagerPtyBuffer } = await import('./pty-transport')
const bufferedPayload = '\x1b]0;partial restored title'
registerEagerPtyBuffer('pty-partial-title', vi.fn())
onData?.({
id: 'pty-partial-title',
data: bufferedPayload
})
const transport = createIpcPtyTransport()
const onReplayData = vi.fn()
transport.attach({
existingPtyId: 'pty-partial-title',
callbacks: {
onReplayData
}
})
const clear = '\x1b[2J\x1b[3J\x1b[H'
expect(onReplayData.mock.calls).toEqual([['', { clearBeforeReplay: false }]])
expect(onReplayData).not.toHaveBeenCalledWith(clear)
})
it('does not let an unterminated OSC 9999 eager buffer swallow live output', async () => {
const { createIpcPtyTransport, registerEagerPtyBuffer } = await import('./pty-transport')
registerEagerPtyBuffer('pty-partial-status', vi.fn())
onData?.({
id: 'pty-partial-status',
data: '\x1b]9999;{"state":"working"'
})
const transport = createIpcPtyTransport({ onAgentStatus: vi.fn() })
const onReplayData = vi.fn()
const onDataCallback = vi.fn()
transport.attach({
existingPtyId: 'pty-partial-status',
callbacks: {
onData: onDataCallback,
onReplayData
}
})
expect(onReplayData.mock.calls).toEqual([['', { clearBeforeReplay: false }]])
onData?.({
id: 'pty-partial-status',
data: 'live output'
})
expect(onDataCallback).toHaveBeenCalledWith('live output')
})
it('does not clear before replaying OSC 9999-only eager-buffered output', async () => {
const { createIpcPtyTransport, registerEagerPtyBuffer } = await import('./pty-transport')
registerEagerPtyBuffer('pty-status-only', vi.fn())
onData?.({
id: 'pty-status-only',
data: '\x1b]9999;{"state":"working","prompt":"ship it","agentType":"codex"}\x07'
})
const transport = createIpcPtyTransport()
const onReplayData = vi.fn()
transport.attach({
existingPtyId: 'pty-status-only',
callbacks: {
onReplayData
}
})
// Why: OSC 9999 is stripped before xterm receives replay data. A non-empty
// raw status frame must not clear restored scrollback and replay nothing.
const clear = '\x1b[2J\x1b[3J\x1b[H'
expect(onReplayData.mock.calls).toEqual([['', { clearBeforeReplay: false }]])
expect(onReplayData).not.toHaveBeenCalledWith(clear)
})
it('does not clear on attach when there is no eager-buffered output', async () => {
const { createIpcPtyTransport } = await import('./pty-transport')
const transport = createIpcPtyTransport()
@@ -754,15 +864,43 @@ describe('createIpcPtyTransport', () => {
}
})
// Why: the clear preamble must travel the replay path so any subsequent
// snapshot bytes sit under the same replay guard in pty-connection.ts.
const clear = '\x1b[2J\x1b[3J\x1b[H'
expect(onReplayData).toHaveBeenCalledWith(clear)
expect(onDataCallback).not.toHaveBeenCalledWith(clear)
// Why: restored scrollback may already be in xterm before attach. An
// empty eager buffer must not erase it and leave the pane cursor-only.
expect(onReplayData).not.toHaveBeenCalled()
expect(onDataCallback).not.toHaveBeenCalled()
})
it('does not clear on attach when the eager buffer is empty', async () => {
const { createIpcPtyTransport, registerEagerPtyBuffer } = await import('./pty-transport')
registerEagerPtyBuffer('pty-attached', vi.fn())
const transport = createIpcPtyTransport()
const onDataCallback = vi.fn()
const onReplayData = vi.fn()
transport.attach({
existingPtyId: 'pty-attached',
callbacks: {
onData: onDataCallback,
onReplayData
}
})
// Why: a live PTY can have an eager handle before any bytes arrive. Clearing
// here would destroy the scrollback restored by TerminalPane mount.
expect(onReplayData).not.toHaveBeenCalled()
expect(onDataCallback).not.toHaveBeenCalled()
})
it('skips the attach-time clear sequence for alternate-screen sessions', async () => {
const { createIpcPtyTransport } = await import('./pty-transport')
const { createIpcPtyTransport, registerEagerPtyBuffer } = await import('./pty-transport')
const bufferedPayload = '\x1b[?1049hAlternate screen is already restored'
registerEagerPtyBuffer('pty-alt-screen', vi.fn())
onData?.({
id: 'pty-alt-screen',
data: bufferedPayload
})
const transport = createIpcPtyTransport()
const onDataCallback = vi.fn()
@@ -780,6 +918,7 @@ describe('createIpcPtyTransport', () => {
// Why: alternate-screen snapshots already fill the viewport; emitting the
// clear would erase the restored content. Neither path should see it.
const clear = '\x1b[2J\x1b[3J\x1b[H'
expect(onReplayData.mock.calls).toEqual([[bufferedPayload, { clearBeforeReplay: false }]])
expect(onReplayData).not.toHaveBeenCalledWith(clear)
expect(onDataCallback).not.toHaveBeenCalledWith(clear)
})
@@ -26,6 +26,10 @@ import { drainPreHandlerPtyData, drainPreHandlerPtyExit } from './pty-pre-handle
import type { PtyDataMeta } from './pty-dispatcher'
import type { IpcPtyTransportOptions, PtyConnectResult, PtyTransport } from './pty-transport-types'
import { createBellDetector } from './bell-detector'
import {
hasTerminalDisplayContent,
trimIncompleteTerminalControlTail
} from './terminal-output-visibility'
import {
createAgentStatusOscProcessor,
type ProcessedAgentStatusChunk
@@ -81,6 +85,7 @@ type PtyOutputProcessorOptions = Pick<
type ProcessPtyOutputOptions = {
replayingBufferedData?: boolean
suppressAttentionEvents?: boolean
clearBeforeReplay?: boolean
}
type PendingPtySideEffect = {
@@ -398,7 +403,11 @@ export function createPtyOutputProcessor({
// session into the live store. The parser still consumes the bytes so they
// do not leak into xterm, we just suppress the callback.
if (options.replayingBufferedData && callbacks.onReplayData) {
callbacks.onReplayData(data)
if (options.clearBeforeReplay === false) {
callbacks.onReplayData(data, { clearBeforeReplay: false })
} else {
callbacks.onReplayData(data)
}
} else {
if (meta) {
callbacks.onData?.(data, { ...meta, rawLength })
@@ -491,12 +500,6 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
ptyReplayHandlers.delete(id)
}
// Why: true while we're replaying buffered/attach-time bytes into the
// terminal. Routes those bytes through onReplayData so the renderer can
// engage the replay guard — otherwise xterm auto-replies to embedded
// query sequences leak into the shell as stray input.
let replayingBufferedData = false
// Why: shared by connect() and attach() to avoid duplicating title/bell/exit
// logic across the two code paths that register a PTY.
function registerPtyDataHandler(id: string): void {
@@ -521,7 +524,6 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
data,
storedCallbacks,
{
replayingBufferedData,
suppressAttentionEvents
},
meta
@@ -818,26 +820,21 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
return
}
// Why: hidden automation PTYs may have already rendered their TUI into
// the eager buffer. Clear stale pane contents before replaying that
// buffer; clearing afterward erases the only visible frame and opens a
// blank terminal until the TUI happens to repaint.
if (!options.isAlternateScreen) {
const clear = '\x1b[2J\x1b[3J\x1b[H'
if (storedCallbacks.onReplayData) {
storedCallbacks.onReplayData(clear)
} else {
storedCallbacks.onData?.(clear)
}
}
// Why: replay buffered data through the real handler so title/bell/agent
// tracking (including OSC 9999 agent status) processes the output —
// otherwise restored tabs keep a default title.
const bufferHandle = getEagerPtyBufferHandle(id)
if (bufferHandle) {
const buffered = bufferHandle.flush()
if (buffered) {
const replayData = trimIncompleteTerminalControlTail(buffered)
const shouldClearBeforeReplay =
!options.isAlternateScreen && hasTerminalDisplayContent(replayData)
// Why: hidden automation PTYs may have already rendered their TUI into
// the eager buffer. Clear stale pane contents before replaying
// terminal-visible bytes, but keep scrollback for control-only frames.
if (shouldClearBeforeReplay && !storedCallbacks.onReplayData) {
const clear = '\x1b[2J\x1b[3J\x1b[H'
storedCallbacks.onData?.(clear)
}
// Why: eager-buffered bytes are raw PTY output captured before the
// pane mounted — often from the previous app session. We replay
// them so titles/scrollback restore correctly, but must silence
@@ -845,20 +842,21 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
// or completion captured from the prior session must not produce
// a fresh bell on the freshly mounted pane.
//
// replayingBufferedData additionally routes the bytes through
// onReplayData so the renderer engages the replay guard — xterm's
// auto-replies to embedded query sequences would otherwise leak
// into the shell's stdin.
// The replay option routes the bytes through onReplayData so the
// renderer engages the replay guard — xterm's auto-replies to
// embedded query sequences would otherwise leak into shell stdin.
suppressAttentionEvents = true
replayingBufferedData = true
try {
ptyDataHandlers.get(id)?.(buffered)
outputProcessor.processData(replayData, storedCallbacks, {
replayingBufferedData: true,
suppressAttentionEvents: true,
clearBeforeReplay: shouldClearBeforeReplay
})
} finally {
// Why: replay side effects are intentionally deferred for live
// output, but replay cleanup must observe them before resetting
// parser state or a partial OSC can swallow the next live BEL.
outputProcessor.flushPendingSideEffects()
replayingBufferedData = false
suppressAttentionEvents = false
// Why: replaying eager-buffered bytes may have observed a "working" title
// without a follow-up title, starting a stale-title timer. That timer would
@@ -0,0 +1,54 @@
import { describe, expect, it } from 'vitest'
import {
hasTerminalDisplayContent,
trimIncompleteTerminalControlTail
} from './terminal-output-visibility'
describe('hasTerminalDisplayContent', () => {
it('treats title and agent-status OSC frames as metadata-only', () => {
expect(hasTerminalDisplayContent('\x1b]0;Restored title\x07')).toBe(false)
expect(hasTerminalDisplayContent('\x1b]9999;{"state":"working","agentType":"codex"}\x07')).toBe(
false
)
})
it('treats unterminated metadata/control frames as metadata-only', () => {
expect(hasTerminalDisplayContent('\x1b]0;partial title')).toBe(false)
expect(hasTerminalDisplayContent('\x1b]9999;{"state":"working"')).toBe(false)
expect(hasTerminalDisplayContent('\x1b[31')).toBe(false)
})
it('treats styling-only control sequences as metadata-only', () => {
expect(hasTerminalDisplayContent('\x1b[31m\x1b[0m')).toBe(false)
expect(hasTerminalDisplayContent('\x1b[0 q\x1b[?25h')).toBe(false)
})
it('treats printable text and whitespace redraws as display content', () => {
expect(hasTerminalDisplayContent('hello')).toBe(true)
expect(hasTerminalDisplayContent(' ')).toBe(true)
expect(hasTerminalDisplayContent('\r ')).toBe(true)
expect(hasTerminalDisplayContent('\n')).toBe(true)
})
it('treats erase and alternate-screen sequences as display-affecting', () => {
expect(hasTerminalDisplayContent('\x1b[2J\x1b[H')).toBe(true)
expect(hasTerminalDisplayContent('\x1b[?1049h')).toBe(true)
expect(hasTerminalDisplayContent('\x1b#8')).toBe(true)
})
})
describe('trimIncompleteTerminalControlTail', () => {
it('drops trailing partial control frames before replay', () => {
expect(trimIncompleteTerminalControlTail('\x1b]0;partial title')).toBe('')
expect(trimIncompleteTerminalControlTail('visible\x1b]0;partial title')).toBe('visible')
expect(trimIncompleteTerminalControlTail('visible\x1b[31')).toBe('visible')
})
it('keeps complete metadata and display frames intact', () => {
expect(trimIncompleteTerminalControlTail('\x1b]0;Restored title\x07')).toBe(
'\x1b]0;Restored title\x07'
)
expect(trimIncompleteTerminalControlTail('\x1b[31mvisible')).toBe('\x1b[31mvisible')
})
})
@@ -0,0 +1,166 @@
// Why: attach-time clear is only safe before bytes that draw a replacement
// frame; metadata/control-only replay must preserve restored scrollback.
export function hasTerminalDisplayContent(chunk: string): boolean {
for (let index = 0; index < chunk.length; index += 1) {
const code = chunk.charCodeAt(index)
const control = parseTerminalControlSequence(chunk, index)
if (control !== undefined) {
if (control === null) {
return false
}
if (control.affectsDisplay) {
return true
}
index = control.end
continue
}
if (isIgnoredControlCode(code)) {
continue
}
if (isDisplayAffectingControlCode(code) || (code >= 0x20 && code !== 0x7f)) {
return true
}
}
return false
}
export function trimIncompleteTerminalControlTail(chunk: string): string {
for (let index = 0; index < chunk.length; index += 1) {
const control = parseTerminalControlSequence(chunk, index)
if (control === undefined) {
continue
}
if (control === null) {
return chunk.slice(0, index)
}
index = control.end
}
return chunk
}
type ParsedTerminalControlSequence = {
end: number
affectsDisplay: boolean
}
function parseTerminalControlSequence(
value: string,
index: number
): ParsedTerminalControlSequence | null | undefined {
const code = value.charCodeAt(index)
if (code === 0x1b) {
return parseEscControlSequence(value, index)
}
if (code === 0x9b) {
return parseCsiSequence(value, index + 1)
}
if (code === 0x9d) {
return parseStringControlSequence(value, index + 1, { affectsDisplay: false })
}
if (code === 0x90 || code === 0x98 || code === 0x9e || code === 0x9f) {
return parseStringControlSequence(value, index + 1, {
affectsDisplay: false,
belTerminates: false
})
}
return undefined
}
function parseEscControlSequence(
value: string,
escapeIndex: number
): ParsedTerminalControlSequence | null {
const introducer = value[escapeIndex + 1]
if (!introducer) {
return null
}
if (introducer === '[') {
return parseCsiSequence(value, escapeIndex + 2)
}
if (introducer === ']') {
return parseStringControlSequence(value, escapeIndex + 2, { affectsDisplay: false })
}
if (isStTerminatedStringControlIntroducer(introducer)) {
return parseStringControlSequence(value, escapeIndex + 2, {
affectsDisplay: false,
belTerminates: false
})
}
if (introducer === '#') {
return value.length > escapeIndex + 2 ? { end: escapeIndex + 2, affectsDisplay: true } : null
}
if (isEscIntermediateIntroducer(introducer)) {
return value.length > escapeIndex + 2 ? { end: escapeIndex + 2, affectsDisplay: false } : null
}
return { end: escapeIndex + 1, affectsDisplay: true }
}
function parseCsiSequence(value: string, startIndex: number): ParsedTerminalControlSequence | null {
for (let index = startIndex; index < value.length; index += 1) {
const code = value.charCodeAt(index)
if (code >= 0x40 && code <= 0x7e) {
return {
end: index,
affectsDisplay: csiSequenceAffectsDisplay(value.slice(startIndex, index), value[index])
}
}
}
return null
}
function parseStringControlSequence(
value: string,
startIndex: number,
options: { affectsDisplay: boolean; belTerminates?: boolean }
): ParsedTerminalControlSequence | null {
const belTerminates = options.belTerminates !== false
for (let index = startIndex; index < value.length; index += 1) {
if (belTerminates && value[index] === '\u0007') {
return { end: index, affectsDisplay: options.affectsDisplay }
}
if (value[index] === '\u001b' && value[index + 1] === '\\') {
return { end: index + 1, affectsDisplay: options.affectsDisplay }
}
if (value.charCodeAt(index) === 0x9c) {
return { end: index, affectsDisplay: options.affectsDisplay }
}
}
return null
}
function csiSequenceAffectsDisplay(parametersAndIntermediates: string, final: string): boolean {
if (final === 'm') {
return false
}
if (final === 'q' && parametersAndIntermediates.includes(' ')) {
return false
}
if (final === 'h' || final === 'l') {
return csiModeSequenceAffectsDisplay(parametersAndIntermediates)
}
return true
}
function csiModeSequenceAffectsDisplay(parametersAndIntermediates: string): boolean {
const modeNumbers = parametersAndIntermediates.match(/\d+/g) ?? []
return modeNumbers.some((mode) => mode === '47' || mode === '1047' || mode === '1049')
}
function isIgnoredControlCode(code: number): boolean {
return code === 0x7f || code < 0x08 || (code > 0x0d && code < 0x20)
}
function isDisplayAffectingControlCode(code: number): boolean {
return (code >= 0x08 && code <= 0x0d) || code === 0x84 || code === 0x85 || code === 0x8d
}
function isStTerminatedStringControlIntroducer(introducer: string): boolean {
return introducer === 'P' || introducer === 'X' || introducer === '^' || introducer === '_'
}
function isEscIntermediateIntroducer(introducer: string): boolean {
const code = introducer.charCodeAt(0)
return code >= 0x20 && code <= 0x2f
}