fix(terminal): reset kitty mirror on fresh spawn to cover replaced-PTY late exits

The exit handler resets the per-pane kitty keyboard mirror, but a late exit
from a replaced PTY takes the stale-transport early return and skips it —
so a restart-in-place could leak the old TUI's kitty flags into the fresh
shell, kitty-encoding Option chords the shell cannot parse. A fresh spawn
is by definition a new process with kitty state at zero, so startFreshSpawn
now resets the reused tracker itself, alongside the other per-pane mode
state it already clears. Reattach paths are untouched, so a live TUI's
mirrored flags still survive reconnects.
This commit is contained in:
Brennan Benson
2026-07-10 12:23:53 -07:00
parent cd98242b8c
commit 78b6e84917
2 changed files with 31 additions and 0 deletions
@@ -920,6 +920,32 @@ describe('connectPanePty', () => {
)
})
// Why: a late exit from a replaced PTY takes the stale-transport early
// return in onExit and skips the kitty mirror reset there — a fresh spawn
// must therefore reset the reused per-pane tracker itself, or a
// restart-in-place leaks the old TUI's kitty flags into a fresh shell.
it('resets a stale kitty keyboard mirror when spawning a fresh PTY', async () => {
const { connectPanePty } = await import('./pty-connection')
const { TerminalKittyKeyboardModeTracker } =
await import('../../../../shared/terminal-kitty-keyboard-mode-tracker')
const transport = createMockTransport()
transportFactoryQueue.push(transport)
const staleTracker = new TerminalKittyKeyboardModeTracker()
staleTracker.scan('\x1b[>1u')
expect(staleTracker.flags).toBe(1)
// Why: a unique tab id keeps this pane's key clear of pendingSpawnByPaneKey
// entries from other tests, so the connect deterministically fresh-spawns.
const deps = createDeps({
tabId: 'tab-kitty-fresh-spawn',
paneKittyKeyboardModesRef: { current: new Map([[91, staleTracker]]) }
})
connectPanePty(createPane(91) as never, createManager(91) as never, deps as never)
await flushAsyncTicks()
expect(staleTracker.flags).toBe(0)
})
it('threads the resolved local project runtime into IPC terminal transport options', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport()
@@ -3771,6 +3771,11 @@ export function connectPanePty(
): void => {
clearPaneMode2031State()
clearHiddenOutputRestoreState()
// Why: a fresh spawn is a new process with kitty keyboard flags at
// zero. The exit-handler reset alone is not enough: a late exit from a
// replaced PTY takes the stale-transport early return and skips it, so
// a restart-in-place would leak the old TUI's flags into a fresh shell.
kittyKeyboardModes.reset()
prepareFreshShellViewportForSpawn(options)
if (connectionId && startupOverride?.command) {
// Why: SSH providers use `command` only as spawn metadata; the renderer