Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions config/fork-ownership.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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')
})
})
Original file line number Diff line number Diff line change
@@ -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)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading