Auto-mode sessions are long-running (minutes to hours) with their own internal per-unit timeout via auto-supervisor. The 300s overall timeout was killing active sessions mid-execution, triggering wasteful restart cycles. Changes: - Disable overall timeout for auto-mode when using the default 300s (user can still set --timeout explicitly, including --timeout 0) - Guard timeout timer creation for null when timeout is 0 - Cancel overall timeout when new-milestone --auto chains into auto-mode - Fix headless auto-responder to pick "Force start" for lock-guard prompts instead of "View status" (which silently blocked auto-mode) - Allow --timeout 0 to explicitly disable timeout for any command Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
153 lines
4.6 KiB
TypeScript
153 lines
4.6 KiB
TypeScript
/**
|
|
* Headless UI Handling — auto-response, progress formatting, and supervised stdin
|
|
*
|
|
* Handles extension UI requests (auto-responding in headless mode),
|
|
* formats progress events for stderr output, and reads orchestrator
|
|
* commands from stdin in supervised mode.
|
|
*/
|
|
|
|
import type { Readable } from 'node:stream'
|
|
|
|
import { RpcClient, attachJsonlLineReader, serializeJsonLine } from '@gsd/pi-coding-agent'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
interface ExtensionUIRequest {
|
|
type: 'extension_ui_request'
|
|
id: string
|
|
method: string
|
|
title?: string
|
|
options?: string[]
|
|
message?: string
|
|
prefill?: string
|
|
timeout?: number
|
|
[key: string]: unknown
|
|
}
|
|
|
|
export type { ExtensionUIRequest }
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Extension UI Auto-Responder
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function handleExtensionUIRequest(
|
|
event: ExtensionUIRequest,
|
|
writeToStdin: (data: string) => void,
|
|
): void {
|
|
const { id, method } = event
|
|
let response: Record<string, unknown>
|
|
|
|
switch (method) {
|
|
case 'select': {
|
|
// Lock-guard prompts list "View status" first, but headless needs "Force start"
|
|
// to proceed. Detect by title and pick the force option.
|
|
const title = String(event.title ?? '')
|
|
let selected = event.options?.[0] ?? ''
|
|
if (title.includes('Auto-mode is running') && event.options) {
|
|
const forceOption = event.options.find(o => o.toLowerCase().includes('force start'))
|
|
if (forceOption) selected = forceOption
|
|
}
|
|
response = { type: 'extension_ui_response', id, value: selected }
|
|
break
|
|
}
|
|
case 'confirm':
|
|
response = { type: 'extension_ui_response', id, confirmed: true }
|
|
break
|
|
case 'input':
|
|
response = { type: 'extension_ui_response', id, value: '' }
|
|
break
|
|
case 'editor':
|
|
response = { type: 'extension_ui_response', id, value: event.prefill ?? '' }
|
|
break
|
|
case 'notify':
|
|
case 'setStatus':
|
|
case 'setWidget':
|
|
case 'setTitle':
|
|
case 'set_editor_text':
|
|
response = { type: 'extension_ui_response', id, value: '' }
|
|
break
|
|
default:
|
|
process.stderr.write(`[headless] Warning: unknown extension_ui_request method "${method}", cancelling\n`)
|
|
response = { type: 'extension_ui_response', id, cancelled: true }
|
|
break
|
|
}
|
|
|
|
writeToStdin(serializeJsonLine(response))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Progress Formatter
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function formatProgress(event: Record<string, unknown>, verbose: boolean): string | null {
|
|
const type = String(event.type ?? '')
|
|
|
|
switch (type) {
|
|
case 'tool_execution_start':
|
|
if (verbose) return ` [tool] ${event.toolName ?? 'unknown'}`
|
|
return null
|
|
|
|
case 'agent_start':
|
|
return '[agent] Session started'
|
|
|
|
case 'agent_end':
|
|
return '[agent] Session ended'
|
|
|
|
case 'extension_ui_request':
|
|
if (event.method === 'notify') {
|
|
return `[gsd] ${event.message ?? ''}`
|
|
}
|
|
if (event.method === 'setStatus') {
|
|
return `[status] ${event.message ?? ''}`
|
|
}
|
|
return null
|
|
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Supervised Stdin Reader
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function startSupervisedStdinReader(
|
|
stdinWriter: (data: string) => void,
|
|
client: RpcClient,
|
|
onResponse: (id: string) => void,
|
|
): () => void {
|
|
return attachJsonlLineReader(process.stdin as Readable, (line) => {
|
|
let msg: Record<string, unknown>
|
|
try {
|
|
msg = JSON.parse(line)
|
|
} catch {
|
|
process.stderr.write(`[headless] Warning: invalid JSON from orchestrator stdin, skipping\n`)
|
|
return
|
|
}
|
|
|
|
const type = String(msg.type ?? '')
|
|
|
|
switch (type) {
|
|
case 'extension_ui_response':
|
|
stdinWriter(line + '\n')
|
|
if (typeof msg.id === 'string') {
|
|
onResponse(msg.id)
|
|
}
|
|
break
|
|
case 'prompt':
|
|
client.prompt(String(msg.message ?? ''))
|
|
break
|
|
case 'steer':
|
|
client.steer(String(msg.message ?? ''))
|
|
break
|
|
case 'follow_up':
|
|
client.followUp(String(msg.message ?? ''))
|
|
break
|
|
default:
|
|
process.stderr.write(`[headless] Warning: unknown message type "${type}" from orchestrator stdin\n`)
|
|
break
|
|
}
|
|
})
|
|
}
|