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) {
{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({