Add conversation view for Claude
This commit is contained in:
@@ -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) {
|
||||
<div key={contentIndex} className="mt-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-4 h-4 flex items-center justify-center">
|
||||
{content.run?.status && (
|
||||
{content.run?.status ? (
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${getToolStatusColor(
|
||||
content.run.status
|
||||
)}`}
|
||||
/>
|
||||
) : content.is_error ? (
|
||||
<div className="w-2 h-2 rounded-full bg-red-500" />
|
||||
) : (
|
||||
<div className="w-2 h-2 rounded-full bg-green-500" />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
@@ -618,12 +701,24 @@ export function ConversationViewer({ jsonlOutput }: ConversationViewerProps) {
|
||||
({safeRenderString(content.run.status)})
|
||||
</span>
|
||||
)}
|
||||
{content.is_error && (
|
||||
<span className="text-xs text-red-500">
|
||||
(Error)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Amp format result */}
|
||||
{content.run?.result && (
|
||||
<pre className="text-xs bg-muted/50 p-2 rounded overflow-x-auto max-h-32">
|
||||
{safeRenderString(content.run.result)}
|
||||
</pre>
|
||||
)}
|
||||
{/* Claude format result */}
|
||||
{content.content && !content.run && (
|
||||
<pre className="text-xs bg-muted/50 p-2 rounded overflow-x-auto max-h-32">
|
||||
{safeRenderString(content.content)}
|
||||
</pre>
|
||||
)}
|
||||
{content.run?.toAllow && (
|
||||
<div className="mt-2">
|
||||
<p className="text-xs text-muted-foreground mb-1">
|
||||
|
||||
@@ -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({
|
||||
<Card className="">
|
||||
<CardContent className="p-3">
|
||||
<div className="space-y-3">
|
||||
{/* View mode toggle for Amp executor with valid JSONL */}
|
||||
{isAmpExecutor && isValidJsonl && hasStdout && (
|
||||
{/* View mode toggle for executors with valid JSONL */}
|
||||
{isValidJsonl && hasStdout && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{executor} output
|
||||
</Badge>
|
||||
{jsonlFormat && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{jsonlFormat} format
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
@@ -98,7 +136,7 @@ export function ExecutionOutputViewer({
|
||||
{/* Output content */}
|
||||
{hasStdout && (
|
||||
<div>
|
||||
{isAmpExecutor && isValidJsonl && viewMode === "conversation" ? (
|
||||
{isValidJsonl && viewMode === "conversation" ? (
|
||||
<ConversationViewer
|
||||
jsonlOutput={executionProcess.stdout || ""}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user