Fix Vim alternate-screen terminal redraw recovery (#7142)
* Fix Vim alternate-screen terminal redraw recovery * Decide alternate-screen atlas recovery from parsed buffer state Replace the raw-chunk scan for alternate-screen enter sequences with the xterm parser's own verdict: in-place rewrite chunks check buffer.active.type (plus a parse-time buffer-switch count) inside the write callback, where the buffer is authoritative. Covers enter/exit sequences split across PTY chunk boundaries and full enter-exit cycles coalesced into one write, and deletes the hand-rolled CSI parser. Adds regression tests from a real captured vim session (including a CSI sequence split at a 1024-byte PTY read boundary) and a contract pin against the real @xterm/headless parser. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
f7ec1c727e
commit
4c4cbf2d76
|
|
@ -9192,6 +9192,308 @@ describe('connectPanePty', () => {
|
|||
expect(scheduleTerminalWebglAtlasRecovery).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('schedules WebGL atlas recovery for Vim-style foreground alternate-screen redraws', async () => {
|
||||
const restoreNavigator = temporarilySetNavigatorUserAgent('Mozilla/5.0 (Macintosh)')
|
||||
try {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport()
|
||||
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const pane = createPane(1)
|
||||
;(pane.terminal.buffer.active as { type: 'normal' | 'alternate' }).type = 'alternate'
|
||||
const refresh = vi.fn()
|
||||
let parseCallback: (() => void) | undefined
|
||||
const terminal = pane.terminal as typeof pane.terminal & {
|
||||
_core?: { refresh: typeof refresh }
|
||||
}
|
||||
terminal._core = { refresh }
|
||||
terminal.write = vi.fn((_data: string, callback?: () => void) => {
|
||||
parseCallback = callback
|
||||
})
|
||||
|
||||
connectPanePty(pane as never, createManager(1) as never, createDeps() as never)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
capturedDataCallback.current?.(
|
||||
'\x1b[2J\x1b[H{"name":"eepo"}\r\n\x1b[2;1H{"name":"expo"}\x1b[K'
|
||||
)
|
||||
|
||||
expect(scheduleTerminalWebglAtlasRecovery).not.toHaveBeenCalled()
|
||||
parseCallback?.()
|
||||
expect(refresh).toHaveBeenCalledWith(0, 39, true)
|
||||
expect(scheduleTerminalWebglAtlasRecovery).toHaveBeenCalledTimes(1)
|
||||
} finally {
|
||||
restoreNavigator()
|
||||
}
|
||||
})
|
||||
|
||||
it('schedules WebGL atlas recovery when a foreground rewrite enters alternate screen', async () => {
|
||||
const restoreNavigator = temporarilySetNavigatorUserAgent('Mozilla/5.0 (Macintosh)')
|
||||
try {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport()
|
||||
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const pane = createPane(1)
|
||||
const refresh = vi.fn()
|
||||
let parseCallback: (() => void) | undefined
|
||||
const terminal = pane.terminal as typeof pane.terminal & {
|
||||
_core?: { refresh: typeof refresh }
|
||||
}
|
||||
terminal._core = { refresh }
|
||||
terminal.write = vi.fn((_data: string, callback?: () => void) => {
|
||||
parseCallback = callback
|
||||
})
|
||||
|
||||
connectPanePty(pane as never, createManager(1) as never, createDeps() as never)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
capturedDataCallback.current?.('\x1b[?1049h\x1b[2J\x1b[HVim package.json')
|
||||
|
||||
expect(scheduleTerminalWebglAtlasRecovery).not.toHaveBeenCalled()
|
||||
// Why: xterm switches to the alternate buffer while parsing the chunk;
|
||||
// the write callback observes the post-parse buffer state.
|
||||
;(pane.terminal.buffer.active as { type: 'normal' | 'alternate' }).type = 'alternate'
|
||||
parseCallback?.()
|
||||
expect(refresh).toHaveBeenCalledWith(0, 39, true)
|
||||
expect(scheduleTerminalWebglAtlasRecovery).toHaveBeenCalledTimes(1)
|
||||
} finally {
|
||||
restoreNavigator()
|
||||
}
|
||||
})
|
||||
|
||||
it('schedules WebGL atlas recovery when the alternate-screen enter sequence splits across chunks', async () => {
|
||||
const restoreNavigator = temporarilySetNavigatorUserAgent('Mozilla/5.0 (Macintosh)')
|
||||
try {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport()
|
||||
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const pane = createPane(1)
|
||||
const refresh = vi.fn()
|
||||
let parseCallback: (() => void) | undefined
|
||||
const terminal = pane.terminal as typeof pane.terminal & {
|
||||
_core?: { refresh: typeof refresh }
|
||||
}
|
||||
terminal._core = { refresh }
|
||||
terminal.write = vi.fn((_data: string, callback?: () => void) => {
|
||||
parseCallback = callback
|
||||
})
|
||||
|
||||
connectPanePty(pane as never, createManager(1) as never, createDeps() as never)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
// Why: PTY reads split CSI sequences at arbitrary byte boundaries (real
|
||||
// vim sessions split cursor moves at 1024-byte chunk edges), so the
|
||||
// enter sequence itself can straddle two onData chunks.
|
||||
capturedDataCallback.current?.('\x1b[?104')
|
||||
parseCallback?.()
|
||||
expect(scheduleTerminalWebglAtlasRecovery).not.toHaveBeenCalled()
|
||||
|
||||
capturedDataCallback.current?.('9h\x1b[2J\x1b[H~\x1b[K')
|
||||
;(pane.terminal.buffer.active as { type: 'normal' | 'alternate' }).type = 'alternate'
|
||||
parseCallback?.()
|
||||
expect(scheduleTerminalWebglAtlasRecovery).toHaveBeenCalledTimes(1)
|
||||
} finally {
|
||||
restoreNavigator()
|
||||
}
|
||||
})
|
||||
|
||||
it('schedules WebGL atlas recovery when a foreground rewrite leaves alternate screen', async () => {
|
||||
const restoreNavigator = temporarilySetNavigatorUserAgent('Mozilla/5.0 (Macintosh)')
|
||||
try {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport()
|
||||
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const pane = createPane(1)
|
||||
;(pane.terminal.buffer.active as { type: 'normal' | 'alternate' }).type = 'alternate'
|
||||
const refresh = vi.fn()
|
||||
let parseCallback: (() => void) | undefined
|
||||
const terminal = pane.terminal as typeof pane.terminal & {
|
||||
_core?: { refresh: typeof refresh }
|
||||
}
|
||||
terminal._core = { refresh }
|
||||
terminal.write = vi.fn((_data: string, callback?: () => void) => {
|
||||
parseCallback = callback
|
||||
})
|
||||
|
||||
connectPanePty(pane as never, createManager(1) as never, createDeps() as never)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
// Vim's final frame erases the status line and restores the normal
|
||||
// buffer in one chunk; the atlas must still rebuild for the restored
|
||||
// prompt even though the post-parse buffer is back to normal.
|
||||
capturedDataCallback.current?.('\x1b[34;1H\x1b[K\x1b[34;1H\x1b[?1049l\x1b[?25h')
|
||||
;(pane.terminal.buffer.active as { type: 'normal' | 'alternate' }).type = 'normal'
|
||||
parseCallback?.()
|
||||
expect(scheduleTerminalWebglAtlasRecovery).toHaveBeenCalledTimes(1)
|
||||
} finally {
|
||||
restoreNavigator()
|
||||
}
|
||||
})
|
||||
|
||||
it('schedules WebGL atlas recovery when one chunk enters and exits alternate screen', async () => {
|
||||
const restoreNavigator = temporarilySetNavigatorUserAgent('Mozilla/5.0 (Macintosh)')
|
||||
try {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport()
|
||||
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const pane = createPane(1)
|
||||
let bufferChangeListener: (() => void) | undefined
|
||||
;(
|
||||
pane.terminal.buffer as {
|
||||
onBufferChange?: (listener: () => void) => { dispose: () => void }
|
||||
}
|
||||
).onBufferChange = (listener) => {
|
||||
bufferChangeListener = listener
|
||||
return { dispose: vi.fn() }
|
||||
}
|
||||
const refresh = vi.fn()
|
||||
let parseCallback: (() => void) | undefined
|
||||
const terminal = pane.terminal as typeof pane.terminal & {
|
||||
_core?: { refresh: typeof refresh }
|
||||
}
|
||||
terminal._core = { refresh }
|
||||
terminal.write = vi.fn((_data: string, callback?: () => void) => {
|
||||
parseCallback = callback
|
||||
})
|
||||
|
||||
connectPanePty(pane as never, createManager(1) as never, createDeps() as never)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
// A coalesced backlog flush can parse a whole enter -> draw -> exit TUI
|
||||
// interaction in one write, netting buffer type back to 'normal'; the
|
||||
// buffer-switch count is what still marks it as alternate-screen work.
|
||||
capturedDataCallback.current?.('\x1b[?1049h\x1b[2J\x1b[Hpager frame\x1b[K\x1b[?1049l')
|
||||
bufferChangeListener?.()
|
||||
bufferChangeListener?.()
|
||||
parseCallback?.()
|
||||
expect(scheduleTerminalWebglAtlasRecovery).toHaveBeenCalledTimes(1)
|
||||
} finally {
|
||||
restoreNavigator()
|
||||
}
|
||||
})
|
||||
|
||||
it('schedules WebGL atlas recovery for real captured Vim redraw chunks split mid-sequence', async () => {
|
||||
const restoreNavigator = temporarilySetNavigatorUserAgent('Mozilla/5.0 (Macintosh)')
|
||||
try {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport()
|
||||
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const pane = createPane(1)
|
||||
;(pane.terminal.buffer.active as { type: 'normal' | 'alternate' }).type = 'alternate'
|
||||
const refresh = vi.fn()
|
||||
let parseCallback: (() => void) | undefined
|
||||
const terminal = pane.terminal as typeof pane.terminal & {
|
||||
_core?: { refresh: typeof refresh }
|
||||
}
|
||||
terminal._core = { refresh }
|
||||
terminal.write = vi.fn((_data: string, callback?: () => void) => {
|
||||
parseCallback = callback
|
||||
})
|
||||
|
||||
connectPanePty(pane as never, createManager(1) as never, createDeps() as never)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
// Captured from a real `vim package.json` session: a 1024-byte PTY read
|
||||
// boundary cuts the cursor move \x1b[30;5H into "\x1b[30" + ";5H".
|
||||
capturedDataCallback.current?.('"rules": {\x1b[29;15H\x1b[K\x1b[30')
|
||||
parseCallback?.()
|
||||
expect(scheduleTerminalWebglAtlasRecovery).toHaveBeenCalledTimes(1)
|
||||
|
||||
capturedDataCallback.current?.(
|
||||
';5H "js-combine-iterations": "off"\r\n }\x1b[31;6H\x1b[K\x1b[33;1H\x1b[?25h'
|
||||
)
|
||||
parseCallback?.()
|
||||
expect(scheduleTerminalWebglAtlasRecovery).toHaveBeenCalledTimes(2)
|
||||
} finally {
|
||||
restoreNavigator()
|
||||
}
|
||||
})
|
||||
|
||||
it('does not schedule WebGL atlas recovery for ordinary foreground shell rewrites', async () => {
|
||||
const restoreNavigator = temporarilySetNavigatorUserAgent('Mozilla/5.0 (Macintosh)')
|
||||
try {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport()
|
||||
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const pane = createPane(1)
|
||||
const refresh = vi.fn()
|
||||
let parseCallback: (() => void) | undefined
|
||||
const terminal = pane.terminal as typeof pane.terminal & {
|
||||
_core?: { refresh: typeof refresh }
|
||||
}
|
||||
terminal._core = { refresh }
|
||||
terminal.write = vi.fn((_data: string, callback?: () => void) => {
|
||||
parseCallback = callback
|
||||
})
|
||||
|
||||
connectPanePty(pane as never, createManager(1) as never, createDeps() as never)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
capturedDataCallback.current?.('\r\x1b[Korca % npm test')
|
||||
|
||||
parseCallback?.()
|
||||
expect(refresh).toHaveBeenCalledWith(0, 39, true)
|
||||
expect(scheduleTerminalWebglAtlasRecovery).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
restoreNavigator()
|
||||
}
|
||||
})
|
||||
|
||||
it('does not schedule WebGL atlas recovery for plain synchronized foreground frames', async () => {
|
||||
const restoreNavigator = temporarilySetNavigatorUserAgent('Mozilla/5.0 (Macintosh)')
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -2550,6 +2550,14 @@ export function connectPanePty(
|
|||
forwardPtyResize(cols, rows)
|
||||
})
|
||||
|
||||
// Why: a rewrite chunk can enter AND exit the alternate screen in one parse
|
||||
// (fast-quitting TUI), netting buffer.active.type back to 'normal'; counting
|
||||
// switches keeps those redraws visible to the atlas-recovery check.
|
||||
let alternateScreenBufferSwitches = 0
|
||||
const onBufferChangeDisposable = pane.terminal.buffer.onBufferChange?.(() => {
|
||||
alternateScreenBufferSwitches += 1
|
||||
})
|
||||
|
||||
// Why: renderer resize forwarding is fire-and-forget. A visible pane can
|
||||
// finish with xterm at the right grid while the PTY silently kept an older
|
||||
// grid, so Codex keeps composing against stale columns. Fit first so xterm's
|
||||
|
|
@ -3777,6 +3785,25 @@ export function connectPanePty(
|
|||
return decision.prefersRenderRefresh
|
||||
}
|
||||
|
||||
// Why: Vim-style TUI redraws are plain-ASCII in-place rewrites whose erased
|
||||
// cells can keep stale WebGL glyphs until the shared atlas rebuilds. Whether
|
||||
// a rewrite touched the alternate screen is only authoritative once xterm
|
||||
// parses the chunk (enter/exit sequences can split across PTY chunks), so
|
||||
// capture the pre-parse state and decide the rest at parse completion.
|
||||
function alternateScreenRewriteAtlasRecoveryOnParsed(): () => void {
|
||||
const wasAlternateScreenBuffer = pane.terminal.buffer.active.type === 'alternate'
|
||||
const switchesBeforeParse = alternateScreenBufferSwitches
|
||||
return () => {
|
||||
if (
|
||||
wasAlternateScreenBuffer ||
|
||||
alternateScreenBufferSwitches !== switchesBeforeParse ||
|
||||
pane.terminal.buffer.active.type === 'alternate'
|
||||
) {
|
||||
scheduleTerminalWebglAtlasRecovery()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function shouldForceForegroundRenderRefresh(data: string): {
|
||||
refresh: boolean
|
||||
inPlaceRewrite: boolean
|
||||
|
|
@ -3864,6 +3891,13 @@ export function connectPanePty(
|
|||
!foregroundOutput && hiddenOutputNeedsAtlasRecoveryAfterParse(data)
|
||||
const recoverWebglAtlasAfterParse =
|
||||
renderRefreshDecision.recoverWebglAtlasAfterParse || recoverHiddenWebglAtlasAfterParse
|
||||
// Why: atlas recovery must repaint from the parsed xterm buffer, not a
|
||||
// pre-write snapshot that a late TUI redraw can immediately stale.
|
||||
const onParsedAtlasRecovery = recoverWebglAtlasAfterParse
|
||||
? scheduleTerminalWebglAtlasRecovery
|
||||
: renderRefreshDecision.inPlaceRewrite
|
||||
? alternateScreenRewriteAtlasRecoveryOnParsed()
|
||||
: undefined
|
||||
const foregroundRenderRefreshNeeded = renderRefreshDecision.refresh
|
||||
// Why: see nativeWindowsRewriteNeedsFollowupRenderRefresh — Claude Code's
|
||||
// in-place prompt redraws on Windows ConPTY can paint one frame late, so a
|
||||
|
|
@ -3910,9 +3944,7 @@ export function connectPanePty(
|
|||
foregroundRenderRefreshNeeded),
|
||||
followupForegroundRefresh:
|
||||
nativeWindowsCursorRestore || nativeWindowsInPlaceRewriteFollowup,
|
||||
// Why: atlas recovery must repaint from the parsed xterm buffer, not
|
||||
// a pre-write snapshot that a late TUI redraw can immediately stale.
|
||||
onParsed: recoverWebglAtlasAfterParse ? scheduleTerminalWebglAtlasRecovery : undefined,
|
||||
onParsed: onParsedAtlasRecovery,
|
||||
stripTransientCursorShows: shouldProtectNativeWindowsSynchronizedOutput && foreground,
|
||||
coalesceForeground: synchronizedForegroundOutput && synchronizedOutputEnded,
|
||||
holdForeground: synchronizedForegroundOutput && nextSynchronizedForegroundOutputActive
|
||||
|
|
@ -5639,6 +5671,7 @@ export function connectPanePty(
|
|||
onDataDisposable.dispose()
|
||||
terminalCapabilityRepliesDisposable.dispose()
|
||||
onResizeDisposable.dispose()
|
||||
onBufferChangeDisposable?.dispose()
|
||||
pane.container.removeEventListener(PANE_PTY_RESIZE_HOLD_FLUSH_EVENT, onHeldPtyResizeFlush)
|
||||
geometryReportObserver?.disconnect()
|
||||
if (pendingGeometryReportRaf !== null) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { Terminal } from '@xterm/headless'
|
||||
|
||||
function writeChunk(term: Terminal, data: string): Promise<void> {
|
||||
return new Promise((resolve) => term.write(data, resolve))
|
||||
}
|
||||
|
||||
// Pins the xterm contract the alternate-screen atlas recovery relies on: by the
|
||||
// time a chunk's write callback runs, buffer.active.type reflects any
|
||||
// alternate-screen enter/exit parsed from that chunk — even when the sequence
|
||||
// splits across PTY chunk boundaries.
|
||||
describe('alternate-screen buffer state at write-callback time', () => {
|
||||
it('reflects an enter sequence split across two chunks', async () => {
|
||||
const term = new Terminal({ cols: 120, rows: 34, allowProposedApi: true })
|
||||
await writeChunk(term, '\x1b[?104')
|
||||
expect(term.buffer.active.type).toBe('normal')
|
||||
await writeChunk(term, '9h\x1b[2J\x1b[H~\x1b[K')
|
||||
expect(term.buffer.active.type).toBe('alternate')
|
||||
term.dispose()
|
||||
})
|
||||
|
||||
it('fires onBufferChange for each switch when one chunk enters and exits', async () => {
|
||||
const term = new Terminal({ cols: 120, rows: 34, allowProposedApi: true })
|
||||
let switches = 0
|
||||
const disposable = term.buffer.onBufferChange(() => {
|
||||
switches += 1
|
||||
})
|
||||
await writeChunk(term, '\x1b[?1049h\x1b[2J\x1b[Hpager frame\x1b[K\x1b[?1049l')
|
||||
expect(term.buffer.active.type).toBe('normal')
|
||||
expect(switches).toBe(2)
|
||||
disposable.dispose()
|
||||
term.dispose()
|
||||
})
|
||||
|
||||
it('tracks enter, split redraw, and exit from a real captured vim session', async () => {
|
||||
const term = new Terminal({ cols: 120, rows: 34, allowProposedApi: true })
|
||||
// Captured from `vim package.json` (macOS, TERM=xterm-256color): startup chunk.
|
||||
await writeChunk(
|
||||
term,
|
||||
'\x1b[?1049h\x1b[>4;2m\x1b[?1h\x1b=\x1b[?2004h\x1b[?1004h\x1b[1;34r\x1b[?12h\x1b[?12l\x1b[22;2t\x1b[22;1t'
|
||||
)
|
||||
expect(term.buffer.active.type).toBe('alternate')
|
||||
// Mid-session redraw where a 1024-byte PTY read split \x1b[30;5H in two.
|
||||
await writeChunk(term, '"rules": {\x1b[29;15H\x1b[K\x1b[30')
|
||||
await writeChunk(
|
||||
term,
|
||||
';5H "js-combine-iterations": "off"\r\n }\x1b[31;6H\x1b[K\x1b[33;1H\x1b[?25h'
|
||||
)
|
||||
expect(term.buffer.active.type).toBe('alternate')
|
||||
// Vim quit: erase the status line and restore the normal buffer in one chunk.
|
||||
await writeChunk(
|
||||
term,
|
||||
'\x1b[23;2t\x1b[23;1t\x1b[34;1H\x1b[K\x1b[34;1H\x1b[?1004l\x1b[?2004l\x1b[?1l\x1b>\x1b[?1049l\x1b[?25h\x1b[>4;m'
|
||||
)
|
||||
expect(term.buffer.active.type).toBe('normal')
|
||||
term.dispose()
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue