fix(terminal): prevent focus prefix in startup drafts (#8433)

* fix(terminal): order startup drafts after focus input

* fix(terminal): preserve startup draft activity
This commit is contained in:
OrcaWin 2026-07-12 18:00:38 -07:00 committed by GitHub
parent fcd60a03f8
commit d5627638ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 53 additions and 12 deletions

View File

@ -4593,7 +4593,7 @@ describe('connectPanePty', () => {
}
})
it('pastes a startup draft when Codex renders its composer in the first observed output', async () => {
it('orders a startup draft behind xterm focus input when Codex renders its composer', async () => {
const { connectPanePty } = await import('./pty-connection')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
@ -4626,13 +4626,27 @@ describe('connectPanePty', () => {
await flushAsyncTicks()
expect(capturedDataCallback.current).not.toBeNull()
// A focused xterm emits CSI I after Codex enables focus reporting. The
// startup draft must use the same transport instead of racing a direct IPC.
;(
pane.terminal.onData as unknown as {
mock: { calls: [(data: string) => void][] }
}
).mock.calls[0]?.[0]('\x1b[I')
;(mockStoreState.recordTerminalInput as ReturnType<typeof vi.fn>).mockClear()
capturedDataCallback.current?.('\x1b[?2004h\x1b[2K ')
await flushAsyncTicks()
expect(window.api.pty.writeAccepted).toHaveBeenCalledWith(
'pty-codex',
expect(transport.sendInputAccepted).toHaveBeenCalledWith(
'\x1b[200~https://github.com/stablyai/orca/issues/42\x1b[201~'
)
expect(transport.sendInput.mock.calls.map(([data]) => data)).toEqual([
'\x1b[I',
'\x1b[200~https://github.com/stablyai/orca/issues/42\x1b[201~'
])
expect(window.api.pty.writeAccepted).not.toHaveBeenCalled()
expect(mockStoreState.recordTerminalInput).toHaveBeenCalledOnce()
expect(mockStoreState.recordTerminalInput).toHaveBeenCalledWith(makePaneKey('tab-1', LEAF_1))
})
it('does not consume startup draft delivery before deferred connect starts', async () => {

View File

@ -219,6 +219,7 @@ import { isWslUncPath } from '../../../../shared/wsl-paths'
import { isTuiAgent, TUI_AGENT_CONFIG } from '../../../../shared/tui-agent-config'
import { createDraftPasteReadyScanner } from '../../../../shared/draft-paste-ready-scanner'
import { sendAgentDraftPasteContent } from '@/lib/agent-draft-paste-content'
import { writeTerminalPastePtyInput } from './terminal-pty-paste-writer'
import {
beginAgentStartupDeliveryAttempt,
releaseAgentStartupDeliveryAttempt
@ -3938,6 +3939,7 @@ export function connectPanePty(
let startupDraftReadinessArmed = false
let startupDraftPasteSettled = !ownsStartupDraftPaste
let startupDraftPasteInFlight = false
let startupDraftInputRecorded = false
let startupDraftQuietTimer: ReturnType<typeof setTimeout> | null = null
let startupDraftHardTimer: ReturnType<typeof setTimeout> | null = null
const clearStartupDraftPasteTimers = (): void => {
@ -3981,7 +3983,18 @@ export function connectPanePty(
startupDraftPasteAttempted = true
cleanupStartupDraftPasteTimers()
const settings = getSettingsForWorktreeRuntimeOwner(useAppStore.getState(), deps.worktreeId)
void sendAgentDraftPasteContent(settings, ptyId, startupDraftPrompt)
// Why: xterm focus reports share this transport queue. Bypassing it can
// race CSI I against the draft on ConPTY and expose a literal `[I` prefix.
void sendAgentDraftPasteContent(settings, ptyId, startupDraftPrompt, async (data) => {
const accepted = await writeTerminalPastePtyInput(transport, data)
if (accepted && !startupDraftInputRecorded) {
// Why: this transport write bypasses xterm's user-input signal; keep
// the composed draft from being discarded by later hibernation.
startupDraftInputRecorded = true
recordTerminalInputForHibernation()
}
return accepted
})
.catch(() => false)
.finally(() => {
startupDraftPasteInFlight = false

View File

@ -16,10 +16,13 @@ const AGENT_DRAFT_PASTE_ESCAPE_CODE_POINT = 0x1b
const AGENT_DRAFT_PASTE_INERT_ESCAPE_CODE_POINT = 0x241b
const AGENT_DRAFT_PASTE_INERT_ESCAPE = '\u241b'
export type AgentDraftPtyInputWriter = (data: string) => boolean | Promise<boolean>
export async function sendAgentDraftPasteContent(
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
ptyId: string,
content: string
content: string,
writePty?: AgentDraftPtyInputWriter
): Promise<boolean> {
if (content.length > AGENT_DRAFT_PASTE_MAX_BYTES) {
return false
@ -29,10 +32,11 @@ export async function sendAgentDraftPasteContent(
stopAfterBytes: AGENT_DRAFT_PASTE_DIRECT_MAX_BYTES
})
if (!directMeasurement.exceededLimit) {
return await sendRuntimePtyInputVerified(
return await writeAgentDraftPtyInput(
settings,
ptyId,
[BRACKETED_PASTE_START, sanitizeTerminalPasteText(content), BRACKETED_PASTE_END].join('')
[BRACKETED_PASTE_START, sanitizeTerminalPasteText(content), BRACKETED_PASTE_END].join(''),
writePty
)
}
@ -46,16 +50,16 @@ export async function sendAgentDraftPasteContent(
for (const chunk of iterateAgentDraftPasteContentChunks(content)) {
let accepted = false
try {
accepted = await sendRuntimePtyInputVerified(settings, ptyId, chunk)
accepted = await writeAgentDraftPtyInput(settings, ptyId, chunk, writePty)
} catch {
if (bracketedPasteOpen && chunk !== BRACKETED_PASTE_END) {
await closeAgentDraftBracketedPaste(settings, ptyId)
await closeAgentDraftBracketedPaste(settings, ptyId, writePty)
}
return false
}
if (!accepted) {
if (bracketedPasteOpen && chunk !== BRACKETED_PASTE_END) {
await closeAgentDraftBracketedPaste(settings, ptyId)
await closeAgentDraftBracketedPaste(settings, ptyId, writePty)
}
return false
}
@ -182,14 +186,24 @@ function yieldToAgentDraftPastePreflight(): Promise<void> {
return new Promise((resolve) => globalThis.setTimeout(resolve, 0))
}
async function writeAgentDraftPtyInput(
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
ptyId: string,
data: string,
writePty?: AgentDraftPtyInputWriter
): Promise<boolean> {
return writePty ? await writePty(data) : await sendRuntimePtyInputVerified(settings, ptyId, data)
}
async function closeAgentDraftBracketedPaste(
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
ptyId: string
ptyId: string,
writePty?: AgentDraftPtyInputWriter
): Promise<void> {
try {
// Why: once the opener reached the PTY, a failed content chunk should not
// leave the target TUI in bracketed-paste mode.
await sendRuntimePtyInputVerified(settings, ptyId, BRACKETED_PASTE_END)
await writeAgentDraftPtyInput(settings, ptyId, BRACKETED_PASTE_END, writePty)
} catch {
// The original write already failed; callers only need the paste to fail closed.
}