feat: headless text mode shows tool calls + skip UAT pause in headless
Text mode observability: - Tool calls now always visible with summarized args (path, command, pattern) - Tool errors surfaced even in non-verbose mode - Cost updates shown periodically ($0.05 4.2k in / 1.1k out) - Empty [status] lines suppressed (setStatus/setWidget are TUI-only) - Empty notify messages suppressed UAT pause skip: - Set GSD_HEADLESS=1 env var when spawning RPC child process - auto-dispatch checks GSD_HEADLESS and skips pauseAfterDispatch for UAT - Headless runs no longer stall waiting for human UAT verification
This commit is contained in:
parent
98eb2ae802
commit
8e1d523f4e
3 changed files with 73 additions and 7 deletions
|
|
@ -82,9 +82,37 @@ export function formatProgress(event: Record<string, unknown>, verbose: boolean)
|
|||
const type = String(event.type ?? '')
|
||||
|
||||
switch (type) {
|
||||
case 'tool_execution_start':
|
||||
if (verbose) return ` [tool] ${event.toolName ?? 'unknown'}`
|
||||
case 'tool_execution_start': {
|
||||
const name = String(event.toolName ?? 'unknown')
|
||||
const summary = summarizeToolArgs(name, event.args as Record<string, unknown> | undefined)
|
||||
return summary ? ` [tool] ${name} ${summary}` : ` [tool] ${name}`
|
||||
}
|
||||
|
||||
case 'tool_execution_end': {
|
||||
if (verbose) {
|
||||
const name = String(event.toolName ?? 'unknown')
|
||||
const isError = Boolean(event.isError)
|
||||
return isError ? ` [tool] ${name} ✗ error` : null
|
||||
}
|
||||
// In non-verbose, only surface errors
|
||||
if (event.isError) {
|
||||
const name = String(event.toolName ?? 'unknown')
|
||||
return ` [tool] ${name} ✗ error`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
case 'cost_update': {
|
||||
const cumCost = event.cumulativeCost as Record<string, unknown> | undefined
|
||||
const costUsd = Number(cumCost?.costUsd ?? 0)
|
||||
if (costUsd > 0) {
|
||||
const tokens = event.tokens as Record<string, number> | undefined
|
||||
const inK = tokens ? (tokens.input / 1000).toFixed(1) : '?'
|
||||
const outK = tokens ? (tokens.output / 1000).toFixed(1) : '?'
|
||||
return ` [cost] $${costUsd.toFixed(4)} (${inK}k in / ${outK}k out)`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
case 'agent_start':
|
||||
return '[agent] Session started'
|
||||
|
|
@ -94,11 +122,10 @@ export function formatProgress(event: Record<string, unknown>, verbose: boolean)
|
|||
|
||||
case 'extension_ui_request':
|
||||
if (event.method === 'notify') {
|
||||
return `[gsd] ${event.message ?? ''}`
|
||||
}
|
||||
if (event.method === 'setStatus') {
|
||||
return `[status] ${event.message ?? ''}`
|
||||
const msg = String(event.message ?? '')
|
||||
return msg ? `[gsd] ${msg}` : null
|
||||
}
|
||||
// setStatus / setWidget are TUI-specific — suppress in text mode
|
||||
return null
|
||||
|
||||
default:
|
||||
|
|
@ -106,6 +133,43 @@ export function formatProgress(event: Record<string, unknown>, verbose: boolean)
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a short summary from tool arguments for display.
|
||||
* Returns null if nothing useful can be summarized.
|
||||
*/
|
||||
function summarizeToolArgs(toolName: string, args: Record<string, unknown> | undefined): string | null {
|
||||
if (!args) return null
|
||||
|
||||
switch (toolName) {
|
||||
case 'Read':
|
||||
case 'read':
|
||||
return args.path ? String(args.path) : null
|
||||
case 'Write':
|
||||
case 'write':
|
||||
return args.path ? String(args.path) : null
|
||||
case 'Edit':
|
||||
case 'edit':
|
||||
return args.path ? String(args.path) : null
|
||||
case 'Bash':
|
||||
case 'bash': {
|
||||
const cmd = String(args.command ?? '')
|
||||
return cmd.length > 80 ? cmd.slice(0, 77) + '...' : cmd || null
|
||||
}
|
||||
case 'Grep':
|
||||
case 'grep':
|
||||
return args.pattern ? `/${args.pattern}/` + (args.path ? ` in ${args.path}` : '') : null
|
||||
case 'find':
|
||||
return args.pattern ? String(args.pattern) + (args.path ? ` in ${args.path}` : '') : null
|
||||
case 'lsp':
|
||||
return args.action ? String(args.action) + (args.symbol ? ` ${args.symbol}` : '') : null
|
||||
default: {
|
||||
// For GSD tools, show the first string arg that looks like an ID or path
|
||||
const first = Object.values(args).find(v => typeof v === 'string' && String(v).length < 80)
|
||||
return first ? String(first) : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Supervised Stdin Reader
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -344,6 +344,8 @@ async function runHeadlessOnce(options: HeadlessOptions, restartCount: number):
|
|||
if (injector) {
|
||||
clientOptions.env = injector.getSecretEnvVars()
|
||||
}
|
||||
// Signal headless mode to the GSD extension (skips UAT human pause, etc.)
|
||||
clientOptions.env = { ...(clientOptions.env as Record<string, string> || {}), GSD_HEADLESS: '1' }
|
||||
// Propagate --bare to the child process
|
||||
if (options.bare) {
|
||||
clientOptions.args = [...((clientOptions.args as string[]) || []), '--bare']
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
|
|||
uatContent ?? "",
|
||||
basePath,
|
||||
),
|
||||
pauseAfterDispatch: uatType !== "artifact-driven" && uatType !== "browser-executable" && uatType !== "runtime-executable",
|
||||
pauseAfterDispatch: !process.env.GSD_HEADLESS && uatType !== "artifact-driven" && uatType !== "browser-executable" && uatType !== "runtime-executable",
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue