From f689e2c6073e918d2fa1124ab6ddffba0d7d5f58 Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Fri, 4 Sep 2026 18:11:36 -0700 Subject: [PATCH] fix(chat): close covered terminal focus fallbacks --- .../terminal-pane/TerminalPaneSurface.tsx | 3 +- .../terminal-pane/native-chat-covered-pane.ts | 6 ++-- ...e-global-effects-visibility-resume.test.ts | 2 +- .../lib/focus-terminal-tab-surface.test.ts | 32 ++++++++++++++++++- .../src/lib/focus-terminal-tab-surface.ts | 4 ++- 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx b/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx index 6c099e0d934..dc346ce95b4 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx @@ -48,6 +48,7 @@ export function TerminalPaneSurface({ dismissTerminalError, expectedLayoutLeafIdsAttr, expandedPaneId, + effectiveChatViewMode, handleCancelClose, handleConfirmClose, handleContextMenuToggleNativeChat, @@ -117,7 +118,7 @@ export function TerminalPaneSurface({ className="absolute inset-0 min-h-0 min-w-0" data-native-file-drop-target="terminal" data-terminal-tab-id={tabId} - data-terminal-chat-view={activePaneIsChatLeaf ? 'true' : undefined} + data-terminal-chat-view={effectiveChatViewMode && activePaneIsChatLeaf ? 'true' : undefined} data-terminal-layout-leaf-ids={expectedLayoutLeafIdsAttr} data-pane-title-surface={titleUsesLightSurface ? 'light' : 'dark'} style={terminalContainerStyle} diff --git a/src/renderer/src/components/terminal-pane/native-chat-covered-pane.ts b/src/renderer/src/components/terminal-pane/native-chat-covered-pane.ts index f40731f383d..f7d6534a70a 100644 --- a/src/renderer/src/components/terminal-pane/native-chat-covered-pane.ts +++ b/src/renderer/src/components/terminal-pane/native-chat-covered-pane.ts @@ -1,18 +1,18 @@ import type { PaneManager } from '@/lib/pane-manager/pane-manager' -const NATIVE_CHAT_ROOT_SELECTOR = '[data-native-chat-root]' +const NATIVE_CHAT_COVER_SELECTOR = '.native-chat-pane-shell' /** * Leaf container selector that excludes panes whose xterm sits under the native * chat portal. Chat mode is a tab flag, but only the chat leaf's xterm is * covered — a split terminal leaf in the same tab must still take focus. */ -export const UNCOVERED_TERMINAL_LEAF_SELECTOR = `[data-leaf-id]:not(:has(${NATIVE_CHAT_ROOT_SELECTOR}))` +export const UNCOVERED_TERMINAL_LEAF_SELECTOR = `[data-leaf-id]:not(:has(${NATIVE_CHAT_COVER_SELECTOR}))` export function paneIsCoveredByNativeChat( pane: { container: Pick } | null | undefined ): boolean { - return pane?.container.querySelector(NATIVE_CHAT_ROOT_SELECTOR) != null + return pane?.container.querySelector(NATIVE_CHAT_COVER_SELECTOR) != null } /** Mirrors focusActivePane's target so the guard tracks exactly the pane that would take focus. */ diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-visibility-resume.test.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-visibility-resume.test.ts index 5af0f21815b..cfb4fc1f95d 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-visibility-resume.test.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-visibility-resume.test.ts @@ -356,7 +356,7 @@ describe('useTerminalPaneGlobalEffects', () => { isChatViewMode: true }) - expect(pane.container.querySelector).toHaveBeenCalledWith('[data-native-chat-root]') + expect(pane.container.querySelector).toHaveBeenCalledWith('.native-chat-pane-shell') if (covered) { expect(mocks.focusActivePane).not.toHaveBeenCalled() } else { diff --git a/src/renderer/src/lib/focus-terminal-tab-surface.test.ts b/src/renderer/src/lib/focus-terminal-tab-surface.test.ts index c8c6b63c3ba..20cf491088e 100644 --- a/src/renderer/src/lib/focus-terminal-tab-surface.test.ts +++ b/src/renderer/src/lib/focus-terminal-tab-surface.test.ts @@ -11,7 +11,9 @@ vi.mock('@/components/terminal-pane/terminal-ime-input-context-refresh', () => ( // Why: tab-wide queries skip leaves whose xterm sits under the native chat portal. const TAB_HELPER_SELECTOR = - '[data-terminal-tab-id="tab-1"] [data-leaf-id]:not(:has([data-native-chat-root])) .xterm-helper-textarea' + '[data-terminal-tab-id="tab-1"] [data-leaf-id]:not(:has(.native-chat-pane-shell)) .xterm-helper-textarea' +const GLOBAL_HELPER_SELECTOR = + '[data-leaf-id]:not(:has(.native-chat-pane-shell)) .xterm-helper-textarea' describe('focusTerminalTabSurface', () => { afterEach(() => { @@ -145,6 +147,34 @@ describe('focusTerminalTabSurface', () => { expect(coveredTextarea.focus).not.toHaveBeenCalled() }) + it('does not use a covered chat helper as the global mount-race fallback', () => { + flushAnimationFrames() + const coveredTextarea = { focus: vi.fn() } + vi.stubGlobal('document', { + querySelector: vi.fn((selector: string) => + selector === '.xterm-helper-textarea' ? coveredTextarea : null + ) + }) + + focusTerminalTabSurface('tab-1') + + expect(coveredTextarea.focus).not.toHaveBeenCalled() + }) + + it('keeps the global mount-race fallback for an uncovered terminal helper', () => { + flushAnimationFrames() + const textarea = { focus: vi.fn() } + vi.stubGlobal('document', { + querySelector: vi.fn((selector: string) => + selector === GLOBAL_HELPER_SELECTOR ? textarea : null + ) + }) + + focusTerminalTabSurface('tab-1') + + expect(textarea.focus).toHaveBeenCalledOnce() + }) + it('falls back to the single tab helper when an old leaf id was reminted', () => { flushAnimationFrames() const textarea = { focus: vi.fn() } diff --git a/src/renderer/src/lib/focus-terminal-tab-surface.ts b/src/renderer/src/lib/focus-terminal-tab-surface.ts index 4a54c76523d..ef23ff051d7 100644 --- a/src/renderer/src/lib/focus-terminal-tab-surface.ts +++ b/src/renderer/src/lib/focus-terminal-tab-surface.ts @@ -105,7 +105,9 @@ export function focusTerminalTabSurface( } return } - const fallback = document.querySelector('.xterm-helper-textarea') as HTMLElement | null + const fallback = document.querySelector( + `${UNCOVERED_TERMINAL_LEAF_SELECTOR} .xterm-helper-textarea` + ) as HTMLElement | null if (fallback) { focusTerminalHelper(fallback, options) }