fix(orchestration): wait for Claude composer render (#14342)

This commit is contained in:
Jinwoo Hong
2026-08-13 15:04:32 -07:00
committed by GitHub
parent a12f8be144
commit e06cdbb4ee
3 changed files with 269 additions and 9 deletions
+98 -2
View File
@@ -1831,6 +1831,10 @@ const BRACKETED_PASTE_BEGIN = '\x1b[200~'
const BRACKETED_PASTE_END = '\x1b[201~'
const BRACKETED_PASTE_QUIET_MS = 1500
const DRAFT_PASTE_READY_TIMEOUT_MS = 8000
const CLAUDE_AGENT_PROMPT_RENDER_TIMEOUT_MS = 8000
const CLAUDE_AGENT_PROMPT_RENDER_QUIET_MS = 1500
// Why: Claude emits show-cursor while rendering its composer; output must settle afterward.
const CLAUDE_AGENT_PROMPT_RENDER_MARKER = '\x1b[?25h'
const MOBILE_TERMINAL_SURFACE_TIMEOUT_MS = 10_000
// Why: the split already failed; the caller waits on this teardown only to learn whether the
// fallback kill is needed, so keep it short — an unreachable host must not stall the rejection.
@@ -17438,19 +17442,24 @@ export class OrcaRuntimeService {
suffixFailureError?: string
} = {}
): Promise<void> {
const renderGate = this.createClaudeAgentPromptRenderGate(ptyId)
let wrotePasteBytes = false
let completedPaste = false
try {
const chunks = iterateTerminalInputChunks(pastePayload)
let chunk = chunks.next()
while (!chunk.done) {
const nextChunk = chunks.next()
await options.beforeWrite?.(ptyId)
if (nextChunk.done) {
renderGate?.arm()
}
const wrote = this.ptyController?.write(ptyId, chunk.value) ?? false
if (!wrote) {
throw new Error('terminal_not_writable')
}
wrotePasteBytes = true
chunk = chunks.next()
chunk = nextChunk
if (!chunk.done) {
await new Promise((resolve) => setTimeout(resolve, 0))
}
@@ -17460,10 +17469,16 @@ export class OrcaRuntimeService {
if (wrotePasteBytes && !completedPaste) {
this.ptyController?.write(ptyId, AGENT_PROMPT_BRACKETED_PASTE_END)
}
renderGate?.dispose()
throw error
}
await new Promise((resolve) => setTimeout(resolve, AGENT_PROMPT_SUBMIT_DELAY_MS))
if (renderGate) {
await renderGate.wait()
renderGate.dispose()
} else {
await new Promise((resolve) => setTimeout(resolve, AGENT_PROMPT_SUBMIT_DELAY_MS))
}
try {
await options.beforeWrite?.(ptyId)
} catch (error) {
@@ -17478,6 +17493,87 @@ export class OrcaRuntimeService {
}
}
private createClaudeAgentPromptRenderGate(ptyId: string): {
arm: () => void
wait: () => Promise<void>
dispose: () => void
} | null {
const pty = this.ptysById.get(ptyId)
if ((pty?.launchAgent ?? pty?.foregroundAgent) !== 'claude') {
return null
}
let armed = false
let observedMarker = false
let settled = false
let markerCarry = ''
let quietTimer: NodeJS.Timeout | null = null
let hardTimer: NodeJS.Timeout | null = null
let resolveRender!: () => void
const rendered = new Promise<void>((resolve) => {
resolveRender = resolve
})
const finish = (): void => {
if (settled) {
return
}
settled = true
if (quietTimer) {
clearTimeout(quietTimer)
quietTimer = null
}
if (hardTimer) {
clearTimeout(hardTimer)
hardTimer = null
}
resolveRender()
}
const armQuietTimer = (): void => {
if (quietTimer) {
clearTimeout(quietTimer)
}
quietTimer = setTimeout(finish, CLAUDE_AGENT_PROMPT_RENDER_QUIET_MS)
}
const unsubscribe = this.subscribeToTerminalData(ptyId, (data) => {
if (!armed || settled) {
return
}
if (!observedMarker) {
const combined = markerCarry + data
markerCarry = combined.slice(-(CLAUDE_AGENT_PROMPT_RENDER_MARKER.length - 1))
if (!combined.includes(CLAUDE_AGENT_PROMPT_RENDER_MARKER)) {
return
}
observedMarker = true
}
armQuietTimer()
})
return {
arm: () => {
armed = true
markerCarry = ''
},
wait: async () => {
if (settled) {
return
}
hardTimer = setTimeout(finish, CLAUDE_AGENT_PROMPT_RENDER_TIMEOUT_MS)
await rendered
},
dispose: () => {
unsubscribe()
if (quietTimer) {
clearTimeout(quietTimer)
quietTimer = null
}
if (hardTimer) {
clearTimeout(hardTimer)
hardTimer = null
}
}
}
}
async waitForTerminal(
handle: string,
options?: {