diff --git a/src/renderer/src/components/terminal-pane/terminal-appearance.test.ts b/src/renderer/src/components/terminal-pane/terminal-appearance.test.ts index ea074546f04..7f26f10a977 100644 --- a/src/renderer/src/components/terminal-pane/terminal-appearance.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-appearance.test.ts @@ -245,6 +245,7 @@ describe('installMode2031Handlers', () => { // on subscribe, never on unsubscribe. const h = setup() try { + // Non-replay path: subscribe then unsubscribe clears state. await writeSync(h.term, '\x1b[?2031h') h.paneLastThemeMode.set(1, 'dark') expect(h.paneMode2031.get(1)).toBe(true) @@ -252,6 +253,18 @@ describe('installMode2031Handlers', () => { await writeSync(h.term, '\x1b[?2031l') expect(h.paneMode2031.has(1)).toBe(false) expect(h.paneLastThemeMode.has(1)).toBe(false) + + // Replay path: resubscribe, then receive `?2031l` during a replay + // window. The `l` handler must still clear — this is the invariant + // promised by the test name. + await writeSync(h.term, '\x1b[?2031h') + h.paneLastThemeMode.set(1, 'dark') + expect(h.paneMode2031.get(1)).toBe(true) + + replayIntoTerminal(h.pane, h.replayingPanesRef, '\x1b[?2031l') + await new Promise((resolve) => h.term.write('', resolve)) + expect(h.paneMode2031.has(1)).toBe(false) + expect(h.paneLastThemeMode.has(1)).toBe(false) } finally { h.dispose() } diff --git a/src/renderer/src/components/terminal-pane/terminal-appearance.ts b/src/renderer/src/components/terminal-pane/terminal-appearance.ts index b68afbd8cc7..56128671bf7 100644 --- a/src/renderer/src/components/terminal-pane/terminal-appearance.ts +++ b/src/renderer/src/components/terminal-pane/terminal-appearance.ts @@ -86,6 +86,10 @@ export function installMode2031Handlers(deps: Mode2031HandlerDeps): IDisposable[ } return false }), + // Why no replay guard on the unsubscribe branch: clearing stale bookkeeping + // is harmless. We only push CSI 997 on subscribe, never on unsubscribe, so + // even if a cold-restore replay carries `?2031l`, this handler just deletes + // map entries that a later real `?2031h` will re-populate normally. deps.parser.registerCsiHandler({ prefix: '?', final: 'l' }, (params) => { if (hasMode2031(params)) { deps.paneMode2031.delete(deps.paneId)