mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-04 01:40:45 +08:00
feat(cli-stream-monitor): add JSON card components and refactor tab UI
Add new components for CLI stream monitoring with JSON visualization: - ExecutionTab: simplified tab with status indicator and active styling - JsonCard: collapsible card for JSON data with type-based styling (6 types) - JsonField: recursive JSON field renderer with color-coded values - OutputLine: auto-detects JSON and renders appropriate component - jsonDetector: smart JSON detection supporting 5 formats Refactor CliStreamMonitorLegacy to use new components: - Replace inline tab rendering with ExecutionTab component - Replace inline output rendering with OutputLine component - Add Badge import for active count display - Fix type safety with proper id propagation Component features: - Type-specific styling for tool_call, metadata, system, stdout, stderr, thought - Collapsible content (show 3 fields by default, expandable) - Copy button and raw JSON view toggle - Timestamp display - Auto-detection of JSON in output lines Fixes: - Missing jsonDetector.ts file - Type mismatch between OutputLine (6 types) and JsonCard (4 types) - Unused isActive prop in ExecutionTab
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
// ========================================
|
||||
// ExecutionTab Component
|
||||
// ========================================
|
||||
// Tab component for displaying CLI execution status
|
||||
|
||||
import { TabsTrigger } from '@/components/ui/Tabs';
|
||||
import { X } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { CliExecutionState } from '@/stores/cliStreamStore';
|
||||
|
||||
export interface ExecutionTabProps {
|
||||
execution: CliExecutionState & { id: string };
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
onClose: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
export function ExecutionTab({ execution, isActive, onClick, onClose }: ExecutionTabProps) {
|
||||
// Simplify tool name (e.g., gemini-2.5-pro -> gemini)
|
||||
const toolNameShort = execution.tool.split('-')[0];
|
||||
|
||||
// Status color mapping
|
||||
const statusColor = {
|
||||
running: 'bg-green-500 animate-pulse',
|
||||
completed: 'bg-blue-500',
|
||||
error: 'bg-red-500',
|
||||
}[execution.status];
|
||||
|
||||
return (
|
||||
<TabsTrigger
|
||||
value={execution.id}
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'gap-2 text-xs px-3 py-1.5',
|
||||
isActive
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted/50 hover:bg-muted/70',
|
||||
'transition-colors'
|
||||
)}
|
||||
>
|
||||
{/* Status indicator dot */}
|
||||
<span className={cn('w-2 h-2 rounded-full shrink-0', statusColor)} />
|
||||
|
||||
{/* Simplified tool name */}
|
||||
<span className="font-medium">{toolNameShort}</span>
|
||||
|
||||
{/* Execution mode */}
|
||||
<span className="opacity-70">{execution.mode}</span>
|
||||
|
||||
{/* Line count statistics */}
|
||||
<span className="text-[10px] opacity-50 tabular-nums">
|
||||
{execution.output.length} lines
|
||||
</span>
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="ml-1 p-0.5 rounded hover:bg-destructive/20 transition-colors"
|
||||
aria-label="Close execution tab"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</TabsTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExecutionTab;
|
||||
@@ -0,0 +1,186 @@
|
||||
// ========================================
|
||||
// JsonCard Component
|
||||
// ========================================
|
||||
// Collapsible card component for displaying JSON data with type-based styling
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Wrench,
|
||||
Settings,
|
||||
Info,
|
||||
Code,
|
||||
Copy,
|
||||
ChevronRight,
|
||||
AlertTriangle,
|
||||
Brain,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { JsonField } from './JsonField';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
export interface JsonCardProps {
|
||||
/** JSON data to display */
|
||||
data: Record<string, unknown>;
|
||||
/** Type of the card (affects styling and icon) */
|
||||
type: 'tool_call' | 'metadata' | 'system' | 'stdout' | 'stderr' | 'thought';
|
||||
/** Timestamp for the data */
|
||||
timestamp?: number;
|
||||
/** Callback when copy button is clicked */
|
||||
onCopy?: () => void;
|
||||
}
|
||||
|
||||
// ========== Type Configuration ==========
|
||||
|
||||
type TypeConfig = {
|
||||
icon: typeof Wrench;
|
||||
label: string;
|
||||
color: string;
|
||||
bg: string;
|
||||
};
|
||||
|
||||
const TYPE_CONFIGS: Record<string, TypeConfig> = {
|
||||
tool_call: {
|
||||
icon: Wrench,
|
||||
label: 'Tool Call',
|
||||
color: 'text-green-400',
|
||||
bg: 'bg-green-950/30 border-green-900/50',
|
||||
},
|
||||
metadata: {
|
||||
icon: Info,
|
||||
label: 'Metadata',
|
||||
color: 'text-yellow-400',
|
||||
bg: 'bg-yellow-950/30 border-yellow-900/50',
|
||||
},
|
||||
system: {
|
||||
icon: Settings,
|
||||
label: 'System',
|
||||
color: 'text-blue-400',
|
||||
bg: 'bg-blue-950/30 border-blue-900/50',
|
||||
},
|
||||
stdout: {
|
||||
icon: Code,
|
||||
label: 'Data',
|
||||
color: 'text-cyan-400',
|
||||
bg: 'bg-cyan-950/30 border-cyan-900/50',
|
||||
},
|
||||
stderr: {
|
||||
icon: AlertTriangle,
|
||||
label: 'Error',
|
||||
color: 'text-red-400',
|
||||
bg: 'bg-red-950/30 border-red-900/50',
|
||||
},
|
||||
thought: {
|
||||
icon: Brain,
|
||||
label: 'Thought',
|
||||
color: 'text-purple-400',
|
||||
bg: 'bg-purple-950/30 border-purple-900/50',
|
||||
},
|
||||
};
|
||||
|
||||
// ========== Component ==========
|
||||
|
||||
export function JsonCard({
|
||||
data,
|
||||
type,
|
||||
timestamp,
|
||||
onCopy,
|
||||
}: JsonCardProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [showRaw, setShowRaw] = useState(false);
|
||||
|
||||
const entries = Object.entries(data);
|
||||
const visibleCount = isExpanded ? entries.length : 3;
|
||||
const hasMore = entries.length > 3;
|
||||
|
||||
const config = TYPE_CONFIGS[type];
|
||||
const Icon = config.icon;
|
||||
|
||||
return (
|
||||
<div className={cn('border rounded-lg overflow-hidden my-2', config.bg)}>
|
||||
{/* Header */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between px-3 py-2 cursor-pointer',
|
||||
'hover:bg-black/5 dark:hover:bg-white/5 transition-colors'
|
||||
)}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className={cn('h-4 w-4', config.color)} />
|
||||
<span className="text-sm font-medium">{config.label}</span>
|
||||
<Badge variant="secondary" className="text-xs h-5">
|
||||
{entries.length}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{timestamp && (
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{new Date(timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCopy?.();
|
||||
}}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowRaw(!showRaw);
|
||||
}}
|
||||
>
|
||||
<Code className="h-3 w-3" />
|
||||
</Button>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'h-4 w-4 transition-transform',
|
||||
isExpanded && 'rotate-90'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{showRaw ? (
|
||||
<pre className="p-3 text-xs bg-black/20 overflow-x-auto max-h-60">
|
||||
<code>{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
) : (
|
||||
<div className="divide-y divide-border/30">
|
||||
{entries.slice(0, visibleCount).map(([key, value]) => (
|
||||
<JsonField key={key} fieldName={key} value={value} />
|
||||
))}
|
||||
{hasMore && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsExpanded(!isExpanded);
|
||||
}}
|
||||
className="w-full px-3 py-2 text-xs text-muted-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors text-left"
|
||||
>
|
||||
{isExpanded
|
||||
? '▲ Show less'
|
||||
: `▼ Show ${entries.length - 3} more fields`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default JsonCard;
|
||||
@@ -0,0 +1,88 @@
|
||||
import { useState } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface JsonFieldProps {
|
||||
fieldName: string;
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
export function JsonField({ fieldName, value }: JsonFieldProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const isObject = value !== null && typeof value === 'object';
|
||||
const isNested = isObject && (Array.isArray(value) || Object.keys(value).length > 0);
|
||||
|
||||
const renderPrimitiveValue = (val: unknown): React.ReactNode => {
|
||||
if (val === null) return <span className="text-muted-foreground italic">null</span>;
|
||||
if (typeof val === 'boolean') return <span className="text-purple-400 font-medium">{String(val)}</span>;
|
||||
if (typeof val === 'number') return <span className="text-orange-400 font-mono">{String(val)}</span>;
|
||||
if (typeof val === 'string') {
|
||||
// Check if it's a JSON string
|
||||
const trimmed = val.trim();
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
return <span className="text-green-400">"{trimmed.substring(0, 30)}..."</span>;
|
||||
}
|
||||
return <span className="text-green-400">"{val}"</span>;
|
||||
}
|
||||
return String(val);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex items-start gap-2 px-3 py-2 hover:bg-black/5 dark:hover:bg-white/5 transition-colors',
|
||||
'text-sm'
|
||||
)}>
|
||||
{/* Field name */}
|
||||
<span className="shrink-0 font-mono text-cyan-400 min-w-[100px]">
|
||||
{fieldName}
|
||||
</span>
|
||||
|
||||
{/* Separator */}
|
||||
<span className="shrink-0 text-muted-foreground">:</span>
|
||||
|
||||
{/* Value */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{isNested ? (
|
||||
<details
|
||||
open={isExpanded}
|
||||
onToggle={(e) => setIsExpanded(e.currentTarget.open)}
|
||||
className="group"
|
||||
>
|
||||
<summary className="cursor-pointer list-none flex items-center gap-1 hover:text-foreground">
|
||||
<span className="text-muted-foreground group-hover:text-foreground transition-colors">
|
||||
{isExpanded ? '▼' : '▶'}
|
||||
</span>
|
||||
{Array.isArray(value) ? (
|
||||
<span className="text-blue-400">Array[{value.length}]</span>
|
||||
) : (
|
||||
<span className="text-yellow-400">Object{'{'}{Object.keys(value).length}{'}'}</span>
|
||||
)}
|
||||
</summary>
|
||||
{isExpanded && (
|
||||
<div className="ml-4 mt-2 space-y-1">
|
||||
{Array.isArray(value)
|
||||
? value.map((item, i) => (
|
||||
<div key={i} className="pl-2 border-l border-border/30">
|
||||
{typeof item === 'object' && item !== null ? (
|
||||
<JsonField fieldName={`[${i}]`} value={item} />
|
||||
) : (
|
||||
renderPrimitiveValue(item)
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
: Object.entries(value as Record<string, unknown>).map(([k, v]) => (
|
||||
<JsonField key={k} fieldName={k} value={v} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</details>
|
||||
) : (
|
||||
<div className="break-all">{renderPrimitiveValue(value)}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default JsonField;
|
||||
@@ -0,0 +1,107 @@
|
||||
// ========================================
|
||||
// OutputLine Component
|
||||
// ========================================
|
||||
// Renders a single output line with JSON auto-detection
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { Brain, Settings, AlertCircle, Info, MessageCircle, Wrench } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { JsonCard } from './JsonCard';
|
||||
import { detectJsonInLine } from '../utils/jsonDetector';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
export interface OutputLineProps {
|
||||
line: {
|
||||
type: 'stdout' | 'stderr' | 'metadata' | 'thought' | 'system' | 'tool_call';
|
||||
content: string;
|
||||
timestamp: number;
|
||||
};
|
||||
onCopy?: (content: string) => void;
|
||||
}
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
/**
|
||||
* Get the icon component for a given output line type
|
||||
*/
|
||||
function getOutputLineIcon(type: OutputLineProps['line']['type']) {
|
||||
switch (type) {
|
||||
case 'thought':
|
||||
return <Brain className="h-3 w-3" />;
|
||||
case 'system':
|
||||
return <Settings className="h-3 w-3" />;
|
||||
case 'stderr':
|
||||
return <AlertCircle className="h-3 w-3" />;
|
||||
case 'metadata':
|
||||
return <Info className="h-3 w-3" />;
|
||||
case 'tool_call':
|
||||
return <Wrench className="h-3 w-3" />;
|
||||
case 'stdout':
|
||||
default:
|
||||
return <MessageCircle className="h-3 w-3" />;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CSS class name for a given output line type
|
||||
* Reuses the existing implementation from LogBlock utils
|
||||
*/
|
||||
function getOutputLineClass(type: OutputLineProps['line']['type']): string {
|
||||
switch (type) {
|
||||
case 'thought':
|
||||
return 'text-purple-400';
|
||||
case 'system':
|
||||
return 'text-blue-400';
|
||||
case 'stderr':
|
||||
return 'text-red-400';
|
||||
case 'metadata':
|
||||
return 'text-yellow-400';
|
||||
case 'tool_call':
|
||||
return 'text-green-400';
|
||||
case 'stdout':
|
||||
default:
|
||||
return 'text-foreground';
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Component ==========
|
||||
|
||||
/**
|
||||
* OutputLine - Renders a single CLI output line
|
||||
*
|
||||
* Features:
|
||||
* - Auto-detects JSON content and renders with JsonCard
|
||||
* - Shows appropriate icon based on line type
|
||||
* - Applies color styling based on line type
|
||||
* - Supports copy functionality
|
||||
*/
|
||||
export function OutputLine({ line, onCopy }: OutputLineProps) {
|
||||
// Memoize JSON detection to avoid re-parsing on every render
|
||||
const jsonDetection = useMemo(() => detectJsonInLine(line.content), [line.content]);
|
||||
|
||||
return (
|
||||
<div className={cn('flex gap-2 text-xs', getOutputLineClass(line.type))}>
|
||||
{/* Icon indicator */}
|
||||
<span className="text-muted-foreground shrink-0 mt-0.5">
|
||||
{getOutputLineIcon(line.type)}
|
||||
</span>
|
||||
|
||||
{/* Content area */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{jsonDetection.isJson && jsonDetection.parsed ? (
|
||||
<JsonCard
|
||||
data={jsonDetection.parsed}
|
||||
type={line.type as 'tool_call' | 'metadata' | 'system' | 'stdout'}
|
||||
timestamp={line.timestamp}
|
||||
onCopy={() => onCopy?.(line.content)}
|
||||
/>
|
||||
) : (
|
||||
<span className="break-all whitespace-pre-wrap">{line.content}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default OutputLine;
|
||||
@@ -0,0 +1,19 @@
|
||||
// ========================================
|
||||
// CliStreamMonitor Components Index
|
||||
// ========================================
|
||||
// Export barrel for CliStreamMonitor components
|
||||
|
||||
// Tab component
|
||||
export { ExecutionTab } from './ExecutionTab';
|
||||
export type { ExecutionTabProps } from './ExecutionTab';
|
||||
|
||||
// JSON display components
|
||||
export { JsonCard } from './JsonCard';
|
||||
export type { JsonCardProps } from './JsonCard';
|
||||
|
||||
export { JsonField } from './JsonField';
|
||||
export type { JsonFieldProps } from './JsonField';
|
||||
|
||||
// Output components
|
||||
export { OutputLine } from './OutputLine';
|
||||
export type { OutputLineProps } from './OutputLine';
|
||||
@@ -1,13 +1,15 @@
|
||||
// ========================================
|
||||
// CliStreamMonitor Component Exports
|
||||
// ========================================
|
||||
// New layout exports for the redesigned CLI Stream Monitor
|
||||
|
||||
// Main component (new layout)
|
||||
// Main components
|
||||
export { CliStreamMonitorNew as CliStreamMonitor } from './CliStreamMonitorNew';
|
||||
export type { CliStreamMonitorNewProps as CliStreamMonitorProps } from './CliStreamMonitorNew';
|
||||
|
||||
// Layout components
|
||||
export { default as CliStreamMonitorLegacy } from '../CliStreamMonitorLegacy';
|
||||
export type { CliStreamMonitorProps as CliStreamMonitorLegacyProps } from '../CliStreamMonitorLegacy';
|
||||
|
||||
// Layout components (new design)
|
||||
export { MonitorHeader } from './MonitorHeader';
|
||||
export type { MonitorHeaderProps } from './MonitorHeader';
|
||||
|
||||
@@ -17,7 +19,7 @@ export type { MonitorToolbarProps, FilterType, ViewMode } from './MonitorToolbar
|
||||
export { MonitorBody } from './MonitorBody';
|
||||
export type { MonitorBodyProps, MonitorBodyRef } from './MonitorBody';
|
||||
|
||||
// Message type components
|
||||
// Message type components (new design)
|
||||
export {
|
||||
SystemMessage,
|
||||
UserMessage,
|
||||
@@ -31,6 +33,23 @@ export type {
|
||||
ErrorMessageProps,
|
||||
} from './messages';
|
||||
|
||||
// Message renderer
|
||||
// Message renderer (new design)
|
||||
export { MessageRenderer } from './MessageRenderer';
|
||||
export type { MessageRendererProps } from './MessageRenderer';
|
||||
|
||||
// Utility components for Tab + JSON Cards (v3 design)
|
||||
export { ExecutionTab } from './components/ExecutionTab';
|
||||
export type { ExecutionTabProps } from './components/ExecutionTab';
|
||||
|
||||
export { JsonCard } from './components/JsonCard';
|
||||
export type { JsonCardProps } from './components/JsonCard';
|
||||
|
||||
export { JsonField } from './components/JsonField';
|
||||
export type { JsonFieldProps } from './components/JsonField';
|
||||
|
||||
export { OutputLine } from './components/OutputLine';
|
||||
export type { OutputLineProps } from './components/OutputLine';
|
||||
|
||||
// Utilities
|
||||
export { detectJsonInLine } from './utils/jsonDetector';
|
||||
export type { JsonDetectionResult } from './utils/jsonDetector';
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// ========================================
|
||||
// CliStreamMonitor Utilities Index
|
||||
// ========================================
|
||||
// Export barrel for CliStreamMonitor utilities
|
||||
|
||||
// JSON detection utility
|
||||
export { detectJsonInLine } from './jsonDetector';
|
||||
export type { JsonDetectionResult } from './jsonDetector';
|
||||
@@ -0,0 +1,104 @@
|
||||
// ========================================
|
||||
// JSON Detection Utility
|
||||
// ========================================
|
||||
// Smart JSON detection for CLI output lines
|
||||
|
||||
/**
|
||||
* Result of JSON detection
|
||||
*/
|
||||
export interface JsonDetectionResult {
|
||||
isJson: boolean;
|
||||
parsed?: Record<string, unknown>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if a line contains JSON data
|
||||
* Supports multiple formats:
|
||||
* - Direct JSON: {...} or [...]
|
||||
* - Tool Call: [Tool] toolName({...})
|
||||
* - Tool Result: [Tool Result] status: {...}
|
||||
* - Embedded JSON: trailing JSON object
|
||||
* - Code block JSON: ```json ... ```
|
||||
*/
|
||||
export function detectJsonInLine(content: string): JsonDetectionResult {
|
||||
const trimmed = content.trim();
|
||||
|
||||
// 1. Direct JSON object or array
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
return { isJson: true, parsed: parsed as Record<string, unknown> };
|
||||
} catch {
|
||||
// Continue to other patterns
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Tool Call format: [Tool] toolName({...})
|
||||
const toolCallMatch = trimmed.match(/^\[Tool\]\s+(\w+)\((.*)\)$/);
|
||||
if (toolCallMatch) {
|
||||
const [, toolName, paramsStr] = toolCallMatch;
|
||||
let parameters: unknown;
|
||||
|
||||
try {
|
||||
parameters = paramsStr ? JSON.parse(paramsStr) : {};
|
||||
} catch {
|
||||
parameters = paramsStr || null;
|
||||
}
|
||||
|
||||
return {
|
||||
isJson: true,
|
||||
parsed: {
|
||||
action: 'invoke',
|
||||
toolName,
|
||||
parameters,
|
||||
} as Record<string, unknown>,
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Tool Result format: [Tool Result] status: output
|
||||
const toolResultMatch = trimmed.match(/^\[Tool Result\]\s+(.+?)\s*:\s*(.+)$/);
|
||||
if (toolResultMatch) {
|
||||
const [, status, outputStr] = toolResultMatch;
|
||||
let output: unknown;
|
||||
|
||||
try {
|
||||
output = outputStr.trim().startsWith('{') ? JSON.parse(outputStr) : outputStr;
|
||||
} catch {
|
||||
output = outputStr;
|
||||
}
|
||||
|
||||
return {
|
||||
isJson: true,
|
||||
parsed: {
|
||||
action: 'result',
|
||||
status,
|
||||
output,
|
||||
} as Record<string, unknown>,
|
||||
};
|
||||
}
|
||||
|
||||
// 4. Embedded JSON at end of line
|
||||
const embeddedJsonMatch = trimmed.match(/\{.*\}$/);
|
||||
if (embeddedJsonMatch) {
|
||||
try {
|
||||
const parsed = JSON.parse(embeddedJsonMatch[0]);
|
||||
return { isJson: true, parsed: parsed as Record<string, unknown> };
|
||||
} catch {
|
||||
// Not valid JSON
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Code block JSON
|
||||
const codeBlockMatch = trimmed.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
|
||||
if (codeBlockMatch) {
|
||||
try {
|
||||
const parsed = JSON.parse(codeBlockMatch[1]);
|
||||
return { isJson: true, parsed: parsed as Record<string, unknown> };
|
||||
} catch {
|
||||
return { isJson: false, error: 'Invalid JSON in code block' };
|
||||
}
|
||||
}
|
||||
|
||||
return { isJson: false };
|
||||
}
|
||||
@@ -9,28 +9,25 @@ import {
|
||||
X,
|
||||
Terminal,
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
Clock,
|
||||
RefreshCw,
|
||||
Search,
|
||||
XCircle,
|
||||
ArrowDownToLine,
|
||||
Brain,
|
||||
Settings,
|
||||
Info,
|
||||
MessageCircle,
|
||||
Wrench,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/Tabs';
|
||||
import { LogBlockList, getOutputLineClass } from '@/components/shared/LogBlock';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { LogBlockList } from '@/components/shared/LogBlock';
|
||||
import { useCliStreamStore, type CliOutputLine } from '@/stores/cliStreamStore';
|
||||
import { useNotificationStore, selectWsLastMessage } from '@/stores';
|
||||
import { useActiveCliExecutions, useInvalidateActiveCliExecutions } from '@/hooks/useActiveCliExecutions';
|
||||
|
||||
// New components for Tab + JSON Cards
|
||||
import { ExecutionTab } from './CliStreamMonitor/components/ExecutionTab';
|
||||
import { OutputLine } from './CliStreamMonitor/components/OutputLine';
|
||||
|
||||
// ========== Types for CLI WebSocket Messages ==========
|
||||
|
||||
interface CliStreamStartedPayload {
|
||||
@@ -77,25 +74,6 @@ function formatDuration(ms: number): string {
|
||||
return `${hours}h ${remainingMinutes}m`;
|
||||
}
|
||||
|
||||
// Local function for icon rendering (uses JSX, must stay in .tsx file)
|
||||
function getOutputLineIcon(type: CliOutputLine['type']) {
|
||||
switch (type) {
|
||||
case 'thought':
|
||||
return <Brain className="h-3 w-3" />;
|
||||
case 'system':
|
||||
return <Settings className="h-3 w-3" />;
|
||||
case 'stderr':
|
||||
return <AlertCircle className="h-3 w-3" />;
|
||||
case 'metadata':
|
||||
return <Info className="h-3 w-3" />;
|
||||
case 'tool_call':
|
||||
return <Wrench className="h-3 w-3" />;
|
||||
case 'stdout':
|
||||
default:
|
||||
return <MessageCircle className="h-3 w-3" />;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Component ==========
|
||||
|
||||
export interface CliStreamMonitorProps {
|
||||
@@ -343,43 +321,18 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="w-full h-auto flex-wrap gap-1 bg-secondary/50 p-1">
|
||||
{sortedExecutionIds.map((id) => {
|
||||
const exec = executions[id];
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={id}
|
||||
value={id}
|
||||
className={cn(
|
||||
'gap-1.5 text-xs px-2 py-1',
|
||||
exec.status === 'running' && 'bg-primary text-primary-foreground'
|
||||
)}
|
||||
>
|
||||
<span className={cn('w-1.5 h-1.5 rounded-full', {
|
||||
'bg-green-500 animate-pulse': exec.status === 'running',
|
||||
'bg-blue-500': exec.status === 'completed',
|
||||
'bg-red-500': exec.status === 'error'
|
||||
})} />
|
||||
<span className="font-medium">{exec.tool}</span>
|
||||
<span className="text-muted-foreground">{exec.mode}</span>
|
||||
{exec.recovered && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1 py-0 h-4">
|
||||
Recovered
|
||||
</Badge>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 p-0 ml-1 hover:bg-destructive hover:text-destructive-foreground"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeExecution(id);
|
||||
}}
|
||||
>
|
||||
<XCircle className="h-3 w-3" />
|
||||
</Button>
|
||||
</TabsTrigger>
|
||||
);
|
||||
})}
|
||||
{sortedExecutionIds.map((id) => (
|
||||
<ExecutionTab
|
||||
key={id}
|
||||
execution={{ ...executions[id], id }}
|
||||
isActive={currentExecutionId === id}
|
||||
onClick={() => setCurrentExecution(id)}
|
||||
onClose={(e) => {
|
||||
e.stopPropagation();
|
||||
removeExecution(id);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{/* Output Panel */}
|
||||
@@ -459,12 +412,11 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{filteredOutput.map((line, index) => (
|
||||
<div key={index} className={cn('flex gap-2', getOutputLineClass(line.type))}>
|
||||
<span className="text-muted-foreground shrink-0">
|
||||
{getOutputLineIcon(line.type)}
|
||||
</span>
|
||||
<span className="break-all">{line.content}</span>
|
||||
</div>
|
||||
<OutputLine
|
||||
key={`${line.timestamp}-${index}`}
|
||||
line={line}
|
||||
onCopy={(content) => navigator.clipboard.writeText(content)}
|
||||
/>
|
||||
))}
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
|
||||
@@ -60,18 +60,18 @@ export type { FlowchartProps } from './Flowchart';
|
||||
export { CliStreamPanel } from './CliStreamPanel';
|
||||
export type { CliStreamPanelProps } from './CliStreamPanel';
|
||||
|
||||
// New CliStreamMonitor with message-based layout
|
||||
export { CliStreamMonitor } from './CliStreamMonitor/index';
|
||||
export type { CliStreamMonitorProps } from './CliStreamMonitor/index';
|
||||
// CliStreamMonitor (updated with Tab + JSON Cards - v3 design)
|
||||
export { default as CliStreamMonitor } from './CliStreamMonitorLegacy';
|
||||
export type { CliStreamMonitorProps } from './CliStreamMonitorLegacy';
|
||||
|
||||
// Legacy CliStreamMonitor (old layout)
|
||||
export { default as CliStreamMonitorLegacy } from './CliStreamMonitorLegacy';
|
||||
export type { CliStreamMonitorProps as CliStreamMonitorLegacyProps } from './CliStreamMonitorLegacy';
|
||||
// Alternative: New message-based layout (CliStreamMonitorNew)
|
||||
export { CliStreamMonitor as CliStreamMonitorNew } from './CliStreamMonitor/index';
|
||||
export type { CliStreamMonitorProps as CliStreamMonitorNewProps } from './CliStreamMonitor/index';
|
||||
|
||||
export { StreamingOutput } from './StreamingOutput';
|
||||
export type { StreamingOutputProps } from './StreamingOutput';
|
||||
|
||||
// CliStreamMonitor sub-components
|
||||
// CliStreamMonitor sub-components (new design)
|
||||
export { MonitorHeader } from './CliStreamMonitor/index';
|
||||
export type { MonitorHeaderProps } from './CliStreamMonitor/index';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user