feat: add structured tool result types for better rendering

Server-side:
- Add StructuredToolResult discriminated union type with command, file_write,
  file_edit, file_read, and generic variants
- Implement buildStructuredToolResult in claude-agent.ts to detect tool types
  and emit properly structured results
- Update codex-agent.ts to emit structured command results

Client-side:
- Add type guard and extraction functions for structured results
- Render tool calls based on result.type when available:
  - command: show command, output, exit code
  - file_write/file_edit: show diff viewer with proper +/- format
  - file_read: show file content
  - generic: show raw JSON
- Fall back to heuristic parsing for backwards compatibility

This fixes file writes showing as "Command: success message" - they now
properly show as diffs with +line additions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-11-28 19:24:52 +00:00
co-authored by Claude
parent a7df671144
commit 38b08fec0c
4 changed files with 479 additions and 10 deletions
@@ -1043,11 +1043,23 @@ class ClaudeAgentSession implements AgentSession {
return undefined;
}
const server = entry?.server ?? block.server ?? "tool";
const tool = entry?.name ?? block.tool_name ?? "tool";
const content = typeof block.content === "string" ? block.content : "";
const input = entry?.input;
// Build structured result based on tool type
const structured = this.buildStructuredToolResult(server, tool, content, input);
if (structured) {
return structured;
}
// Fallback to legacy format
const result: Record<string, unknown> = {};
// SDK tool_result blocks have content as a string
if (typeof block.content === "string" && block.content.length > 0) {
result.result = { output: block.content };
if (content.length > 0) {
result.result = { output: content };
}
// Preserve file changes tracked during tool execution
@@ -1058,6 +1070,86 @@ class ClaudeAgentSession implements AgentSession {
return Object.keys(result).length > 0 ? result : undefined;
}
private buildStructuredToolResult(
server: string,
tool: string,
output: string,
input?: Record<string, unknown> | null
): Record<string, unknown> | undefined {
const normalizedServer = server.toLowerCase();
const normalizedTool = tool.toLowerCase();
// Command execution tools
if (
normalizedServer.includes("bash") ||
normalizedServer.includes("shell") ||
normalizedServer.includes("command") ||
normalizedTool.includes("bash") ||
normalizedTool.includes("shell") ||
normalizedTool.includes("command") ||
(input && (typeof input.command === "string" || Array.isArray(input.command)))
) {
const command = this.extractCommandText(input ?? {}) ?? "command";
return {
type: "command",
command,
output,
cwd: typeof input?.cwd === "string" ? input.cwd : undefined,
};
}
// File write tools (new files or complete replacements)
if (
normalizedTool.includes("write") ||
normalizedTool === "write_file" ||
normalizedTool === "create_file"
) {
if (input && typeof input.file_path === "string") {
return {
type: "file_write",
filePath: input.file_path,
oldContent: "",
newContent: typeof input.content === "string" ? input.content : output,
};
}
}
// File edit/patch tools
if (
normalizedTool.includes("edit") ||
normalizedTool.includes("patch") ||
normalizedTool === "apply_patch" ||
normalizedTool === "apply_diff"
) {
if (input && typeof input.file_path === "string") {
return {
type: "file_edit",
filePath: input.file_path,
diff: typeof input.patch === "string" ? input.patch : typeof input.diff === "string" ? input.diff : undefined,
oldContent: typeof input.old_str === "string" ? input.old_str : undefined,
newContent: typeof input.new_str === "string" ? input.new_str : undefined,
};
}
}
// File read tools
if (
normalizedTool.includes("read") ||
normalizedTool === "read_file" ||
normalizedTool === "view_file"
) {
if (input && typeof input.file_path === "string") {
return {
type: "file_read",
filePath: input.file_path,
content: output,
};
}
}
return undefined;
}
private mapPartialEvent(event: SDKPartialAssistantMessage["event"]): AgentTimelineItem[] {
if (event.type === "content_block_start" && (event.content_block as ClaudeContentChunk | undefined)?.type === "tool_use") {
const block = event.content_block as ClaudeContentChunk;
@@ -817,7 +817,12 @@ class CodexAgentSession implements AgentSession {
return { type: "assistant_message", text: item.text };
case "reasoning":
return { type: "reasoning", text: item.text };
case "command_execution":
case "command_execution": {
const output = (item as any)?.output;
const cwd = (item as any)?.cwd;
const commandValue = item.command;
const command = typeof commandValue === "string" ? commandValue :
Array.isArray(commandValue) ? (commandValue as string[]).join(" ") : "command";
return createToolCallTimelineItem({
server: "command",
tool: "shell",
@@ -825,10 +830,16 @@ class CodexAgentSession implements AgentSession {
callId: (item as any)?.call_id,
displayName: buildCommandDisplayName(item.command),
kind: "execute",
input: { command: item.command, cwd: (item as any)?.cwd },
output: (item as any)?.output,
input: { command: item.command, cwd },
output: typeof output === "string" ? {
type: "command",
command,
output,
cwd,
} : undefined,
error: (item as any)?.error,
});
}
case "file_change": {
const files = item.changes.map((change) => ({ path: change.path, kind: change.kind }));
return createToolCallTimelineItem({
@@ -1100,6 +1111,16 @@ function finalizeRolloutFunctionCall(
const result = safeJsonParse<CommandExecutionResult>(payload.output);
const exitCode = result?.metadata?.exit_code;
const status = exitCode === undefined || exitCode === 0 ? "completed" : "failed";
// Build structured command output
const output = result?.stdout ? {
type: "command" as const,
command: command.command,
output: result.stdout,
exitCode,
cwd: command.cwd,
} : result;
events.push({
type: "timeline",
provider: "codex",
@@ -1111,7 +1132,7 @@ function finalizeRolloutFunctionCall(
displayName: buildCommandDisplayName(command.command),
kind: "execute",
input: { command: command.command, cwd: command.cwd },
output: result,
output,
}),
});
commandCalls.delete(payload.call_id);
+9
View File
@@ -115,6 +115,15 @@ export const AgentPermissionRequestPayloadSchema: z.ZodType<AgentPermissionReque
metadata: z.record(z.unknown()).optional(),
});
// Structured tool result types for better client rendering
// These types define the structure of the `output` field in tool_call timeline items
export type StructuredToolResult =
| { type: "command"; command: string; output: string; exitCode?: number; cwd?: string }
| { type: "file_write"; filePath: string; oldContent: string; newContent: string }
| { type: "file_edit"; filePath: string; diff?: string; oldContent?: string; newContent?: string }
| { type: "file_read"; filePath: string; content: string }
| { type: "generic"; data: unknown };
export const AgentTimelineItemPayloadSchema: z.ZodType<AgentTimelineItem> =
z.discriminatedUnion("type", [
z.object({