fix(codex): improve web rendering for Codex events (#544)

* test(codex): add web event rendering harness

* fix(codex): surface plan updates in web

* fix(codex): render MCP tool calls in web

* fix(codex): improve terminal and context display

* fix(codex): format token usage events

* fix(codex): show status context in web

* fix(codex): preserve tool result errors
This commit is contained in:
CoColate
2026-04-29 17:22:45 +08:00
committed by GitHub
parent a612be50d6
commit e76738aa5a
27 changed files with 1138 additions and 15 deletions
+86 -2
View File
@@ -123,6 +123,50 @@ function extractReasoningText(item: Record<string, unknown>): string | null {
return null;
}
function normalizePlanStatus(value: unknown): 'pending' | 'in_progress' | 'completed' {
const raw = typeof value === 'string' ? value.trim().toLowerCase().replace(/[\s-]/g, '_') : '';
if (raw === 'completed' || raw === 'complete' || raw === 'done') return 'completed';
if (raw === 'in_progress' || raw === 'inprogress' || raw === 'active' || raw === 'running') return 'in_progress';
return 'pending';
}
function extractPlanEntries(value: unknown): Array<{ step: string; status: 'pending' | 'in_progress' | 'completed' }> {
const record = asRecord(value);
const entries = Array.isArray(value)
? value
: Array.isArray(record?.plan)
? record.plan
: Array.isArray(record?.items)
? record.items
: Array.isArray(record?.steps)
? record.steps
: [];
const plan: Array<{ step: string; status: 'pending' | 'in_progress' | 'completed' }> = [];
for (const entry of entries) {
if (typeof entry === 'string') {
plan.push({ step: entry, status: 'pending' });
continue;
}
const item = asRecord(entry);
if (!item) continue;
const step = asString(item.step ?? item.content ?? item.text ?? item.title ?? item.description);
if (!step) continue;
plan.push({
step,
status: normalizePlanStatus(item.status ?? item.state)
});
}
return plan;
}
function extractPlanUpdate(params: Record<string, unknown>): ConvertedEvent[] {
const plan = extractPlanEntries(
params.plan ?? params.update ?? params.items ?? params.steps ?? params
);
return plan.length > 0 ? [{ type: 'plan_update', plan }] : [];
}
export class AppServerEventConverter {
private readonly agentMessageBuffers = new Map<string, string>();
private readonly reasoningBuffers = new Map<string, string>();
@@ -228,10 +272,13 @@ export class AppServerEventConverter {
return error ? [{ type: 'task_failed', error }] : [];
}
if (msgType === 'plan_update') {
return extractPlanUpdate(msg);
}
if (
msgType === 'mcp_startup_update' ||
msgType === 'mcp_startup_complete' ||
msgType === 'plan_update' ||
msgType === 'skills_update_available' ||
msgType === 'stream_error' ||
msgType === 'warning' ||
@@ -253,7 +300,11 @@ export class AppServerEventConverter {
return this.handleWrappedCodexEvent(paramsRecord) ?? events;
}
if (method === 'account/rateLimits/updated' || method === 'turn/plan/updated' || method === 'thread/compacted') {
if (method === 'turn/plan/updated') {
return extractPlanUpdate(paramsRecord);
}
if (method === 'account/rateLimits/updated' || method === 'thread/compacted') {
return events;
}
@@ -484,6 +535,39 @@ export class AppServerEventConverter {
return events;
}
if (itemType === 'mcptoolcall') {
const server = asString(item.server ?? item.serverName ?? item.server_name);
const tool = asString(item.tool ?? item.toolName ?? item.tool_name ?? item.name);
const input = item.arguments ?? item.input ?? {};
if (method === 'item/started') {
events.push({
type: 'mcp_tool_call_begin',
call_id: itemId,
server,
tool,
invocation: {
server,
tool,
arguments: input
}
});
}
if (method === 'item/completed') {
const error = item.error;
events.push({
type: 'mcp_tool_call_end',
call_id: itemId,
server,
tool,
result: error ? { Err: error } : item.result
});
}
return events;
}
if (itemType === 'filechange') {
if (method === 'item/started') {
const changes = extractChanges(item.changes ?? item.change ?? item.diff);