diff --git a/config/fork-ownership.json b/config/fork-ownership.json index 4f892278839a..9ef2f227ca90 100644 --- a/config/fork-ownership.json +++ b/config/fork-ownership.json @@ -5308,6 +5308,14 @@ "feature": "fork-infra", "kind": "passthrough", "lines": ["const PRE_CHANGE_REF = 'v1.4.216-rc.0.zy01'"] + }, + { + "path": "src/renderer/src/components/terminal-pane/terminal-pane-paste-listeners.ts", + "feature": "terminal-dock", + "kind": "import-swap", + "lines": [ + "import { isTerminalPasteOwnedByChat as isInsideNativeChatCover } from './fork-terminal-dock/terminal-dock-paste-yield'" + ] } ], "exceptions": [ @@ -8710,6 +8718,10 @@ "tests/e2e/native-chat-ssh-delivery.unit.test.ts": { "added": 9, "removed": 8 + }, + "src/renderer/src/components/terminal-pane/terminal-pane-paste-listeners.ts": { + "added": 1, + "removed": 1 } } } diff --git a/src/renderer/src/components/terminal-pane/fork-terminal-dock/terminal-dock-paste-yield.test.ts b/src/renderer/src/components/terminal-pane/fork-terminal-dock/terminal-dock-paste-yield.test.ts new file mode 100644 index 000000000000..61507ac598eb --- /dev/null +++ b/src/renderer/src/components/terminal-pane/fork-terminal-dock/terminal-dock-paste-yield.test.ts @@ -0,0 +1,109 @@ +// @vitest-environment happy-dom + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { APP_MENU_PASTE_EVENT } from '@/lib/app-menu-paste' +import { registerTerminalPanePasteListeners } from '../terminal-pane-paste-listeners' +import type { TerminalPaneCloseController } from '../use-terminal-pane-close-actions' +import type { TerminalPanePasteExecution } from '../terminal-pane-paste-execution' + +const cleanups: (() => void)[] = [] + +afterEach(() => { + cleanups.splice(0).forEach((cleanup) => cleanup()) + document.body.innerHTML = '' +}) + +// Builds a pane container shaped like a docked pane: the xterm's input and the dock's +// chat root are siblings, with no chat cover over either. +function mountDockedPane() { + const container = document.createElement('div') + const xtermInput = document.createElement('textarea') + const dockRoot = document.createElement('div') + dockRoot.setAttribute('data-native-chat-root', 'true') + dockRoot.setAttribute('data-terminal-dock', '') + const composerInput = document.createElement('textarea') + dockRoot.append(composerInput) + container.append(xtermInput, dockRoot) + document.body.append(container) + + const pane = { id: 1 } + const pasteFromClipboard = vi.fn() + const controller = { + forceBracketedMultilineTextPaste: false, + keybindings: {}, + managerRef: { current: { getActivePane: () => pane, getPanes: () => [pane] } }, + setTerminalError: vi.fn(), + worktreeId: 'wt-1' + } + const execution = { executePanePasteText: vi.fn(), pasteFromClipboard } + cleanups.push( + registerTerminalPanePasteListeners({ + container, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the listeners read only the five controller fields this fixture supplies. + controller: controller as unknown as TerminalPaneCloseController, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the listeners call only executePanePasteText and pasteFromClipboard. + execution: execution as unknown as TerminalPanePasteExecution, + isMac: true, + shortcutPlatform: 'darwin' + }) + ) + return { xtermInput, composerInput, pasteFromClipboard } +} + +function pressCmdV(target: HTMLElement): KeyboardEvent { + const event = new KeyboardEvent('keydown', { + key: 'v', + code: 'KeyV', + metaKey: true, + bubbles: true, + cancelable: true + }) + target.dispatchEvent(event) + return event +} + +function firePaste(target: HTMLElement): Event { + const event = new Event('paste', { bubbles: true, cancelable: true }) + target.dispatchEvent(event) + return event +} + +describe('terminal pane paste listeners beside a terminal dock', () => { + it('leave Cmd+V in the dock composer to the dock', () => { + const { composerInput, pasteFromClipboard } = mountDockedPane() + + const event = pressCmdV(composerInput) + + expect(event.defaultPrevented).toBe(false) + expect(pasteFromClipboard).not.toHaveBeenCalled() + }) + + it('leave a paste event in the dock composer to the dock', () => { + const { composerInput, pasteFromClipboard } = mountDockedPane() + + const event = firePaste(composerInput) + + expect(event.defaultPrevented).toBe(false) + expect(pasteFromClipboard).not.toHaveBeenCalled() + }) + + it('leave an app-menu paste with focus in the dock composer to the dock', () => { + const { composerInput, pasteFromClipboard } = mountDockedPane() + composerInput.focus() + + const event = new Event(APP_MENU_PASTE_EVENT, { cancelable: true }) + window.dispatchEvent(event) + + expect(event.defaultPrevented).toBe(false) + expect(pasteFromClipboard).not.toHaveBeenCalled() + }) + + it('still paste Cmd+V on the xterm into the terminal', () => { + const { xtermInput, pasteFromClipboard } = mountDockedPane() + + const event = pressCmdV(xtermInput) + + expect(event.defaultPrevented).toBe(true) + expect(pasteFromClipboard).toHaveBeenCalledWith({ id: 1 }, 'keyboard') + }) +}) diff --git a/src/renderer/src/components/terminal-pane/fork-terminal-dock/terminal-dock-paste-yield.ts b/src/renderer/src/components/terminal-pane/fork-terminal-dock/terminal-dock-paste-yield.ts new file mode 100644 index 000000000000..6785659767e0 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/fork-terminal-dock/terminal-dock-paste-yield.ts @@ -0,0 +1,13 @@ +import { NATIVE_CHAT_ROOT_SELECTOR } from '@/lib/native-chat-paste-request' +import { isInsideNativeChatCover } from '../native-chat-covered-pane' + +/** + * True when the terminal pane must leave a paste to a chat: anything under a chat cover, + * or inside the dock's chat root, which sits beside the xterm instead of covering it. + */ +export function isTerminalPasteOwnedByChat(target: EventTarget | null): boolean { + return ( + isInsideNativeChatCover(target) || + (target instanceof Element && target.closest(NATIVE_CHAT_ROOT_SELECTOR) !== null) + ) +} diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-paste-listeners.ts b/src/renderer/src/components/terminal-pane/terminal-pane-paste-listeners.ts index 77cf5bb020a4..57b228989ec1 100644 --- a/src/renderer/src/components/terminal-pane/terminal-pane-paste-listeners.ts +++ b/src/renderer/src/components/terminal-pane/terminal-pane-paste-listeners.ts @@ -16,7 +16,7 @@ import { } from '@/lib/app-menu-selection-actions' import { isEditableTarget } from '@/lib/editable-target' import { copyTerminalSelection } from './terminal-selection-copy' -import { isInsideNativeChatCover } from './native-chat-covered-pane' +import { isTerminalPasteOwnedByChat as isInsideNativeChatCover } from './fork-terminal-dock/terminal-dock-paste-yield' import type { TerminalPaneCloseController } from './use-terminal-pane-close-actions' import { formatClipboardImagePasteError,