diff --git a/src/renderer/src/components/dashboard/AgentDashboardDrawer.test.tsx b/src/renderer/src/components/dashboard/AgentDashboardDrawer.test.tsx index ee141195843..786c8195b0d 100644 --- a/src/renderer/src/components/dashboard/AgentDashboardDrawer.test.tsx +++ b/src/renderer/src/components/dashboard/AgentDashboardDrawer.test.tsx @@ -72,6 +72,7 @@ beforeEach(() => { afterEach(() => { cleanup() + document.body.replaceChildren() useAppStore.setState(initialState, true) }) @@ -101,6 +102,28 @@ describe('AgentDashboardDrawer', () => { expect(useAppStore.getState().agentDashboardDrawerOpen).toBe(false) }) + it('keeps the dismissal Escape from a focused terminal but not from other fields', () => { + const terminalInput = document.createElement('textarea') + terminalInput.className = 'xterm-helper-textarea' + const field = document.createElement('input') + document.body.append(terminalInput, field) + const terminalKeyDown = vi.fn() + const fieldKeyDown = vi.fn() + terminalInput.addEventListener('keydown', terminalKeyDown) + field.addEventListener('keydown', fieldKeyDown) + render() + + act(() => useAppStore.setState({ agentDashboardDrawerOpen: true })) + fireEvent.keyDown(terminalInput, { key: 'Escape' }) + expect(useAppStore.getState().agentDashboardDrawerOpen).toBe(false) + expect(terminalKeyDown).not.toHaveBeenCalled() + + act(() => useAppStore.setState({ agentDashboardDrawerOpen: true })) + fireEvent.keyDown(field, { key: 'Escape' }) + expect(useAppStore.getState().agentDashboardDrawerOpen).toBe(false) + expect(fieldKeyDown).toHaveBeenCalledTimes(1) + }) + type RevealAgent = (args: { repoId: string worktreeId: string diff --git a/src/renderer/src/components/dashboard/AgentDashboardDrawer.tsx b/src/renderer/src/components/dashboard/AgentDashboardDrawer.tsx index 2b71fdcf7b1..7e4a744d8b4 100644 --- a/src/renderer/src/components/dashboard/AgentDashboardDrawer.tsx +++ b/src/renderer/src/components/dashboard/AgentDashboardDrawer.tsx @@ -4,6 +4,7 @@ import type { AgentSubjectReadIntent } from '@/attention/agent-subject-read-acti import { Sheet, SheetContent, SheetTitle } from '@/components/ui/sheet' import { revealDashboardAgent } from './reveal-dashboard-agent' import { AgentKanbanBoard } from '../dashboard-popout/AgentKanbanBoard' +import { isXtermHelperTextarea } from '../terminal-pane/regular-terminal-focus-ownership' import type { AgentRevealArgs } from '../dashboard-popout/AgentTerminalDialog' import { isWorkspaceBoardKeepOpenTarget, @@ -132,6 +133,10 @@ export function AgentDashboardDrawer({ return } event.preventDefault() + // xterm ignores defaultPrevented; stop only its Escape so other targets keep theirs (rename/drag cancel). + if (isXtermHelperTextarea(event.target)) { + event.stopPropagation() + } close() } // The non-modal drawer must hear Escape even when focus is outside it. diff --git a/src/renderer/src/components/sidebar/useWorkspaceBoardPanel.test.tsx b/src/renderer/src/components/sidebar/useWorkspaceBoardPanel.test.tsx index fbbf6effcd8..4cba42d0499 100644 --- a/src/renderer/src/components/sidebar/useWorkspaceBoardPanel.test.tsx +++ b/src/renderer/src/components/sidebar/useWorkspaceBoardPanel.test.tsx @@ -223,11 +223,29 @@ describe('useWorkspaceBoardPanel', () => { it('still closes the board on Escape from a text field outside it', async () => { await renderHookProbe() const field = appendInput('app') + const fieldKeyDown = vi.fn() + field.addEventListener('keydown', fieldKeyDown) await updatePanel((state) => state.openWorkspaceBoard()) await pressEscape(field) expect(panelState().workspaceBoardOpen).toBe(false) + expect(fieldKeyDown).toHaveBeenCalledTimes(1) + }) + + it('keeps the dismissal Escape from reaching a focused terminal', async () => { + await renderHookProbe() + const terminalInput = document.createElement('textarea') + terminalInput.className = 'xterm-helper-textarea' + document.body.appendChild(terminalInput) + const terminalKeyDown = vi.fn() + terminalInput.addEventListener('keydown', terminalKeyDown) + + await updatePanel((state) => state.openWorkspaceBoard()) + await pressEscape(terminalInput) + + expect(panelState().workspaceBoardOpen).toBe(false) + expect(terminalKeyDown).not.toHaveBeenCalled() }) it('keeps the board open on Escape while a nested dialog is open', async () => { diff --git a/src/renderer/src/components/sidebar/useWorkspaceBoardPanel.ts b/src/renderer/src/components/sidebar/useWorkspaceBoardPanel.ts index 0e23891a406..5a8c74f759e 100644 --- a/src/renderer/src/components/sidebar/useWorkspaceBoardPanel.ts +++ b/src/renderer/src/components/sidebar/useWorkspaceBoardPanel.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { isEditableTarget } from '@/lib/editable-target' import { useAppStore } from '@/store' +import { isXtermHelperTextarea } from '../terminal-pane/regular-terminal-focus-ownership' const WORKSPACE_BOARD_SHEET_SELECTOR = '[data-workspace-board-sheet]' @@ -143,6 +144,10 @@ export function useWorkspaceBoardPanel(): WorkspaceBoardPanelState { return } event.preventDefault() + // xterm ignores defaultPrevented; stop only its Escape so other targets keep theirs (rename/drag cancel). + if (isXtermHelperTextarea(event.target)) { + event.stopPropagation() + } closeWorkspaceBoard() }