Files
vibe-kanban/frontend/src/components/tasks/ExecutionOutputViewer.tsx

225 lines
6.8 KiB
TypeScript
Raw Normal View History

2025-06-25 09:36:07 +01:00
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';
import { FileText, MessageSquare } from 'lucide-react';
import { ConversationViewer } from './ConversationViewer';
import type { ExecutionProcess, ExecutionProcessStatus } from 'shared/types';
2025-06-21 22:12:32 +01:00
interface ExecutionOutputViewerProps {
executionProcess: ExecutionProcess;
executor?: string;
}
const getExecutionProcessStatusDisplay = (
status: ExecutionProcessStatus
): { label: string; color: string } => {
switch (status) {
2025-06-25 09:36:07 +01:00
case 'running':
return { label: 'Running', color: 'bg-blue-500' };
case 'completed':
return { label: 'Completed', color: 'bg-green-500' };
case 'failed':
return { label: 'Failed', color: 'bg-red-500' };
case 'killed':
return { label: 'Stopped', color: 'bg-gray-500' };
default:
2025-06-25 09:36:07 +01:00
return { label: 'Unknown', color: 'bg-gray-400' };
}
};
2025-06-21 22:12:32 +01:00
export function ExecutionOutputViewer({
executionProcess,
executor,
}: ExecutionOutputViewerProps) {
2025-06-25 09:36:07 +01:00
const [viewMode, setViewMode] = useState<'conversation' | 'raw'>('raw');
2025-06-21 22:12:32 +01:00
2025-06-25 09:36:07 +01:00
const isAmpExecutor = executor === 'amp';
const isClaudeExecutor = executor === 'claude';
2025-06-25 18:23:50 +01:00
const isGeminiExecutor = executor === 'gemini';
2025-06-21 22:12:32 +01:00
const hasStdout = !!executionProcess.stdout;
const hasStderr = !!executionProcess.stderr;
2025-06-25 18:23:50 +01:00
// Check if stdout looks like JSONL (for Amp, Claude, or Gemini executor)
2025-06-22 23:27:39 +01:00
const { isValidJsonl, jsonlFormat } = useMemo(() => {
if (
(!isAmpExecutor && !isClaudeExecutor && !isGeminiExecutor) ||
!executionProcess.stdout
) {
2025-06-22 23:27:39 +01:00
return { isValidJsonl: false, jsonlFormat: null };
}
2025-06-21 22:12:32 +01:00
try {
const lines = executionProcess.stdout
2025-06-25 09:36:07 +01:00
.split('\n')
2025-06-21 22:12:32 +01:00
.filter((line) => line.trim());
2025-06-22 23:27:39 +01:00
if (lines.length === 0) return { isValidJsonl: false, jsonlFormat: null };
2025-06-21 22:12:32 +01:00
// Try to parse at least the first few lines as JSON
const testLines = lines.slice(0, Math.min(3, lines.length));
2025-06-22 23:27:39 +01:00
const allValid = testLines.every((line) => {
2025-06-21 22:12:32 +01:00
try {
JSON.parse(line);
return true;
} catch {
return false;
}
});
2025-06-22 23:27:39 +01:00
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);
2025-06-25 09:36:07 +01:00
if (parsed.type === 'messages' || parsed.type === 'token-usage') {
2025-06-22 23:27:39 +01:00
hasAmpFormat = true;
}
2025-06-25 09:36:07 +01:00
if (
parsed.type === 'user' ||
parsed.type === 'assistant' ||
parsed.type === 'system' ||
parsed.type === 'result'
) {
2025-06-22 23:27:39 +01:00
hasClaudeFormat = true;
}
} catch {
// Skip invalid lines
}
}
return {
isValidJsonl: true,
2025-06-25 09:36:07 +01:00
jsonlFormat: hasAmpFormat
? 'amp'
: hasClaudeFormat
? 'claude'
: 'unknown',
2025-06-22 23:27:39 +01:00
};
2025-06-21 22:12:32 +01:00
} catch {
2025-06-22 23:27:39 +01:00
return { isValidJsonl: false, jsonlFormat: null };
}
}, [
isAmpExecutor,
isClaudeExecutor,
isGeminiExecutor,
executionProcess.stdout,
]);
2025-06-22 23:27:39 +01:00
// Set initial view mode based on JSONL detection
useEffect(() => {
if (isValidJsonl) {
2025-06-25 09:36:07 +01:00
setViewMode('conversation');
2025-06-21 22:12:32 +01:00
}
2025-06-22 23:27:39 +01:00
}, [isValidJsonl]);
2025-06-21 22:12:32 +01:00
if (!hasStdout && !hasStderr) {
return (
<Card className="bg-muted border-none">
<CardContent className="p-3">
<div className="text-xs text-muted-foreground italic text-center">
Waiting for output...
</div>
</CardContent>
</Card>
);
}
2025-06-25 09:36:07 +01:00
const statusDisplay = getExecutionProcessStatusDisplay(
executionProcess.status
);
2025-06-21 22:12:32 +01:00
return (
2025-06-21 22:33:33 +01:00
<Card className="">
2025-06-21 22:12:32 +01:00
<CardContent className="p-3">
<div className="space-y-3">
{/* Execution process header with status */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs capitalize">
2025-06-25 09:36:07 +01:00
{executionProcess.process_type
.replace(/([A-Z])/g, ' $1')
.toLowerCase()}
</Badge>
<div className="flex items-center gap-1">
2025-06-25 09:36:07 +01:00
<div
className={`h-2 w-2 rounded-full ${statusDisplay.color}`}
/>
<span className="text-xs text-muted-foreground">
{statusDisplay.label}
</span>
</div>
{executor && (
<Badge variant="secondary" className="text-xs">
{executor}
</Badge>
)}
</div>
</div>
2025-06-22 23:27:39 +01:00
{/* View mode toggle for executors with valid JSONL */}
{isValidJsonl && hasStdout && (
2025-06-21 22:12:32 +01:00
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
2025-06-22 23:27:39 +01:00
{jsonlFormat && (
<Badge variant="secondary" className="text-xs">
{jsonlFormat} format
</Badge>
)}
2025-06-21 22:12:32 +01:00
</div>
<div className="flex items-center gap-1">
<Button
2025-06-25 09:36:07 +01:00
variant={viewMode === 'conversation' ? 'default' : 'ghost'}
2025-06-21 22:12:32 +01:00
size="sm"
2025-06-25 09:36:07 +01:00
onClick={() => setViewMode('conversation')}
2025-06-21 22:12:32 +01:00
className="h-7 px-2 text-xs"
>
<MessageSquare className="h-3 w-3 mr-1" />
Conversation
</Button>
<Button
2025-06-25 09:36:07 +01:00
variant={viewMode === 'raw' ? 'default' : 'ghost'}
2025-06-21 22:12:32 +01:00
size="sm"
2025-06-25 09:36:07 +01:00
onClick={() => setViewMode('raw')}
2025-06-21 22:12:32 +01:00
className="h-7 px-2 text-xs"
>
<FileText className="h-3 w-3 mr-1" />
Raw
</Button>
</div>
</div>
)}
{/* Output content */}
{hasStdout && (
<div>
2025-06-25 09:36:07 +01:00
{isValidJsonl && viewMode === 'conversation' ? (
2025-06-21 22:12:32 +01:00
<ConversationViewer
2025-06-25 09:36:07 +01:00
jsonlOutput={executionProcess.stdout || ''}
2025-06-21 22:12:32 +01:00
/>
) : (
<div>
<pre className="text-xs overflow-x-auto whitespace-pre-wrap p-2">
{executionProcess.stdout}
</pre>
</div>
)}
</div>
)}
{hasStderr && (
<div>
<pre className="text-xs overflow-x-auto whitespace-pre-wrap p-2 text-red-600">
{executionProcess.stderr}
</pre>
</div>
)}
</div>
</CardContent>
</Card>
);
}