diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/components/ExecutionTab.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/components/ExecutionTab.tsx new file mode 100644 index 00000000..1c724a1a --- /dev/null +++ b/ccw/frontend/src/components/shared/CliStreamMonitor/components/ExecutionTab.tsx @@ -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 ( + + {/* Status indicator dot */} + + + {/* Simplified tool name */} + {toolNameShort} + + {/* Execution mode */} + {execution.mode} + + {/* Line count statistics */} + + {execution.output.length} lines + + + {/* Close button */} + + + ); +} + +export default ExecutionTab; diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/components/JsonCard.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/components/JsonCard.tsx new file mode 100644 index 00000000..e18ce985 --- /dev/null +++ b/ccw/frontend/src/components/shared/CliStreamMonitor/components/JsonCard.tsx @@ -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; + /** 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 = { + 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 ( +
+ {/* Header */} +
setIsExpanded(!isExpanded)} + > +
+ + {config.label} + + {entries.length} + +
+ +
+ {timestamp && ( + + {new Date(timestamp).toLocaleTimeString()} + + )} + + + +
+
+ + {/* Content */} + {showRaw ? ( +
+          {JSON.stringify(data, null, 2)}
+        
+ ) : ( +
+ {entries.slice(0, visibleCount).map(([key, value]) => ( + + ))} + {hasMore && ( + + )} +
+ )} +
+ ); +} + +export default JsonCard; diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/components/JsonField.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/components/JsonField.tsx new file mode 100644 index 00000000..88bb1002 --- /dev/null +++ b/ccw/frontend/src/components/shared/CliStreamMonitor/components/JsonField.tsx @@ -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 null; + if (typeof val === 'boolean') return {String(val)}; + if (typeof val === 'number') return {String(val)}; + if (typeof val === 'string') { + // Check if it's a JSON string + const trimmed = val.trim(); + if (trimmed.startsWith('{') || trimmed.startsWith('[')) { + return "{trimmed.substring(0, 30)}..."; + } + return "{val}"; + } + return String(val); + }; + + return ( +
+ {/* Field name */} + + {fieldName} + + + {/* Separator */} + : + + {/* Value */} +
+ {isNested ? ( +
setIsExpanded(e.currentTarget.open)} + className="group" + > + + + {isExpanded ? '▼' : '▶'} + + {Array.isArray(value) ? ( + Array[{value.length}] + ) : ( + Object{'{'}{Object.keys(value).length}{'}'} + )} + + {isExpanded && ( +
+ {Array.isArray(value) + ? value.map((item, i) => ( +
+ {typeof item === 'object' && item !== null ? ( + + ) : ( + renderPrimitiveValue(item) + )} +
+ )) + : Object.entries(value as Record).map(([k, v]) => ( + + )) + } +
+ )} +
+ ) : ( +
{renderPrimitiveValue(value)}
+ )} +
+
+ ); +} + +export default JsonField; diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/components/OutputLine.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/components/OutputLine.tsx new file mode 100644 index 00000000..759d47ae --- /dev/null +++ b/ccw/frontend/src/components/shared/CliStreamMonitor/components/OutputLine.tsx @@ -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 ; + case 'system': + return ; + case 'stderr': + return ; + case 'metadata': + return ; + case 'tool_call': + return ; + case 'stdout': + default: + return ; + } +} + +/** + * 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 ( +
+ {/* Icon indicator */} + + {getOutputLineIcon(line.type)} + + + {/* Content area */} +
+ {jsonDetection.isJson && jsonDetection.parsed ? ( + onCopy?.(line.content)} + /> + ) : ( + {line.content} + )} +
+
+ ); +} + +export default OutputLine; diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/components/index.ts b/ccw/frontend/src/components/shared/CliStreamMonitor/components/index.ts new file mode 100644 index 00000000..387e13c7 --- /dev/null +++ b/ccw/frontend/src/components/shared/CliStreamMonitor/components/index.ts @@ -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'; diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/index.ts b/ccw/frontend/src/components/shared/CliStreamMonitor/index.ts index d00a059f..d6c134f0 100644 --- a/ccw/frontend/src/components/shared/CliStreamMonitor/index.ts +++ b/ccw/frontend/src/components/shared/CliStreamMonitor/index.ts @@ -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'; diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/utils/index.ts b/ccw/frontend/src/components/shared/CliStreamMonitor/utils/index.ts new file mode 100644 index 00000000..1f462242 --- /dev/null +++ b/ccw/frontend/src/components/shared/CliStreamMonitor/utils/index.ts @@ -0,0 +1,8 @@ +// ======================================== +// CliStreamMonitor Utilities Index +// ======================================== +// Export barrel for CliStreamMonitor utilities + +// JSON detection utility +export { detectJsonInLine } from './jsonDetector'; +export type { JsonDetectionResult } from './jsonDetector'; diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/utils/jsonDetector.ts b/ccw/frontend/src/components/shared/CliStreamMonitor/utils/jsonDetector.ts new file mode 100644 index 00000000..08883208 --- /dev/null +++ b/ccw/frontend/src/components/shared/CliStreamMonitor/utils/jsonDetector.ts @@ -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; + 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 }; + } 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, + }; + } + + // 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, + }; + } + + // 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 }; + } 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 }; + } catch { + return { isJson: false, error: 'Invalid JSON in code block' }; + } + } + + return { isJson: false }; +} diff --git a/ccw/frontend/src/components/shared/CliStreamMonitorLegacy.tsx b/ccw/frontend/src/components/shared/CliStreamMonitorLegacy.tsx index 6322013b..92a9dc7f 100644 --- a/ccw/frontend/src/components/shared/CliStreamMonitorLegacy.tsx +++ b/ccw/frontend/src/components/shared/CliStreamMonitorLegacy.tsx @@ -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 ; - case 'system': - return ; - case 'stderr': - return ; - case 'metadata': - return ; - case 'tool_call': - return ; - case 'stdout': - default: - return ; - } -} - // ========== Component ========== export interface CliStreamMonitorProps { @@ -343,43 +321,18 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) { className="w-full" > - {sortedExecutionIds.map((id) => { - const exec = executions[id]; - return ( - - - {exec.tool} - {exec.mode} - {exec.recovered && ( - - Recovered - - )} - - - ); - })} + {sortedExecutionIds.map((id) => ( + setCurrentExecution(id)} + onClose={(e) => { + e.stopPropagation(); + removeExecution(id); + }} + /> + ))} {/* Output Panel */} @@ -459,12 +412,11 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) { ) : (
{filteredOutput.map((line, index) => ( -
- - {getOutputLineIcon(line.type)} - - {line.content} -
+ navigator.clipboard.writeText(content)} + /> ))}
diff --git a/ccw/frontend/src/components/shared/index.ts b/ccw/frontend/src/components/shared/index.ts index cd8d37a5..c682a56f 100644 --- a/ccw/frontend/src/components/shared/index.ts +++ b/ccw/frontend/src/components/shared/index.ts @@ -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';