diff --git a/frontend/src/components/tasks/ConversationViewer.tsx b/frontend/src/components/tasks/ConversationViewer.tsx index aad15e16..084d6e7f 100644 --- a/frontend/src/components/tasks/ConversationViewer.tsx +++ b/frontend/src/components/tasks/ConversationViewer.tsx @@ -17,6 +17,7 @@ import { interface JSONLLine { type: string; threadID?: string; + // Amp format messages?: [ number, { @@ -63,7 +64,31 @@ interface JSONLLine { state?: string; tool?: string; command?: string; - message?: string; + // Claude format + message?: { + role: "user" | "assistant" | "system"; + content: Array<{ + type: "text" | "tool_use" | "tool_result"; + text?: string; + id?: string; + name?: string; + input?: any; + tool_use_id?: string; + content?: any; + is_error?: boolean; + }> | string; + }; + // Tool rejection message (string format) + rejectionMessage?: string; + usage?: { + input_tokens: number; + output_tokens: number; + cache_creation_input_tokens?: number; + cache_read_input_tokens?: number; + }; + result?: any; + duration_ms?: number; + total_cost_usd?: number; error?: string; // For parse errors } @@ -89,6 +114,21 @@ const isValidMessage = (data: any): boolean => { ); }; +const isValidClaudeMessage = (data: any): boolean => { + return ( + typeof data.role === "string" && + (typeof data.content === "string" || + (Array.isArray(data.content) && + data.content.every( + (item: any) => + typeof item.type === "string" && + (item.type !== "text" || typeof item.text === "string") && + (item.type !== "tool_use" || typeof item.name === "string") && + (item.type !== "tool_result" || typeof item.content !== "undefined") + ))) + ); +}; + const isValidTokenUsage = (data: any): boolean => { return ( data && @@ -97,11 +137,19 @@ const isValidTokenUsage = (data: any): boolean => { ); }; +const isValidClaudeUsage = (data: any): boolean => { + return ( + data && + typeof data.input_tokens === "number" && + typeof data.output_tokens === "number" + ); +}; + const isValidToolRejection = (data: any): boolean => { return ( typeof data.tool === "string" && typeof data.command === "string" && - typeof data.message === "string" + (typeof data.message === "string" || typeof data.rejectionMessage === "string") ); }; @@ -200,19 +248,50 @@ export function ConversationViewer({ jsonlOutput }: ConversationViewerProps) { isValidMessagesLine(line) && line.messages ) { + // Amp format for (const [messageIndex, message] of line.messages) { items.push({ type: "message", - ...message, + role: message.role, + content: message.content, + timestamp: message.meta?.sentAt, messageIndex, lineIndex: line._lineIndex, }); } + } else if ( + (line.type === "user" || line.type === "assistant" || line.type === "system") && + line.message && + isValidClaudeMessage(line.message) + ) { + // Claude format + const content = typeof line.message.content === "string" + ? [{ type: "text", text: line.message.content }] + : line.message.content; + + items.push({ + type: "message", + role: line.message.role === "system" ? "assistant" : line.message.role, + content: content, + lineIndex: line._lineIndex, + }); + } else if ( + line.type === "result" && + line.usage && + isValidClaudeUsage(line.usage) + ) { + // Claude usage info + tokenUsages.push({ + used: line.usage.input_tokens + line.usage.output_tokens, + maxAvailable: line.usage.input_tokens + line.usage.output_tokens + 100000, // Approximate + lineIndex: line._lineIndex, + }); } else if ( line.type === "token-usage" && line.tokenUsage && isValidTokenUsage(line.tokenUsage) ) { + // Amp format tokenUsages.push({ used: line.tokenUsage.used, maxAvailable: line.tokenUsage.maxAvailable, @@ -231,7 +310,7 @@ export function ConversationViewer({ jsonlOutput }: ConversationViewerProps) { type: "tool-rejection", tool: line.tool, command: line.command, - message: line.message, + message: typeof line.message === "string" ? line.message : line.rejectionMessage || "Tool rejected", lineIndex: line._lineIndex, }); } else { @@ -602,12 +681,16 @@ export function ConversationViewer({ jsonlOutput }: ConversationViewerProps) {
- {content.run?.status && ( + {content.run?.status ? (
+ ) : content.is_error ? ( +
+ ) : ( +
)}
@@ -618,12 +701,24 @@ export function ConversationViewer({ jsonlOutput }: ConversationViewerProps) { ({safeRenderString(content.run.status)}) )} + {content.is_error && ( + + (Error) + + )}
+ {/* Amp format result */} {content.run?.result && (
                                 {safeRenderString(content.run.result)}
                               
)} + {/* Claude format result */} + {content.content && !content.run && ( +
+                                {safeRenderString(content.content)}
+                              
+ )} {content.run?.toAllow && (

diff --git a/frontend/src/components/tasks/ExecutionOutputViewer.tsx b/frontend/src/components/tasks/ExecutionOutputViewer.tsx index 7cda06eb..2f32d0f2 100644 --- a/frontend/src/components/tasks/ExecutionOutputViewer.tsx +++ b/frontend/src/components/tasks/ExecutionOutputViewer.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from "react"; +import { useState, useMemo, useEffect } from "react"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -15,27 +15,28 @@ export function ExecutionOutputViewer({ executionProcess, executor, }: ExecutionOutputViewerProps) { - const [viewMode, setViewMode] = useState<"conversation" | "raw">( - executor === "amp" ? "conversation" : "raw" - ); + const [viewMode, setViewMode] = useState<"conversation" | "raw">("raw"); const isAmpExecutor = executor === "amp"; + const isClaudeExecutor = executor === "claude"; const hasStdout = !!executionProcess.stdout; const hasStderr = !!executionProcess.stderr; - // Check if stdout looks like JSONL (for Amp executor) - const isValidJsonl = useMemo(() => { - if (!isAmpExecutor || !executionProcess.stdout) return false; + // Check if stdout looks like JSONL (for Amp or Claude executor) + const { isValidJsonl, jsonlFormat } = useMemo(() => { + if ((!isAmpExecutor && !isClaudeExecutor) || !executionProcess.stdout) { + return { isValidJsonl: false, jsonlFormat: null }; + } try { const lines = executionProcess.stdout .split("\n") .filter((line) => line.trim()); - if (lines.length === 0) return false; + if (lines.length === 0) return { isValidJsonl: false, jsonlFormat: null }; // Try to parse at least the first few lines as JSON const testLines = lines.slice(0, Math.min(3, lines.length)); - return testLines.every((line) => { + const allValid = testLines.every((line) => { try { JSON.parse(line); return true; @@ -43,10 +44,42 @@ export function ExecutionOutputViewer({ return false; } }); + + if (!allValid) return { isValidJsonl: false, jsonlFormat: null }; + + // Detect format by checking for Amp vs Claude structure + let hasAmpFormat = false; + let hasClaudeFormat = false; + + for (const line of testLines) { + try { + const parsed = JSON.parse(line); + if (parsed.type === "messages" || parsed.type === "token-usage") { + hasAmpFormat = true; + } + if (parsed.type === "user" || parsed.type === "assistant" || parsed.type === "system" || parsed.type === "result") { + hasClaudeFormat = true; + } + } catch { + // Skip invalid lines + } + } + + return { + isValidJsonl: true, + jsonlFormat: hasAmpFormat ? "amp" : hasClaudeFormat ? "claude" : "unknown" + }; } catch { - return false; + return { isValidJsonl: false, jsonlFormat: null }; } - }, [isAmpExecutor, executionProcess.stdout]); + }, [isAmpExecutor, isClaudeExecutor, executionProcess.stdout]); + + // Set initial view mode based on JSONL detection + useEffect(() => { + if (isValidJsonl) { + setViewMode("conversation"); + } + }, [isValidJsonl]); if (!hasStdout && !hasStderr) { return ( @@ -64,13 +97,18 @@ export function ExecutionOutputViewer({

- {/* View mode toggle for Amp executor with valid JSONL */} - {isAmpExecutor && isValidJsonl && hasStdout && ( + {/* View mode toggle for executors with valid JSONL */} + {isValidJsonl && hasStdout && (
{executor} output + {jsonlFormat && ( + + {jsonlFormat} format + + )}