diff --git a/src/renderer/src/components/native-chat/native-chat-pty-send-queue.ts b/src/renderer/src/components/native-chat/native-chat-pty-send-queue.ts index 2f1862376b3..0d82f8ce309 100644 --- a/src/renderer/src/components/native-chat/native-chat-pty-send-queue.ts +++ b/src/renderer/src/components/native-chat/native-chat-pty-send-queue.ts @@ -64,11 +64,23 @@ export function cancelNativeChatPtySends(ptyId: string): void { /** Wait until every chat sequence on this PTY has finished or been cancelled. */ export async function waitForNativeChatPtyIdle(ptyId: string): Promise { - const state = ptyQueues.get(ptyId) - if (!state) { + while (true) { + const state = ptyQueues.get(ptyId) + if (!state) { + return + } + const tail = state.tail + await tail + const current = ptyQueues.get(ptyId) + if (!current) { + return + } + // A send may append while the captured tail drains; include its replacement. + if (current !== state || current.tail !== tail) { + continue + } return } - await state.tail } /** diff --git a/src/renderer/src/components/native-chat/native-chat-send-settlement.test.ts b/src/renderer/src/components/native-chat/native-chat-send-settlement.test.ts index 71f094bf492..e87d7324d66 100644 --- a/src/renderer/src/components/native-chat/native-chat-send-settlement.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-send-settlement.test.ts @@ -39,4 +39,25 @@ describe('waitForNativeChatSendQueueIdle', () => { await vi.advanceTimersByTimeAsync(1) expect(resolved).toBe(true) }) + + it('waits for sends appended while an earlier successor is draining', async () => { + const revealingSend = enqueueTimedSend(10) + enqueueTimedSend(20) + const queueIdle = waitForNativeChatSendQueueIdle(PTY_ID, revealingSend.settled) + let resolved = false + void queueIdle?.then(() => { + resolved = true + }) + + await vi.advanceTimersByTimeAsync(15) + enqueueTimedSend(30) + await vi.advanceTimersByTimeAsync(15) + expect(resolved).toBe(false) + + await vi.advanceTimersByTimeAsync(29) + expect(resolved).toBe(false) + + await vi.advanceTimersByTimeAsync(1) + expect(resolved).toBe(true) + }) }) diff --git a/src/renderer/src/components/native-chat/use-native-chat-slash-command-dispatched.test.tsx b/src/renderer/src/components/native-chat/use-native-chat-slash-command-dispatched.test.tsx index 63307eeb7df..b2174611b26 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-slash-command-dispatched.test.tsx +++ b/src/renderer/src/components/native-chat/use-native-chat-slash-command-dispatched.test.tsx @@ -84,6 +84,21 @@ describe('useNativeChatSlashCommandDispatched', () => { expect(onSwitchToTerminal).not.toHaveBeenCalled() }) + it('does not reveal after the originating chat view unmounts', async () => { + let settle = (): void => {} + const settled = new Promise((resolve) => { + settle = resolve + }) + const hook = render('claude') + act(() => hook.result.current('/resume', settled)) + + hook.unmount() + settle() + await settled + + expect(onSwitchToTerminal).not.toHaveBeenCalled() + }) + it('records the marker immediately, not on settle', async () => { // The `Ran /x` line is local feedback for a command with no transcript turn; // holding it until the writes finish would lag the user's own action. diff --git a/src/renderer/src/components/native-chat/use-native-chat-slash-command-dispatched.ts b/src/renderer/src/components/native-chat/use-native-chat-slash-command-dispatched.ts index 23a4887ab57..946d467877e 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-slash-command-dispatched.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-slash-command-dispatched.ts @@ -1,4 +1,4 @@ -import { useCallback, type Dispatch, type SetStateAction } from 'react' +import { useCallback, useEffect, useRef, type Dispatch, type SetStateAction } from 'react' import type { AgentType } from '../../../../shared/agent-status-types' import { getVerifiedNativeChatCommands } from '../../../../shared/native-chat-agent-profiles' import { nativeChatSlashCommandOpensAgentPicker } from '../../../../shared/native-chat-slash-commands' @@ -35,6 +35,13 @@ export function useNativeChatSlashCommandDispatched(args: { onSwitchToTerminal?: (agent: AgentType) => void }): (command: string, settled?: Promise, cancelled?: () => boolean) => void { const { agent, commandMarkerScope, setCommandMarkers, onSwitchToTerminal } = args + const mountedRef = useRef(true) + useEffect(() => { + mountedRef.current = true + return () => { + mountedRef.current = false + } + }, []) return useCallback( (command: string, settled?: Promise, cancelled?: () => boolean) => { setCommandMarkers(appendCommandMarkerCache(commandMarkerScope, command)) @@ -42,7 +49,7 @@ export function useNativeChatSlashCommandDispatched(args: { return } const reveal = (): void => { - if (cancelled?.()) { + if (!mountedRef.current || cancelled?.()) { return } onSwitchToTerminal?.(agent)