mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +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
|
// 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 { CliStreamMonitorNew as CliStreamMonitor } from './CliStreamMonitorNew';
|
||||||
export type { CliStreamMonitorNewProps as CliStreamMonitorProps } 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 { MonitorHeader } from './MonitorHeader';
|
||||||
export type { MonitorHeaderProps } from './MonitorHeader';
|
export type { MonitorHeaderProps } from './MonitorHeader';
|
||||||
|
|
||||||
@@ -17,7 +19,7 @@ export type { MonitorToolbarProps, FilterType, ViewMode } from './MonitorToolbar
|
|||||||
export { MonitorBody } from './MonitorBody';
|
export { MonitorBody } from './MonitorBody';
|
||||||
export type { MonitorBodyProps, MonitorBodyRef } from './MonitorBody';
|
export type { MonitorBodyProps, MonitorBodyRef } from './MonitorBody';
|
||||||
|
|
||||||
// Message type components
|
// Message type components (new design)
|
||||||
export {
|
export {
|
||||||
SystemMessage,
|
SystemMessage,
|
||||||
UserMessage,
|
UserMessage,
|
||||||
@@ -31,6 +33,23 @@ export type {
|
|||||||
ErrorMessageProps,
|
ErrorMessageProps,
|
||||||
} from './messages';
|
} from './messages';
|
||||||
|
|
||||||
// Message renderer
|
// Message renderer (new design)
|
||||||
export { MessageRenderer } from './MessageRenderer';
|
export { MessageRenderer } from './MessageRenderer';
|
||||||
export type { MessageRendererProps } 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,
|
X,
|
||||||
Terminal,
|
Terminal,
|
||||||
Loader2,
|
Loader2,
|
||||||
AlertCircle,
|
|
||||||
Clock,
|
Clock,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Search,
|
Search,
|
||||||
XCircle,
|
|
||||||
ArrowDownToLine,
|
ArrowDownToLine,
|
||||||
Brain,
|
|
||||||
Settings,
|
|
||||||
Info,
|
|
||||||
MessageCircle,
|
|
||||||
Wrench,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/Tabs';
|
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 { useCliStreamStore, type CliOutputLine } from '@/stores/cliStreamStore';
|
||||||
import { useNotificationStore, selectWsLastMessage } from '@/stores';
|
import { useNotificationStore, selectWsLastMessage } from '@/stores';
|
||||||
import { useActiveCliExecutions, useInvalidateActiveCliExecutions } from '@/hooks/useActiveCliExecutions';
|
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 ==========
|
// ========== Types for CLI WebSocket Messages ==========
|
||||||
|
|
||||||
interface CliStreamStartedPayload {
|
interface CliStreamStartedPayload {
|
||||||
@@ -77,25 +74,6 @@ function formatDuration(ms: number): string {
|
|||||||
return `${hours}h ${remainingMinutes}m`;
|
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 ==========
|
// ========== Component ==========
|
||||||
|
|
||||||
export interface CliStreamMonitorProps {
|
export interface CliStreamMonitorProps {
|
||||||
@@ -343,43 +321,18 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<TabsList className="w-full h-auto flex-wrap gap-1 bg-secondary/50 p-1">
|
<TabsList className="w-full h-auto flex-wrap gap-1 bg-secondary/50 p-1">
|
||||||
{sortedExecutionIds.map((id) => {
|
{sortedExecutionIds.map((id) => (
|
||||||
const exec = executions[id];
|
<ExecutionTab
|
||||||
return (
|
key={id}
|
||||||
<TabsTrigger
|
execution={{ ...executions[id], id }}
|
||||||
key={id}
|
isActive={currentExecutionId === id}
|
||||||
value={id}
|
onClick={() => setCurrentExecution(id)}
|
||||||
className={cn(
|
onClose={(e) => {
|
||||||
'gap-1.5 text-xs px-2 py-1',
|
e.stopPropagation();
|
||||||
exec.status === 'running' && 'bg-primary text-primary-foreground'
|
removeExecution(id);
|
||||||
)}
|
}}
|
||||||
>
|
/>
|
||||||
<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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{/* Output Panel */}
|
{/* Output Panel */}
|
||||||
@@ -459,12 +412,11 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{filteredOutput.map((line, index) => (
|
{filteredOutput.map((line, index) => (
|
||||||
<div key={index} className={cn('flex gap-2', getOutputLineClass(line.type))}>
|
<OutputLine
|
||||||
<span className="text-muted-foreground shrink-0">
|
key={`${line.timestamp}-${index}`}
|
||||||
{getOutputLineIcon(line.type)}
|
line={line}
|
||||||
</span>
|
onCopy={(content) => navigator.clipboard.writeText(content)}
|
||||||
<span className="break-all">{line.content}</span>
|
/>
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
<div ref={logsEndRef} />
|
<div ref={logsEndRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -60,18 +60,18 @@ export type { FlowchartProps } from './Flowchart';
|
|||||||
export { CliStreamPanel } from './CliStreamPanel';
|
export { CliStreamPanel } from './CliStreamPanel';
|
||||||
export type { CliStreamPanelProps } from './CliStreamPanel';
|
export type { CliStreamPanelProps } from './CliStreamPanel';
|
||||||
|
|
||||||
// New CliStreamMonitor with message-based layout
|
// CliStreamMonitor (updated with Tab + JSON Cards - v3 design)
|
||||||
export { CliStreamMonitor } from './CliStreamMonitor/index';
|
export { default as CliStreamMonitor } from './CliStreamMonitorLegacy';
|
||||||
export type { CliStreamMonitorProps } from './CliStreamMonitor/index';
|
export type { CliStreamMonitorProps } from './CliStreamMonitorLegacy';
|
||||||
|
|
||||||
// Legacy CliStreamMonitor (old layout)
|
// Alternative: New message-based layout (CliStreamMonitorNew)
|
||||||
export { default as CliStreamMonitorLegacy } from './CliStreamMonitorLegacy';
|
export { CliStreamMonitor as CliStreamMonitorNew } from './CliStreamMonitor/index';
|
||||||
export type { CliStreamMonitorProps as CliStreamMonitorLegacyProps } from './CliStreamMonitorLegacy';
|
export type { CliStreamMonitorProps as CliStreamMonitorNewProps } from './CliStreamMonitor/index';
|
||||||
|
|
||||||
export { StreamingOutput } from './StreamingOutput';
|
export { StreamingOutput } from './StreamingOutput';
|
||||||
export type { StreamingOutputProps } from './StreamingOutput';
|
export type { StreamingOutputProps } from './StreamingOutput';
|
||||||
|
|
||||||
// CliStreamMonitor sub-components
|
// CliStreamMonitor sub-components (new design)
|
||||||
export { MonitorHeader } from './CliStreamMonitor/index';
|
export { MonitorHeader } from './CliStreamMonitor/index';
|
||||||
export type { MonitorHeaderProps } from './CliStreamMonitor/index';
|
export type { MonitorHeaderProps } from './CliStreamMonitor/index';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user