mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
fix(terminal): preserve paste across xterm focus replacement
This commit is contained in:
@@ -227,8 +227,10 @@ import { formatTerminalPasteExecutionError } from './terminal-paste-errors'
|
||||
import { resolveTerminalPasteRuntime } from './terminal-paste-runtime'
|
||||
import { getTerminalPasteSshRemotePlatform } from './terminal-paste-ssh-platform'
|
||||
import {
|
||||
captureTerminalPanePasteFocusSnapshot,
|
||||
isTerminalPanePasteFocusCurrent,
|
||||
isTerminalPanePasteTargetCurrent
|
||||
isTerminalPanePasteTargetCurrent,
|
||||
type TerminalPanePasteFocusSnapshot
|
||||
} from './terminal-paste-target-state'
|
||||
import { writeTerminalPastePtyInput } from './terminal-pty-paste-writer'
|
||||
import {
|
||||
@@ -1952,7 +1954,7 @@ function TerminalPane(
|
||||
const executePanePasteText = async (
|
||||
pane: ManagedPane,
|
||||
source: TerminalPasteSource,
|
||||
activeElementAtDispatch: Element | null,
|
||||
focusAtDispatch: TerminalPanePasteFocusSnapshot,
|
||||
text: string,
|
||||
options?: TerminalPasteTextOptions
|
||||
): Promise<void> => {
|
||||
@@ -1993,7 +1995,7 @@ function TerminalPane(
|
||||
}
|
||||
return isTerminalPanePasteFocusCurrent({
|
||||
requireSameFocusedElement: keyboardOwnedPaste,
|
||||
activeElementAtDispatch,
|
||||
focusAtDispatch,
|
||||
paneContainer: pane.container
|
||||
})
|
||||
},
|
||||
@@ -2044,7 +2046,7 @@ function TerminalPane(
|
||||
useAppStore.getState(),
|
||||
worktreeId
|
||||
)
|
||||
const activeElementAtDispatch = document.activeElement
|
||||
const focusAtDispatch = captureTerminalPanePasteFocusSnapshot(pane.container)
|
||||
void pasteTerminalClipboard({
|
||||
readClipboardText,
|
||||
saveClipboardImageAsTempFile: window.api.ui.saveClipboardImageAsTempFile,
|
||||
@@ -2052,7 +2054,7 @@ function TerminalPane(
|
||||
runtimeEnvironmentId,
|
||||
protectedMultilineTextPasteOptions: resolvePaneProtectedMultilinePasteOptions(pane),
|
||||
pasteText: (text, options) =>
|
||||
executePanePasteText(pane, source, activeElementAtDispatch, text, options),
|
||||
executePanePasteText(pane, source, focusAtDispatch, text, options),
|
||||
onTextPasteError: () =>
|
||||
setTerminalError('Paste failed: clipboard text is too large for a safe terminal paste.'),
|
||||
onImagePasteError: (error) => setTerminalError(formatClipboardImagePasteError(error))
|
||||
@@ -2187,6 +2189,10 @@ function TerminalPane(
|
||||
if (!pane) {
|
||||
return
|
||||
}
|
||||
const focusAtDispatch = captureTerminalPanePasteFocusSnapshot(
|
||||
pane.container,
|
||||
activeElementAtDispatch
|
||||
)
|
||||
const connectionId = getConnectionId(worktreeId) ?? null
|
||||
const runtimeEnvironmentId = getRuntimeEnvironmentIdForWorktree(
|
||||
useAppStore.getState(),
|
||||
@@ -2199,7 +2205,7 @@ function TerminalPane(
|
||||
runtimeEnvironmentId,
|
||||
protectedMultilineTextPasteOptions: resolvePaneProtectedMultilinePasteOptions(pane),
|
||||
pasteText: (text, options) =>
|
||||
executePanePasteText(pane, 'app-menu', activeElementAtDispatch, text, options),
|
||||
executePanePasteText(pane, 'app-menu', focusAtDispatch, text, options),
|
||||
onTextPasteError: () =>
|
||||
setTerminalError('Paste failed: clipboard text is too large for a safe terminal paste.'),
|
||||
onImagePasteError: (error) => setTerminalError(formatClipboardImagePasteError(error))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
captureTerminalPanePasteFocusSnapshot,
|
||||
isTerminalPanePasteFocusCurrent,
|
||||
isTerminalPanePasteTargetCurrent
|
||||
} from './terminal-paste-target-state'
|
||||
@@ -121,7 +122,7 @@ describe('terminal paste target state', () => {
|
||||
expect(
|
||||
isTerminalPanePasteFocusCurrent({
|
||||
requireSameFocusedElement: true,
|
||||
activeElementAtDispatch: terminalInput,
|
||||
focusAtDispatch: { element: terminalInput, ownedByPane: true },
|
||||
paneContainer,
|
||||
activeElement: terminalInput
|
||||
})
|
||||
@@ -136,7 +137,7 @@ describe('terminal paste target state', () => {
|
||||
expect(
|
||||
isTerminalPanePasteFocusCurrent({
|
||||
requireSameFocusedElement: true,
|
||||
activeElementAtDispatch: terminalInput,
|
||||
focusAtDispatch: { element: terminalInput, ownedByPane: true },
|
||||
paneContainer,
|
||||
activeElement: renameInput
|
||||
})
|
||||
@@ -151,7 +152,7 @@ describe('terminal paste target state', () => {
|
||||
expect(
|
||||
isTerminalPanePasteFocusCurrent({
|
||||
requireSameFocusedElement: true,
|
||||
activeElementAtDispatch: terminalInput,
|
||||
focusAtDispatch: { element: terminalInput, ownedByPane: true },
|
||||
paneContainer,
|
||||
activeElement: body
|
||||
})
|
||||
@@ -161,18 +162,42 @@ describe('terminal paste target state', () => {
|
||||
it('keeps keyboard-owned paste current when xterm replaces its helper textarea', () => {
|
||||
const originalTerminalInput = makeElement('textarea', ['xterm-helper-textarea'])
|
||||
const replacementTerminalInput = makeElement('textarea', ['xterm-helper-textarea'])
|
||||
const paneContainer = makePaneContainerFor([originalTerminalInput, replacementTerminalInput])
|
||||
let acceptedElements: readonly Element[] = [originalTerminalInput]
|
||||
const paneContainer = {
|
||||
contains: vi.fn((element: Element | null) => acceptedElements.includes(element as Element))
|
||||
} as unknown as Element
|
||||
const focusAtDispatch = captureTerminalPanePasteFocusSnapshot(
|
||||
paneContainer,
|
||||
originalTerminalInput
|
||||
)
|
||||
acceptedElements = [replacementTerminalInput]
|
||||
|
||||
expect(focusAtDispatch.ownedByPane).toBe(true)
|
||||
expect(
|
||||
isTerminalPanePasteFocusCurrent({
|
||||
requireSameFocusedElement: true,
|
||||
activeElementAtDispatch: originalTerminalInput,
|
||||
focusAtDispatch,
|
||||
paneContainer,
|
||||
activeElement: replacementTerminalInput
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects a replacement helper when the dispatch focus was never owned by the pane', () => {
|
||||
const outsideInput = makeElement('input')
|
||||
const replacementTerminalInput = makeElement('textarea', ['xterm-helper-textarea'])
|
||||
const paneContainer = makePaneContainerFor([replacementTerminalInput])
|
||||
|
||||
expect(
|
||||
isTerminalPanePasteFocusCurrent({
|
||||
requireSameFocusedElement: true,
|
||||
focusAtDispatch: { element: outsideInput, ownedByPane: false },
|
||||
paneContainer,
|
||||
activeElement: replacementTerminalInput
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects keyboard-owned paste when focus moves to another control in the pane', () => {
|
||||
const terminalInput = makeElement('textarea', ['xterm-helper-textarea'])
|
||||
const searchInput = makeElement('input')
|
||||
@@ -181,7 +206,7 @@ describe('terminal paste target state', () => {
|
||||
expect(
|
||||
isTerminalPanePasteFocusCurrent({
|
||||
requireSameFocusedElement: true,
|
||||
activeElementAtDispatch: terminalInput,
|
||||
focusAtDispatch: { element: terminalInput, ownedByPane: true },
|
||||
paneContainer,
|
||||
activeElement: searchInput
|
||||
})
|
||||
@@ -196,7 +221,7 @@ describe('terminal paste target state', () => {
|
||||
expect(
|
||||
isTerminalPanePasteFocusCurrent({
|
||||
requireSameFocusedElement: false,
|
||||
activeElementAtDispatch: terminalInput,
|
||||
focusAtDispatch: { element: terminalInput, ownedByPane: true },
|
||||
paneContainer,
|
||||
activeElement: otherInput
|
||||
})
|
||||
|
||||
@@ -38,23 +38,39 @@ export function isTerminalPanePasteTargetCurrent({
|
||||
)
|
||||
}
|
||||
|
||||
export type TerminalPanePasteFocusSnapshot = {
|
||||
element: Element | null
|
||||
ownedByPane: boolean
|
||||
}
|
||||
|
||||
export function captureTerminalPanePasteFocusSnapshot(
|
||||
paneContainer: Element,
|
||||
element: Element | null = typeof document === 'undefined' ? null : document.activeElement
|
||||
): TerminalPanePasteFocusSnapshot {
|
||||
return {
|
||||
element,
|
||||
ownedByPane: element === null || paneContainer.contains(element)
|
||||
}
|
||||
}
|
||||
|
||||
export type TerminalPanePasteFocusState = {
|
||||
requireSameFocusedElement: boolean
|
||||
activeElementAtDispatch: Element | null
|
||||
focusAtDispatch: TerminalPanePasteFocusSnapshot
|
||||
paneContainer: Element
|
||||
activeElement?: Element | null
|
||||
}
|
||||
|
||||
export function isTerminalPanePasteFocusCurrent({
|
||||
requireSameFocusedElement,
|
||||
activeElementAtDispatch,
|
||||
focusAtDispatch,
|
||||
paneContainer,
|
||||
activeElement = typeof document === 'undefined' ? null : document.activeElement
|
||||
}: TerminalPanePasteFocusState): boolean {
|
||||
const activeElementAtDispatch = focusAtDispatch.element
|
||||
if (!requireSameFocusedElement || activeElementAtDispatch === null) {
|
||||
return true
|
||||
}
|
||||
if (!paneContainer.contains(activeElementAtDispatch)) {
|
||||
if (!focusAtDispatch.ownedByPane) {
|
||||
return false
|
||||
}
|
||||
if (activeElement === activeElementAtDispatch) {
|
||||
|
||||
Reference in New Issue
Block a user