From 40560cc9f39544fd2b7103d8d68a0d71dfb848b5 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 30 Jul 2026 21:32:10 -0700 Subject: [PATCH] Revert "fix(agents): prevent startup paste from submitting user input" This reverts commit affbf333e26226ad9d509e64ac351d00b9477ccb. --- .../components/terminal-pane/TerminalPane.tsx | 5 +- .../terminal-pane/pty-connection.test.ts | 5 -- .../terminal-pane/pty-connection.ts | 9 +--- .../terminal-clipboard-paste.test.ts | 17 ------- .../terminal-pane/terminal-clipboard-paste.ts | 3 -- .../terminal-pane/terminal-input-activity.ts | 20 +------- .../use-terminal-pane-context-menu.ts | 4 +- .../src/lib/agent-paste-draft.test.ts | 49 +------------------ src/renderer/src/lib/agent-paste-draft.ts | 27 +--------- 9 files changed, 12 insertions(+), 127 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/TerminalPane.tsx b/src/renderer/src/components/terminal-pane/TerminalPane.tsx index e7612df36fe..e1e4c590a68 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPane.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPane.tsx @@ -2029,6 +2029,9 @@ function TerminalPane( setTerminalError(formatTerminalPasteExecutionError(execution.reason)) return } + if (text) { + recordTerminalUserInputForLeaf(tabId, pane.leafId) + } if (options?.recoverImagePasteWebglAtlas) { scheduleImagePasteWebglAtlasRecovery() } @@ -2052,7 +2055,6 @@ function TerminalPane( connectionId, runtimeEnvironmentId, forceBracketedMultilineTextPaste, - onPasteIntent: () => recordTerminalUserInputForLeaf(tabId, pane.leafId), pasteText: (text, options) => executePanePasteText(pane, source, activeElementAtDispatch, text, options), onTextPasteError: () => @@ -2200,7 +2202,6 @@ function TerminalPane( connectionId, runtimeEnvironmentId, forceBracketedMultilineTextPaste, - onPasteIntent: () => recordTerminalUserInputForLeaf(tabId, pane.leafId), pasteText: (text, options) => executePanePasteText(pane, 'app-menu', activeElementAtDispatch, text, options), onTextPasteError: () => diff --git a/src/renderer/src/components/terminal-pane/pty-connection.test.ts b/src/renderer/src/components/terminal-pane/pty-connection.test.ts index 3ffbc766d7b..1d42057d539 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.test.ts @@ -3621,7 +3621,6 @@ describe('connectPanePty', () => { it('records hibernation activity from the core user-input signal, not synthetic onData replies', async () => { // Regression: xterm auto-replies share the onData stream with typing; recording them as input made the planner see "input after done" and never hibernate. const { connectPanePty } = await import('./pty-connection') - const { readTerminalUserInputGeneration } = await import('./terminal-input-activity') const transport = createMockTransport('pty-pane-2') transportFactoryQueue.push(transport) const manager = createManager(1) @@ -3652,12 +3651,8 @@ describe('connectPanePty', () => { expect(transport.sendInput).toHaveBeenCalledWith('\x1b[O') // Real user input fires the core signal and records activity. - const activePtyId = (transport.getPtyId as unknown as () => string | null)() - expect(activePtyId).toBeTypeOf('string') - const inputGeneration = readTerminalUserInputGeneration(activePtyId!) ;(userInputListener as unknown as () => void)() expect(mockStoreState.recordTerminalInput).toHaveBeenCalledTimes(1) - expect(readTerminalUserInputGeneration(activePtyId!)).toBe(inputGeneration + 1) binding.dispose() expect(userInputDispose).toHaveBeenCalled() diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index 83398227ee4..daf09427ca7 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -136,7 +136,6 @@ import { } from '../../../../shared/terminal-color-scheme-protocol' import { warnTerminalLifecycleAnomaly } from './terminal-lifecycle-diagnostics' import { subscribeToTerminalUserInput } from './terminal-user-input-signal' -import { markTerminalUserInputForPtyId } from './terminal-input-activity' import { hasPtySerializer, registerPtySerializer, @@ -3616,10 +3615,6 @@ export function connectPanePty( const recordTerminalInputForHibernation = (): void => { useAppStore.getState().recordTerminalInput(cacheKey) } - const recordRealTerminalUserInput = (): void => { - markTerminalUserInputForPtyId(transport.getPtyId()) - recordTerminalInputForHibernation() - } // Why: onData mixes real user input with xterm's parser auto-replies (focus // reports, DA/DSR/CPR responses). Recording those replies as activity makes // the hibernation planner treat a pane hidden after its agent finished as @@ -3628,11 +3623,11 @@ export function connectPanePty( // solely as the fallback when the internal API is unavailable. const userInputActivityDisposable = subscribeToTerminalUserInput( pane.terminal, - recordRealTerminalUserInput + recordTerminalInputForHibernation ) const recordTerminalInputForHibernationFallback = (): void => { if (userInputActivityDisposable === null) { - recordRealTerminalUserInput() + recordTerminalInputForHibernation() } } const markAcceptedTerminalInputSent = (): void => { 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 dd2909f99f3..5e08bc002bb 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 @@ -186,23 +186,6 @@ describe('terminal clipboard paste', () => { expect(saveClipboardImageAsTempFile).not.toHaveBeenCalled() }) - it('records paste intent before waiting for clipboard text', async () => { - const onPasteIntent = vi.fn() - const readClipboardText = vi.fn(async () => { - expect(onPasteIntent).toHaveBeenCalledOnce() - return 'one\r\ntwo\r\nthree\r\nfour\r\nfive' - }) - - await pasteTerminalClipboard({ - readClipboardText, - saveClipboardImageAsTempFile: vi.fn(), - pasteText: vi.fn(), - onPasteIntent - }) - - expect(onPasteIntent).toHaveBeenCalledOnce() - }) - it('reports text paste execution failures without probing for image fallback', async () => { const pasteError = new Error('terminal disconnected') const saveClipboardImageAsTempFile = vi.fn() 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 ed3505f66d8..2c889502918 100644 --- a/src/renderer/src/components/terminal-pane/terminal-clipboard-paste.ts +++ b/src/renderer/src/components/terminal-pane/terminal-clipboard-paste.ts @@ -22,7 +22,6 @@ type PasteTerminalClipboardDeps = { connectionId?: string | null runtimeEnvironmentId?: string | null forceBracketedMultilineTextPaste?: boolean - onPasteIntent?: () => void onTextPasteError?: (error: unknown) => void onImagePasteError?: (error: unknown) => void } @@ -47,11 +46,9 @@ export async function pasteTerminalClipboard({ connectionId, runtimeEnvironmentId, forceBracketedMultilineTextPaste = false, - onPasteIntent, onTextPasteError, onImagePasteError }: PasteTerminalClipboardDeps): Promise { - onPasteIntent?.() let text = '' try { text = await readClipboardText({ maxBytes: TERMINAL_PASTE_MAX_BYTES }) diff --git a/src/renderer/src/components/terminal-pane/terminal-input-activity.ts b/src/renderer/src/components/terminal-pane/terminal-input-activity.ts index 2949f8f094e..d37568553a9 100644 --- a/src/renderer/src/components/terminal-pane/terminal-input-activity.ts +++ b/src/renderer/src/components/terminal-pane/terminal-input-activity.ts @@ -1,29 +1,11 @@ import { makePaneKey } from '../../../../shared/stable-pane-id' import { useAppStore } from '@/store' -// Why: delayed automatic Enter must yield when real input reaches the same PTY. -const userInputGenerationByPtyId = new Map() - -export function markTerminalUserInputForPtyId(ptyId: string | null | undefined): void { - if (!ptyId) { - return - } - userInputGenerationByPtyId.set(ptyId, (userInputGenerationByPtyId.get(ptyId) ?? 0) + 1) -} - -export function readTerminalUserInputGeneration(ptyId: string): number { - return userInputGenerationByPtyId.get(ptyId) ?? 0 -} - export function recordTerminalUserInputForLeaf(tabId: string, leafId: string): void { try { - const state = useAppStore.getState() - const layoutPtyId = state.terminalLayoutsByTabId?.[tabId]?.ptyIdsByLeafId?.[leafId] - const tabPtyIds = state.ptyIdsByTabId?.[tabId] ?? [] - markTerminalUserInputForPtyId(layoutPtyId ?? (tabPtyIds.length === 1 ? tabPtyIds[0] : null)) // Why: hibernation must see all user-authorized terminal writes, including // sends that bypass xterm.onData. - state.recordTerminalInput(makePaneKey(tabId, leafId)) + useAppStore.getState().recordTerminalInput(makePaneKey(tabId, leafId)) } catch { // Legacy/malformed layouts are ignored; hibernation remains conservative // when it cannot match live PTYs to stable pane keys. diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-context-menu.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-context-menu.ts index 4fadc9615ed..a87291fb776 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-context-menu.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-context-menu.ts @@ -266,6 +266,9 @@ export function useTerminalPaneContextMenu({ onPasteError(formatTerminalPasteExecutionError(execution.reason)) return false } + if (text) { + recordTerminalUserInputForLeaf(tabId, pane.leafId) + } if (options?.recoverImagePasteWebglAtlas) { scheduleImagePasteWebglAtlasRecovery() } @@ -320,7 +323,6 @@ export function useTerminalPaneContextMenu({ connectionId, runtimeEnvironmentId, forceBracketedMultilineTextPaste, - onPasteIntent: () => recordTerminalUserInputForLeaf(tabId, pane.leafId), pasteText: (text, options) => executeMenuPasteText(pane, source, text, options), onTextPasteError: () => onPasteError('Paste failed: clipboard text is too large for a safe terminal paste.'), diff --git a/src/renderer/src/lib/agent-paste-draft.test.ts b/src/renderer/src/lib/agent-paste-draft.test.ts index 1061e6001d2..c5e4ddd2963 100644 --- a/src/renderer/src/lib/agent-paste-draft.test.ts +++ b/src/renderer/src/lib/agent-paste-draft.test.ts @@ -12,7 +12,6 @@ import { sendBracketedPasteToRunningAgent, submitPromptToAgentPty } from './agent-paste-draft' -import { markTerminalUserInputForPtyId } from '@/components/terminal-pane/terminal-input-activity' const testState = vi.hoisted(() => ({ appState: { @@ -55,7 +54,7 @@ vi.mock('@/runtime/runtime-terminal-stream', () => ({ const DECSET_BRACKETED_PASTE = '\x1b[?2004h' const SHOW_CURSOR = '\x1b[?25h' const CODEX_COMPOSER_PROMPT_RENDER = '\x1b[1m›\x1b[0m Ask Codex to do anything' -const ISSUE_URL = 'Synthetic linked work item context' +const ISSUE_URL = 'https://github.com/stablyai/orca/issues/123' const PASTED_ISSUE_URL = `\x1b[200~${ISSUE_URL}\x1b[201~` describe('pasteDraftWhenAgentReady', () => { @@ -359,52 +358,6 @@ describe('pasteDraftWhenAgentReady', () => { expect(testState.sendRuntimePtyInputVerified).toHaveBeenNthCalledWith(2, {}, 'pty-1', '\r') }) - it('does not submit when a five-line Windows paste follows injected context', async () => { - const ptyWrites: string[] = [] - testState.sendRuntimePtyInputVerified.mockImplementation( - async (_settings: unknown, _ptyId: string, data: string) => { - ptyWrites.push(data) - return true - } - ) - const promise = pasteDraftWhenAgentReady({ - tabId: 'tab-1', - content: ISSUE_URL, - agent: 'codex', - submit: true, - forcePaste: true - }) - await flushMicrotasks() - - testState.ptyObserver?.(`${DECSET_BRACKETED_PASTE}${CODEX_COMPOSER_PROMPT_RENDER}`) - await flushMicrotasks() - expect(ptyWrites).toEqual([PASTED_ISSUE_URL]) - - markTerminalUserInputForPtyId('pty-1') - ptyWrites.push('\x1b[200~one\rtwo\rthree\rfour\rfive\x1b[201~') - await vi.advanceTimersByTimeAsync(50) - - await expect(promise).resolves.toBe(false) - expect(ptyWrites).toEqual([PASTED_ISSUE_URL, '\x1b[200~one\rtwo\rthree\rfour\rfive\x1b[201~']) - }) - - it('does not inject launch context after user input takes over during readiness', async () => { - const promise = pasteDraftWhenAgentReady({ - tabId: 'tab-1', - content: ISSUE_URL, - agent: 'codex', - submit: true, - forcePaste: true - }) - await flushMicrotasks() - - markTerminalUserInputForPtyId('pty-1') - testState.ptyObserver?.(`${DECSET_BRACKETED_PASTE}${CODEX_COMPOSER_PROMPT_RENDER}`) - - await expect(promise).resolves.toBe(false) - expect(testState.sendRuntimePtyInputVerified).not.toHaveBeenCalled() - }) - it('does not submit when the verified paste write fails', async () => { testState.sendRuntimePtyInputVerified.mockResolvedValueOnce(false) diff --git a/src/renderer/src/lib/agent-paste-draft.ts b/src/renderer/src/lib/agent-paste-draft.ts index 51569debf65..86b0472c9c4 100644 --- a/src/renderer/src/lib/agent-paste-draft.ts +++ b/src/renderer/src/lib/agent-paste-draft.ts @@ -10,7 +10,6 @@ import { BRACKETED_PASTE_START, sanitizeTerminalPasteText } from '@/components/terminal-pane/terminal-bracketed-paste' -import { readTerminalUserInputGeneration } from '@/components/terminal-pane/terminal-input-activity' import { waitForAgentReady } from './agent-ready-wait' import { getSettingsForWorktreeRuntimeOwner } from './worktree-runtime-owner' import { sendAgentDraftPasteContent } from './agent-draft-paste-content' @@ -103,8 +102,6 @@ export async function pasteDraftWhenAgentReady(args: { onTimeout?.() return false } - const submitUserInputGeneration = - submit === true ? readTerminalUserInputGeneration(ptyId) : undefined const settings = getSettingsForAgentTabRuntimeOwner(tabId) const ready = await waitForAgentDraftInputReady(ptyId, budget, readySignal, settings) @@ -126,8 +123,7 @@ export async function pasteDraftWhenAgentReady(args: { settings, ptyId, content, - submit: submit === true, - submitUserInputGeneration + submit: submit === true }) } @@ -150,8 +146,6 @@ export async function pasteDraftToAgentPtyWhenReady(args: { const budget = timeoutMs ?? READINESS_TIMEOUT_MS const settings = getSettingsForAgentTabRuntimeOwner(tabId) - const submitUserInputGeneration = - submit === true ? readTerminalUserInputGeneration(ptyId) : undefined const readySignal = agentConfig?.draftPasteReadySignal ?? 'render-quiet-after-bracketed-paste' const ready = await waitForAgentDraftInputReady(ptyId, budget, readySignal, settings) if (!ready) { @@ -168,8 +162,7 @@ export async function pasteDraftToAgentPtyWhenReady(args: { settings, ptyId, content, - submit: submit === true, - submitUserInputGeneration + submit: submit === true }) } @@ -216,17 +209,9 @@ async function sendBracketedPasteToAgent(args: { ptyId: string content: string submit: boolean - submitUserInputGeneration?: number }): Promise { const { settings = useAppStore.getState().settings, ptyId, content, submit } = args - const submitUserInputGeneration = - submit === true - ? (args.submitUserInputGeneration ?? readTerminalUserInputGeneration(ptyId)) - : undefined try { - if (didTerminalUserInputChange(ptyId, submitUserInputGeneration)) { - return false - } const pasted = await sendAgentDraftPasteContent(settings, ptyId, content) if (!pasted) { return false @@ -239,20 +224,12 @@ async function sendBracketedPasteToAgent(args: { // Enter arrive in the same PTY write. Split the submit into the next turn so // the TUI processes bracketed-paste termination before handling Enter. await new Promise((resolve) => window.setTimeout(resolve, POST_PASTE_SUBMIT_DELAY_MS)) - // Why: user input has a separate transport queue; never submit mixed content. - if (didTerminalUserInputChange(ptyId, submitUserInputGeneration)) { - return false - } return await sendRuntimePtyInputVerified(settings, ptyId, '\r') } catch { return false } } -function didTerminalUserInputChange(ptyId: string, generation: number | undefined): boolean { - return generation !== undefined && readTerminalUserInputGeneration(ptyId) !== generation -} - /** * Why: activation creates the tab synchronously but the PTY spawn is * async. Poll the store until the primary PTY id appears or the budget