fix(agent-dashboard): keep the drawer's dismissal Escape out of the focused terminal (#34037)

Closing the Agent Dashboard drawer or workspace board with Escape still
forwarded the key to a focused xterm, because xterm ignores
defaultPrevented. Stop propagation only when the target is the xterm
helper textarea so other fields keep their own Escape handling.
This commit is contained in:
Neil
2026-10-26 11:48:08 -07:00
committed by GitHub
parent c979079fa6
commit caa49c8d52
4 changed files with 51 additions and 0 deletions
@@ -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(<AgentDashboardDrawer statusBarVisible />)
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
@@ -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.
@@ -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 () => {
@@ -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()
}