fix(terminal): serialize bracketed paste operations

This commit is contained in:
OrcaWin
2026-07-30 21:29:17 -07:00
parent fd0af970ac
commit 4163cb9a05
4 changed files with 107 additions and 0 deletions
@@ -6,6 +6,7 @@ import {
TERMINAL_REMOTE_PASTE_OPERATION_TIMEOUT_MS
} from './terminal-paste-limits'
import { runTerminalPasteOperationWithTimeout } from './terminal-paste-operation-timeout'
import { runTerminalPtyInputTransaction } from './terminal-pty-input-transaction'
import type {
TerminalPasteExecutionReason,
TerminalPasteExecutionResult,
@@ -24,6 +25,15 @@ type ExecuteTerminalPastePlanArgs = {
}
export async function executeTerminalPastePlan(
plan: TerminalPastePlan,
args: ExecuteTerminalPastePlanArgs
): Promise<TerminalPasteExecutionResult> {
return await runTerminalPtyInputTransaction(plan.target.ptyId, () =>
executeTerminalPastePlanNow(plan, args)
)
}
async function executeTerminalPastePlanNow(
plan: TerminalPastePlan,
{
pasteText,
@@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest'
import { sendAgentDraftPasteContent } from '@/lib/agent-draft-paste-content'
import {
BRACKETED_PASTE_END,
BRACKETED_PASTE_START,
wrapTerminalBracketedPasteText
} from './terminal-bracketed-paste'
import { executeTerminalPastePlan, planTerminalPaste } from './terminal-paste-coordinator'
describe('terminal paste operation ordering', () => {
it('keeps startup context outside an active chunked user paste frame', async () => {
const writes: string[] = []
let startupDraft: Promise<boolean> | null = null
const writePty = async (data: string): Promise<boolean> => {
writes.push(data)
return true
}
const userPaste = planTerminalPaste({
text: 'user-line-1\nuser-line-2',
source: 'keyboard',
target: {
kind: 'terminal',
paneId: 1,
leafId: 'leaf-1',
ptyId: 'pty-1',
runtime: {
platform: 'win32',
runtimeKey: 'local:win32',
kind: 'local',
isWindowsConpty: true
}
},
terminalBracketedPasteMode: true,
maxDirectBytes: 4,
maxChunkBytes: 12
})
const userPasteResult = await executeTerminalPastePlan(userPaste, {
pasteText: () => {},
writePty,
yieldToEventLoop: async () => {
startupDraft ??= sendAgentDraftPasteContent(null, 'pty-1', 'GENERATED_CONTEXT', writePty)
}
})
await startupDraft
expect(userPasteResult.status).toBe('pasted')
expect(writes).toEqual([
BRACKETED_PASTE_START,
'user-line-1\r',
'user-line-2',
BRACKETED_PASTE_END,
wrapTerminalBracketedPasteText('GENERATED_CONTEXT')
])
})
})
@@ -0,0 +1,27 @@
const transactionTails = new Map<string, Promise<void>>()
export async function runTerminalPtyInputTransaction<T>(
ptyId: string | null | undefined,
operation: () => Promise<T>
): Promise<T> {
if (!ptyId) {
return await operation()
}
const previous = transactionTails.get(ptyId) ?? Promise.resolve()
let release!: () => void
const current = new Promise<void>((resolve) => {
release = resolve
})
transactionTails.set(ptyId, current)
await previous
try {
return await operation()
} finally {
release()
if (transactionTails.get(ptyId) === current) {
transactionTails.delete(ptyId)
}
}
}
@@ -6,6 +6,7 @@ import {
normalizeTerminalPasteLineEndings,
wrapTerminalBracketedPasteText
} from '@/components/terminal-pane/terminal-bracketed-paste'
import { runTerminalPtyInputTransaction } from '@/components/terminal-pane/terminal-pty-input-transaction'
import { sendRuntimePtyInputVerified } from '@/runtime/runtime-terminal-inspection'
// Why: bracketed paste markers let supported TUIs treat generated prompt text
@@ -26,6 +27,18 @@ export async function sendAgentDraftPasteContent(
content: string,
writePty?: AgentDraftPtyInputWriter,
canContinue?: () => boolean
): Promise<boolean> {
return await runTerminalPtyInputTransaction(ptyId, () =>
sendAgentDraftPasteContentNow(settings, ptyId, content, writePty, canContinue)
)
}
async function sendAgentDraftPasteContentNow(
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
ptyId: string,
content: string,
writePty?: AgentDraftPtyInputWriter,
canContinue?: () => boolean
): Promise<boolean> {
if (content.length > AGENT_DRAFT_PASTE_MAX_BYTES || canContinue?.() === false) {
return false