mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 16:03:26 +00:00
Revert "fix(agents): prevent startup paste from submitting user input"
This reverts commit affbf333e2.
This commit is contained in:
@@ -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: () =>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<TerminalClipboardPasteResult> {
|
||||
onPasteIntent?.()
|
||||
let text = ''
|
||||
try {
|
||||
text = await readClipboardText({ maxBytes: TERMINAL_PASTE_MAX_BYTES })
|
||||
|
||||
@@ -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<string, number>()
|
||||
|
||||
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.
|
||||
|
||||
@@ -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.'),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<void>((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
|
||||
|
||||
Reference in New Issue
Block a user