// Shared structured-progress contract for the Activity feature. // // Any capability that wants live progress bars / phase chips (instead of the UI regexing prose) appends // NDJSON lines of this shape to its output/log file, alongside whatever human text it already writes: // // {"job":"dearly-devoted","cap":"split-audiobook","phase":"transcription","status":"running","pct":9,"detail":"0:50:00 / 9:28:24","ts":1730000000000} // {"job":"dearly-devoted","cap":"split-audiobook","phase":"verdict","status":"complete","verdict":"CLEAN","chapters":31,"pieces":52,"ts":...} // // The parser is deliberately tolerant: a line is treated as structured progress iff it JSON-parses to // an object carrying a `phase` or `status`; everything else is passed through as a raw log line. So a // capability can dual-write (human lines + NDJSON) and both render correctly. export type ProgressLine = { job?: string; cap?: string; phase?: string; status?: 'running' | 'complete' | 'failed' | string; pct?: number; detail?: string; ts?: number; [k: string]: unknown; }; export type TailEvent = { kind: 'progress'; progress: ProgressLine } | { kind: 'line'; text: string }; export function parseTailLine(line: string): TailEvent { const trimmed = line.trim(); if (trimmed.startsWith('{') && trimmed.endsWith('}')) { try { const obj = JSON.parse(trimmed) as Record; if (obj && (typeof obj.phase === 'string' || typeof obj.status === 'string')) { return { kind: 'progress', progress: obj as ProgressLine }; } } catch { /* not JSON → fall through to raw line */ } } return { kind: 'line', text: line }; }