diff --git a/src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts b/src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts index bcc61c9edf7..b4c955a0d60 100644 --- a/src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts @@ -343,10 +343,6 @@ describe('sendNativeChatMessageWithImageAttachments', () => { vi.advanceTimersByTime(NATIVE_CHAT_IMAGE_ATTACHMENT_SETTLE_MS) expect(sendRuntimePtyInput).toHaveBeenLastCalledWith(SETTINGS, PTY, 'what do you see?') - expect(`${framedImageWithSeparator}what do you see?`).toBe( - '\x1b[200~/tmp/orca-paste-image.png\x1b[201~ what do you see?' - ) - expect(`${framedImageWithSeparator}what do you see?`).not.toContain('png\x1b[201~what') vi.advanceTimersByTime(NATIVE_CHAT_SUBMIT_DELAY_MS) expect(sendRuntimePtyInput).toHaveBeenLastCalledWith(SETTINGS, PTY, NATIVE_CHAT_SUBMIT) diff --git a/src/renderer/src/components/terminal-pane/TerminalPane.tsx b/src/renderer/src/components/terminal-pane/TerminalPane.tsx index a59d5c57967..2b58732f0ea 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPane.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPane.tsx @@ -1981,7 +1981,6 @@ function TerminalPane( forceBracketedPaste: options?.forceBracketedPaste, forceBracketedPasteForMultiline: options?.forceBracketedPasteForMultiline, windowsInputRecordNewline: options?.windowsInputRecordNewline, - followedByNonImageInput: options?.followedByNonImageInput, terminalBracketedPasteMode: pane.terminal.modes.bracketedPasteMode }) const execution = await executeTerminalPastePlan(plan, { diff --git a/src/renderer/src/components/terminal-pane/terminal-bracketed-paste.ts b/src/renderer/src/components/terminal-pane/terminal-bracketed-paste.ts index cf06fd23583..ab0960a3ff7 100644 --- a/src/renderer/src/components/terminal-pane/terminal-bracketed-paste.ts +++ b/src/renderer/src/components/terminal-pane/terminal-bracketed-paste.ts @@ -1,5 +1,4 @@ import type { Terminal } from '@xterm/xterm' -import { separateImagePasteFromFollowingText } from '../../../../shared/image-paste-following-text' import type { WindowsInputRecordNewline } from './terminal-paste-model' type BracketedPasteTerminal = { @@ -17,7 +16,6 @@ type PasteTerminal = BracketedPasteTerminal & { type PasteTerminalTextOptions = { forceBracketedPaste?: boolean windowsInputRecordNewline?: WindowsInputRecordNewline - followedByNonImageInput?: boolean } const interruptedBracketedPasteTerminals = new WeakSet() @@ -103,6 +101,12 @@ export function encodeWindowsInputRecordPasteText( return encoded } +function forceBracketedPaste(terminal: PasteTerminal, text: string): void { + // Why: forced callers already built the exact paste protocol bytes. Send + // them as PTY input so xterm's DOM/native paste machinery cannot defer them. + terminal.input(wrapTerminalBracketedPasteText(text)) +} + export function markTerminalBracketedPasteInterrupted(terminal: BracketedPasteTerminal): void { if (terminal.modes.bracketedPasteMode) { interruptedBracketedPasteTerminals.add(terminal) @@ -139,14 +143,7 @@ export function pasteTerminalText( if (options?.forceBracketedPaste) { // Why: generated image paths are paste payloads, even when they are a // single line, so they must bypass stale Ctrl+C plain-text suppression. - // Trailing space is keyed off followedByNonImageInput only — never off - // forceBracketedPaste, which also wraps multiline text. - terminal.input( - separateImagePasteFromFollowingText( - wrapTerminalBracketedPasteText(text), - options.followedByNonImageInput === true - ) - ) + forceBracketedPaste(terminal, text) return } if (!interruptedBracketedPasteTerminals.has(terminal)) { diff --git a/src/renderer/src/components/terminal-pane/terminal-clipboard-paste.test.ts b/src/renderer/src/components/terminal-pane/terminal-clipboard-paste.test.ts index 93cb7f33873..422dc28791a 100644 --- a/src/renderer/src/components/terminal-pane/terminal-clipboard-paste.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-clipboard-paste.test.ts @@ -22,11 +22,7 @@ describe('terminal clipboard paste', () => { expect(pasteText).toHaveBeenCalledWith( '/var/folders/3l/b7w02vh17tg5r5s3nhhdf3kh0000gn/T/orca-paste-1760000000000-id.png', - { - forceBracketedPaste: true, - recoverImagePasteWebglAtlas: true, - followedByNonImageInput: true - } + { forceBracketedPaste: true, recoverImagePasteWebglAtlas: true } ) }) @@ -52,10 +48,8 @@ describe('terminal clipboard paste', () => { pasteText: (text, options) => pasteTerminalText(terminal, text, options) }) - const imageBytes = '\x1b[200~/tmp/orca-paste-1760000000000-id.png\x1b[201~ ' - expect(terminal.input).toHaveBeenCalledWith(imageBytes) - expect(`${imageBytes}describe this`).toBe( - '\x1b[200~/tmp/orca-paste-1760000000000-id.png\x1b[201~ describe this' + expect(terminal.input).toHaveBeenCalledWith( + '\x1b[200~/tmp/orca-paste-1760000000000-id.png\x1b[201~' ) expect(terminal.paste).not.toHaveBeenCalled() expect(observedIgnoreBracketedPasteMode).toEqual([false]) @@ -84,7 +78,7 @@ describe('terminal clipboard paste', () => { }) expect(terminal.input).toHaveBeenCalledWith( - '\x1b[200~/tmp/orca-paste-1760000000000-id.png\x1b[201~ ' + '\x1b[200~/tmp/orca-paste-1760000000000-id.png\x1b[201~' ) expect(terminal.paste).not.toHaveBeenCalled() expect(observedIgnoreBracketedPasteMode).toEqual([false]) @@ -110,8 +104,7 @@ describe('terminal clipboard paste', () => { }) expect(pasteText).toHaveBeenCalledWith('/var/tmp/orca-paste-1760000000000-id.png', { forceBracketedPaste: true, - recoverImagePasteWebglAtlas: true, - followedByNonImageInput: true + recoverImagePasteWebglAtlas: true }) }) @@ -134,8 +127,7 @@ describe('terminal clipboard paste', () => { }) expect(pasteText).toHaveBeenCalledWith('/tmp/orca-paste-1760000000000-runtime.png', { forceBracketedPaste: true, - recoverImagePasteWebglAtlas: true, - followedByNonImageInput: true + recoverImagePasteWebglAtlas: true }) }) @@ -152,8 +144,7 @@ describe('terminal clipboard paste', () => { expect(pasteText).toHaveBeenCalledWith('/tmp/orca-paste-1760000000000-id.png', { forceBracketedPaste: true, - recoverImagePasteWebglAtlas: true, - followedByNonImageInput: true + recoverImagePasteWebglAtlas: true }) }) @@ -175,8 +166,7 @@ describe('terminal clipboard paste', () => { }) expect(pasteText).toHaveBeenCalledWith('/tmp/orca-paste-1760000000000-id.png', { forceBracketedPaste: true, - recoverImagePasteWebglAtlas: true, - followedByNonImageInput: true + recoverImagePasteWebglAtlas: true }) }) diff --git a/src/renderer/src/components/terminal-pane/terminal-clipboard-paste.ts b/src/renderer/src/components/terminal-pane/terminal-clipboard-paste.ts index 594e3bbfa0d..abd0fb95b97 100644 --- a/src/renderer/src/components/terminal-pane/terminal-clipboard-paste.ts +++ b/src/renderer/src/components/terminal-pane/terminal-clipboard-paste.ts @@ -86,10 +86,8 @@ export async function pasteTerminalClipboard({ const result = await pasteText(filePath, { // Why: a generated clipboard-image path is terminal image injection, not // ordinary one-line text. Keep it off the Ctrl+C stale-text paste path. - // Trailing space so later typed text cannot glue onto `.png`. forceBracketedPaste: true, - recoverImagePasteWebglAtlas: true, - followedByNonImageInput: true + recoverImagePasteWebglAtlas: true }) if (result === false) { return { status: 'skipped', reason: 'image-paste-rejected' } diff --git a/src/renderer/src/components/terminal-pane/terminal-image-paste-following-text.test.ts b/src/renderer/src/components/terminal-pane/terminal-image-paste-following-text.test.ts deleted file mode 100644 index 91f7ed1a461..00000000000 --- a/src/renderer/src/components/terminal-pane/terminal-image-paste-following-text.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' - -import { pasteTerminalText } from './terminal-bracketed-paste' -import { - executeTerminalPastePlan, - planTerminalPaste, - type TerminalPasteTarget -} from './terminal-paste-coordinator' - -function terminalTarget(): TerminalPasteTarget { - return { - kind: 'terminal', - paneId: 1, - leafId: 'leaf-1', - ptyId: 'pty-1', - runtime: { - platform: 'darwin', - runtimeKey: 'local:darwin', - kind: 'local' - } - } -} - -function createTerminal() { - return { - modes: { bracketedPasteMode: true }, - options: { ignoreBracketedPasteMode: false }, - input: vi.fn(), - paste: vi.fn() - } -} - -describe('clipboard image paste through plan → execute → pasteTerminalText', () => { - it('writes a trailing space after the bracketed image path so later typing cannot glue', async () => { - const terminal = createTerminal() - const plan = planTerminalPaste({ - text: '/tmp/orca-paste-1760000000000-id.png', - source: 'keyboard', - target: terminalTarget(), - forceBracketedPaste: true, - followedByNonImageInput: true - }) - - const result = await executeTerminalPastePlan(plan, { - pasteText: (text, options) => pasteTerminalText(terminal, text, options), - isTargetCurrent: () => true - }) - - expect(result.status).toBe('pasted') - const imageBytes = '\x1b[200~/tmp/orca-paste-1760000000000-id.png\x1b[201~ ' - expect(terminal.input).toHaveBeenCalledWith(imageBytes) - expect(`${imageBytes}describe this`).toBe( - '\x1b[200~/tmp/orca-paste-1760000000000-id.png\x1b[201~ describe this' - ) - expect(terminal.paste).not.toHaveBeenCalled() - }) - - it('does not append a trailing space to forced-bracketed multiline text paste', async () => { - const terminal = createTerminal() - const plan = planTerminalPaste({ - text: 'one\r\ntwo', - source: 'keyboard', - target: terminalTarget(), - forceBracketedPasteForMultiline: true - }) - - const result = await executeTerminalPastePlan(plan, { - pasteText: (text, options) => pasteTerminalText(terminal, text, options), - isTargetCurrent: () => true - }) - - expect(result.status).toBe('pasted') - expect(plan.mode).toBe('bracketed-terminal') - expect(terminal.input).toHaveBeenCalledWith('\x1b[200~one\rtwo\x1b[201~') - expect(terminal.paste).not.toHaveBeenCalled() - }) -}) diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-menu-paste.ts b/src/renderer/src/components/terminal-pane/terminal-pane-menu-paste.ts index 179997934ad..295b7e35c53 100644 --- a/src/renderer/src/components/terminal-pane/terminal-pane-menu-paste.ts +++ b/src/renderer/src/components/terminal-pane/terminal-pane-menu-paste.ts @@ -84,7 +84,6 @@ export const executeTerminalPaneMenuPasteText = async ( forceBracketedPaste: options?.forceBracketedPaste, forceBracketedPasteForMultiline: options?.forceBracketedPasteForMultiline, windowsInputRecordNewline: options?.windowsInputRecordNewline, - followedByNonImageInput: options?.followedByNonImageInput, terminalBracketedPasteMode: pane.terminal.modes.bracketedPasteMode }) const execution = await executeTerminalPastePlan(plan, { diff --git a/src/renderer/src/components/terminal-pane/terminal-paste-coordinator.ts b/src/renderer/src/components/terminal-pane/terminal-paste-coordinator.ts index f587f3b20f2..a6aa23cc262 100644 --- a/src/renderer/src/components/terminal-pane/terminal-paste-coordinator.ts +++ b/src/renderer/src/components/terminal-pane/terminal-paste-coordinator.ts @@ -44,7 +44,6 @@ type PlanTerminalPasteArgs = { forceBracketedPaste?: boolean forceBracketedPasteForMultiline?: boolean windowsInputRecordNewline?: WindowsInputRecordNewline - followedByNonImageInput?: boolean terminalBracketedPasteMode?: boolean hasRichText?: boolean maxDirectBytes?: number @@ -87,7 +86,6 @@ export function planTerminalPaste({ forceBracketedPaste = false, forceBracketedPasteForMultiline = false, windowsInputRecordNewline, - followedByNonImageInput = false, terminalBracketedPasteMode = false, hasRichText = false, maxDirectBytes = TERMINAL_PASTE_DIRECT_MAX_BYTES, @@ -99,7 +97,6 @@ export function planTerminalPaste({ forceBracketedPaste, forceBracketedPasteForMultiline, windowsInputRecordNewline, - followedByNonImageInput, maxBytes, maxChunkBytes, maxDirectBytes, @@ -116,7 +113,6 @@ export async function planTerminalPasteWithYield({ forceBracketedPaste = false, forceBracketedPasteForMultiline = false, windowsInputRecordNewline, - followedByNonImageInput = false, terminalBracketedPasteMode = false, hasRichText = false, maxDirectBytes = TERMINAL_PASTE_DIRECT_MAX_BYTES, @@ -143,7 +139,6 @@ export async function planTerminalPasteWithYield({ forceBracketedPaste, forceBracketedPasteForMultiline, windowsInputRecordNewline, - followedByNonImageInput, maxBytes, maxChunkBytes, maxDirectBytes, @@ -159,7 +154,6 @@ function buildTerminalPastePlan({ forceBracketedPaste, forceBracketedPasteForMultiline, windowsInputRecordNewline, - followedByNonImageInput, terminalBracketedPasteMode, maxDirectBytes, maxChunkBytes, @@ -170,7 +164,6 @@ function buildTerminalPastePlan({ forceBracketedPaste: boolean forceBracketedPasteForMultiline: boolean windowsInputRecordNewline?: WindowsInputRecordNewline - followedByNonImageInput: boolean terminalBracketedPasteMode: boolean maxDirectBytes: number maxChunkBytes: number @@ -212,7 +205,6 @@ function buildTerminalPastePlan({ ...(effectiveWindowsInputRecordNewline ? { windowsInputRecordNewline: effectiveWindowsInputRecordNewline } : {}), - ...(followedByNonImageInput ? { followedByNonImageInput: true } : {}), runtimeKey: target.runtime.runtimeKey, ...(shouldChunk ? { maxChunkBytes } : {}), bracketed: mode === 'bracketed-terminal' || (mode === 'chunked' && shouldBracketChunk), diff --git a/src/renderer/src/components/terminal-pane/terminal-paste-executor.ts b/src/renderer/src/components/terminal-pane/terminal-paste-executor.ts index edeb66e1cdd..39a05f4dd24 100644 --- a/src/renderer/src/components/terminal-pane/terminal-paste-executor.ts +++ b/src/renderer/src/components/terminal-pane/terminal-paste-executor.ts @@ -65,8 +65,7 @@ async function executeTerminalPastePlanNow( forceBracketedPaste: plan.mode === 'bracketed-terminal', ...(plan.windowsInputRecordNewline ? { windowsInputRecordNewline: plan.windowsInputRecordNewline } - : {}), - ...(plan.followedByNonImageInput ? { followedByNonImageInput: true } : {}) + : {}) }) }, operationTimeoutMs) if (pasteResult.timedOut) { diff --git a/src/renderer/src/components/terminal-pane/terminal-paste-model.ts b/src/renderer/src/components/terminal-pane/terminal-paste-model.ts index a7f6f055ecb..6a1d21fafeb 100644 --- a/src/renderer/src/components/terminal-pane/terminal-paste-model.ts +++ b/src/renderer/src/components/terminal-pane/terminal-paste-model.ts @@ -40,7 +40,6 @@ export type TerminalPastePlan = { // every write path applies xterm's native \r?\n -> \r before ConPTY sees LF. newlinePolicy: 'preserve' | 'terminal-cr' | 'windows-input-record' windowsInputRecordNewline?: WindowsInputRecordNewline - followedByNonImageInput?: boolean runtimeKey: string maxChunkBytes?: number bracketed: boolean @@ -53,7 +52,6 @@ export type TerminalPasteTextOptions = { forceBracketedPasteForMultiline?: boolean windowsInputRecordNewline?: WindowsInputRecordNewline recoverImagePasteWebglAtlas?: boolean - followedByNonImageInput?: boolean } export type WindowsInputRecordNewline = 'alt-enter' | 'csi-u'