fix(terminal): scope mode 2031 retries to subscription

This commit is contained in:
Neil
2026-07-10 21:52:26 -07:00
parent 4df9457530
commit 5c911d91bf
3 changed files with 32 additions and 2 deletions
@@ -17,6 +17,7 @@ describe('pushMode2031SeedReply', () => {
const sendInputImmediate = vi.fn<(data: string) => boolean>(() => true)
const scheduled: (() => void)[] = []
const recordMode = vi.fn()
const currentAttempt = { current: Symbol() }
const transport = {
isConnected: () => connectedState.current,
sendInput,
@@ -29,15 +30,19 @@ describe('pushMode2031SeedReply', () => {
sendInputImmediate,
scheduled,
recordMode,
push: () =>
push: () => {
const attempt = Symbol()
currentAttempt.current = attempt
pushMode2031SeedReply(1, {
hasPane: () => true,
isSubscribed: () => subscribed.current,
isCurrentAttempt: () => currentAttempt.current === attempt,
getTransport: () => transport,
getMode: () => 'dark',
recordMode,
schedule: (callback) => scheduled.push(callback)
})
}
}
}
@@ -65,4 +70,21 @@ describe('pushMode2031SeedReply', () => {
expect(harness.sendInput).not.toHaveBeenCalled()
expect(harness.scheduled).toHaveLength(0)
})
it('does not let an old retry answer a later subscription', () => {
const harness = createHarness(false)
harness.push()
harness.subscribed.current = false
harness.subscribed.current = true
harness.push()
expect(harness.scheduled).toHaveLength(2)
harness.connected.current = true
harness.scheduled.shift()?.()
harness.scheduled.shift()?.()
expect(harness.sendInputImmediate).toHaveBeenCalledTimes(1)
expect(harness.recordMode).toHaveBeenCalledTimes(1)
})
})
@@ -10,6 +10,7 @@ type Mode2031ReplyTransport = Pick<PtyTransport, 'isConnected' | 'sendInputImmed
type Mode2031SeedReplyDeps = {
hasPane: (paneId: number) => boolean
isSubscribed: (paneId: number) => boolean
isCurrentAttempt: (paneId: number) => boolean
getTransport: (paneId: number) => Mode2031ReplyTransport | undefined
getMode: () => TerminalColorSchemeMode | null
recordMode: (paneId: number, mode: TerminalColorSchemeMode) => void
@@ -30,7 +31,7 @@ export function pushMode2031SeedReply(paneId: number, deps: Mode2031SeedReplyDep
const send = (): void => {
// Why: a TUI can unsubscribe while the PTY is connecting; every delayed
// attempt must revalidate intent or its color reply can land at a shell prompt.
if (!deps.hasPane(paneId) || !deps.isSubscribed(paneId)) {
if (!deps.hasPane(paneId) || !deps.isSubscribed(paneId) || !deps.isCurrentAttempt(paneId)) {
return
}
const transport = deps.getTransport(paneId)
@@ -575,6 +575,7 @@ export function useTerminalPaneLifecycle({
const selectionDisposablesRef = useRef(new Map<number, IDisposable>())
const selectionCaptureTimersRef = useRef(new Map<number, number>())
const mode2031DisposablesRef = useRef(new Map<number, IDisposable[]>())
const mode2031SeedAttemptTokensRef = useRef(new Map<number, symbol>())
const osc52DisposablesRef = useRef(new Map<number, IDisposable>())
const osc7DisposablesRef = useRef(new Map<number, IDisposable>())
const mouseHideDisposablesRef = useRef(new Map<number, IDisposable>())
@@ -601,10 +602,15 @@ export function useTerminalPaneLifecycle({
}
const pushMode2031ForPane = (paneId: number): void => {
const attemptToken = Symbol()
mode2031SeedAttemptTokensRef.current.set(paneId, attemptToken)
pushMode2031SeedReply(paneId, {
hasPane: (candidateId) =>
managerRef.current?.getPanes().some((pane) => pane.id === candidateId) === true,
isSubscribed: (candidateId) => paneMode2031Ref.current.get(candidateId) === true,
// Why: an older connect retry must not answer a later resubscription.
isCurrentAttempt: (candidateId) =>
mode2031SeedAttemptTokensRef.current.get(candidateId) === attemptToken,
getTransport: (candidateId) => paneTransportsRef.current.get(candidateId),
getMode: () => {
const currentSettings = settingsRef.current
@@ -1222,6 +1228,7 @@ export function useTerminalPaneLifecycle({
mode2031DisposablesRef.current.delete(paneId)
}
paneMode2031Ref.current.delete(paneId)
mode2031SeedAttemptTokensRef.current.delete(paneId)
paneKittyKeyboardModesRef.current.delete(paneId)
paneLastThemeModeRef.current.delete(paneId)
const osc52Disposable = osc52DisposablesRef.current.get(paneId)