fix(web): object tool args crashed React #31 — normalize at derive boundary (argsText); contract scenario replays real-plugin frame shapes end-to-end (73/73)

This commit is contained in:
Raphael Westphal
2026-08-18 16:23:44 +02:00
parent 72c593da18
commit 7f0672f5d8
7 changed files with 239 additions and 13 deletions
+19 -5
View File
@@ -44,6 +44,19 @@ export interface ChatDerivation {
busy: boolean;
}
// The plugin mirrors pi extension events verbatim: tool args arrive as a raw
// JSON object (or string in older frames). Normalizing at this boundary keeps
// the render layer string-only; regression: object args crashed React (#31).
export function argsText(args: unknown): string {
if (typeof args === "string") return args;
if (args === null || args === undefined) return "";
try {
return JSON.stringify(args, null, 2) ?? "";
} catch {
return String(args);
}
}
export function deriveChat(events: EventFrame[]): ChatDerivation {
const messages: ChatMessage[] = [];
const tools = new Map<string, ToolState>();
@@ -57,7 +70,7 @@ export function deriveChat(events: EventFrame[]): ChatDerivation {
tools.set(e.toolCallId, {
id: e.toolCallId,
name: e.toolName ?? "tool",
args: e.args ?? "",
args: argsText(e.args),
running: true,
isError: false,
preview: "",
@@ -148,10 +161,11 @@ export interface TaskDerivation {
const TODO_TOOL = "todo";
const SUBAGENT_TOOL = "subagent";
function parseJson(text: string | undefined | null): unknown {
if (typeof text !== "string" || text.length === 0) return undefined;
function parseJson(raw: unknown): unknown {
if (typeof raw === "object" && raw !== null) return raw; // already-decoded args
if (typeof raw !== "string" || raw.length === 0) return undefined;
try {
return JSON.parse(text) as unknown;
return JSON.parse(raw) as unknown;
} catch {
return undefined;
}
@@ -234,7 +248,7 @@ export function deriveTasks(events: EventFrame[]): TaskDerivation {
runningTools.push({
id: e.toolCallId,
name,
args: e.args ?? "",
args: argsText(e.args),
running: true,
isError: false,
preview: "",