surface pi process errors to web ui

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 20:47:32 +00:00
co-authored by Claude Opus 4.6
parent 25e8aaaea6
commit c972553f22
+52 -26
View File
@@ -346,8 +346,8 @@ export async function spawnPi(
if (!line.trim()) continue; if (!line.trim()) continue;
try { try {
const event = JSON.parse(line) as Record<string, unknown>; const event = JSON.parse(line) as Record<string, unknown>;
const piEvent = parsePiEvent(event, streamBuffer); const piEvents = parsePiEvent(event, streamBuffer);
if (piEvent) { for (const piEvent of piEvents) {
if (piEvent.type === 'delta') { if (piEvent.type === 'delta') {
streamBuffer += piEvent.text; streamBuffer += piEvent.text;
} else if (piEvent.type === 'text' || piEvent.type === 'tool:start') { } else if (piEvent.type === 'text' || piEvent.type === 'tool:start') {
@@ -404,37 +404,60 @@ export async function spawnPi(
return proc; return proc;
} }
function parsePiEvent(event: Record<string, unknown>, currentStreamBuffer: string): PiEvent | null { function parseErrorMessage(raw: string): string {
try {
const parsed = JSON.parse(raw.replace(/^\d+\s*/, ''));
const inner = parsed?.error;
if (inner?.message) return inner.message;
} catch {
// not JSON
}
return raw;
}
function extractMessageError(msg: Record<string, unknown>): string | null {
if (msg.stopReason !== 'error') return null;
const raw = msg.errorMessage as string | undefined;
if (!raw) return null;
return parseErrorMessage(raw);
}
function parsePiEvent(event: Record<string, unknown>, currentStreamBuffer: string): PiEvent[] {
const type = event.type as string; const type = event.type as string;
// Handle response (success/failure for commands) // Handle response (success/failure for commands)
if (type === 'response') { if (type === 'response') {
if (event.command === 'prompt' && !event.success) { if (event.command === 'prompt' && !event.success) {
const errorMsg = (event.error as string) ?? 'Prompt failed'; const errorMsg = (event.error as string) ?? 'Prompt failed';
return { type: 'error', message: errorMsg }; return [{ type: 'error', message: errorMsg }];
} }
return null; return [];
} }
switch (type) { switch (type) {
case 'agent_start': case 'agent_start':
// No event to emit, just resets state return [];
return null;
case 'message_update': { case 'message_update': {
const ame = event.assistantMessageEvent as Record<string, unknown> | undefined; const ame = event.assistantMessageEvent as Record<string, unknown> | undefined;
if (ame?.type === 'text_delta') { if (ame?.type === 'text_delta') {
const delta = ame.delta as string; const delta = ame.delta as string;
return { type: 'delta', text: delta }; return [{ type: 'delta', text: delta }];
} }
return null; return [];
} }
case 'message_end': { case 'message_end': {
const events: PiEvent[] = [];
if (currentStreamBuffer) { if (currentStreamBuffer) {
return { type: 'text', text: currentStreamBuffer }; events.push({ type: 'text', text: currentStreamBuffer });
} }
return null; const msg = event.message as Record<string, unknown> | undefined;
if (msg) {
const errorText = extractMessageError(msg);
if (errorText) events.push({ type: 'error', message: errorText });
}
return events;
} }
case 'tool_execution_start': { case 'tool_execution_start': {
@@ -442,12 +465,12 @@ function parsePiEvent(event: Record<string, unknown>, currentStreamBuffer: strin
const toolName = (event.toolName as string) ?? 'unknown'; const toolName = (event.toolName as string) ?? 'unknown';
const args = (event.args as Record<string, unknown>) ?? {}; const args = (event.args as Record<string, unknown>) ?? {};
return { return [{
type: 'tool:start', type: 'tool:start',
toolCallId, toolCallId,
toolName, toolName,
toolInput: args, toolInput: args,
}; }];
} }
case 'tool_execution_end': { case 'tool_execution_end': {
@@ -466,37 +489,40 @@ function parsePiEvent(event: Record<string, unknown>, currentStreamBuffer: strin
const isError = (event.isError as boolean) ?? (resultObj?.isError as boolean) ?? false; const isError = (event.isError as boolean) ?? (resultObj?.isError as boolean) ?? false;
const output = result != null ? (typeof result === 'string' ? result : JSON.stringify(result)) : ''; const output = result != null ? (typeof result === 'string' ? result : JSON.stringify(result)) : '';
return { return [{
type: 'tool:result', type: 'tool:result',
toolCallId, toolCallId,
output, output,
isError, isError,
}; }];
} }
case 'agent_end': { case 'agent_end': {
const cost: MessageCost = { inputTokens: 0, outputTokens: 0, totalUSD: 0 }; const cost: MessageCost = { inputTokens: 0, outputTokens: 0, totalUSD: 0 };
const events: PiEvent[] = [];
const messages = event.messages as Array<Record<string, unknown>> | undefined; const messages = event.messages as Array<Record<string, unknown>> | undefined;
if (messages) { if (messages) {
for (const msg of messages) { for (const msg of messages) {
const usage = msg.usage as Record<string, unknown> | undefined; const usage = msg.usage as Record<string, unknown> | undefined;
if (!usage) continue; if (usage) {
cost.inputTokens += (usage.input as number) ?? 0; cost.inputTokens += (usage.input as number) ?? 0;
cost.outputTokens += (usage.output as number) ?? 0; cost.outputTokens += (usage.output as number) ?? 0;
const usageCost = usage.cost as Record<string, unknown> | undefined; const usageCost = usage.cost as Record<string, unknown> | undefined;
if (usageCost) cost.totalUSD += (usageCost.total as number) ?? 0; if (usageCost) cost.totalUSD += (usageCost.total as number) ?? 0;
}
const errorText = extractMessageError(msg);
if (errorText) events.push({ type: 'error', message: errorText });
} }
} }
return { type: 'result', cost }; events.push({ type: 'result', cost });
return events;
} }
case 'extension_ui_request': { case 'extension_ui_request':
// Will be handled separately return [];
return null;
}
default: default:
return null; return [];
} }
} }