From 666ab7f2d6e653dc1e02f9ad648770466839f52c Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sat, 14 Feb 2026 23:59:08 +0800 Subject: [PATCH] feat: Enhance CliStreamPanel with collapsible views and native session support --- .../src/components/shared/CliStreamPanel.tsx | 428 +++++++++++++++--- 1 file changed, 359 insertions(+), 69 deletions(-) diff --git a/ccw/frontend/src/components/shared/CliStreamPanel.tsx b/ccw/frontend/src/components/shared/CliStreamPanel.tsx index 74c87231..101d2ef5 100644 --- a/ccw/frontend/src/components/shared/CliStreamPanel.tsx +++ b/ccw/frontend/src/components/shared/CliStreamPanel.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { useIntl } from 'react-intl'; -import { User, Bot, AlertTriangle, Info, Layers, Clock, Copy, Terminal, Hash, Calendar, CheckCircle2, XCircle, Timer } from 'lucide-react'; +import { User, Bot, AlertTriangle, Info, Layers, Clock, Copy, Terminal, Hash, Calendar, CheckCircle2, XCircle, Timer, ChevronDown, ChevronRight, FileJson, Brain, Wrench, Coins, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/Button'; import { Badge } from '@/components/ui/Badge'; @@ -17,9 +17,10 @@ import { DialogTitle, } from '@/components/ui/Dialog'; import { useCliExecutionDetail } from '@/hooks/useCliExecution'; -import type { ConversationRecord, ConversationTurn } from '@/lib/api'; +import { useNativeSession } from '@/hooks/useNativeSession'; +import type { ConversationRecord, ConversationTurn, NativeSessionTurn, NativeTokenInfo, NativeToolCall } from '@/lib/api'; -type ViewMode = 'per-turn' | 'concatenated'; +type ViewMode = 'per-turn' | 'concatenated' | 'native'; type ConcatFormat = 'plain' | 'yaml' | 'json'; export interface CliStreamPanelProps { @@ -34,6 +35,8 @@ export interface CliStreamPanelProps { interface TurnSectionProps { turn: ConversationTurn; isLatest: boolean; + isExpanded: boolean; + onToggle: () => void; } interface ConcatenatedViewProps { @@ -148,9 +151,9 @@ function buildConcatenatedPrompt(execution: ConversationRecord, format: ConcatFo // ========== Sub-Components ========== /** - * TurnSection - Single turn display with header and content + * TurnSection - Single turn display with collapsible content */ -function TurnSection({ turn, isLatest }: TurnSectionProps) { +function TurnSection({ turn, isLatest, isExpanded, onToggle }: TurnSectionProps) { const { formatMessage } = useIntl(); const StatusIcon = getStatusInfo(turn.status as string).icon; const statusColor = getStatusInfo(turn.status as string).color; @@ -162,12 +165,21 @@ function TurnSection({ turn, isLatest }: TurnSectionProps) { isLatest && 'ring-2 ring-primary/50 shadow-md' )} > - {/* Turn Header */} -
+ {/* Turn Header - Clickable for expand/collapse */} +
{ if (e.key === 'Enter' || e.key === ' ') onToggle(); }} + >
- + {isExpanded ? ( + + ) : ( + + )} {formatMessage({ id: 'cli.details.turn' })} {turn.turn} {isLatest && ( @@ -190,66 +202,92 @@ function TurnSection({ turn, isLatest }: TurnSectionProps) {
- {/* Turn Body */} -
- {/* User Prompt */} -
-

-

-
-            {turn.prompt}
-          
-
- - {/* Assistant Response */} - {turn.output.stdout && ( + {/* Turn Body - Collapsible */} + {isExpanded && ( +
+ {/* User Prompt */}

-

-
-              {turn.output.stdout}
+            
+              {turn.prompt}
             
- )} - {/* Errors */} - {turn.output.stderr && ( -
-

-

-
-              {turn.output.stderr}
-            
-
- )} + {/* Assistant Response */} + {turn.output.stdout && ( +
+

+

+
+                {turn.output.stdout}
+              
+
+ )} - {/* Truncated Notice */} - {turn.output.truncated && ( -
-
- )} -
+ {/* Errors */} + {turn.output.stderr && ( +
+

+

+
+                {turn.output.stderr}
+              
+
+ )} + + {/* Truncated Notice */} + {turn.output.truncated && ( +
+
+ )} +
+ )} ); } /** - * PerTurnView - Display all turns as separate sections with connectors + * PerTurnView - Display all turns with collapsible sections + * Default behavior: only latest turn is expanded, others are collapsed */ function PerTurnView({ turns }: { turns: ConversationTurn[] }) { + // Default: only the latest turn is expanded + const [expandedTurns, setExpandedTurns] = React.useState>(() => { + if (turns.length === 0) return new Set(); + return new Set([turns[turns.length - 1].turn]); + }); + + const handleToggle = React.useCallback((turnNumber: number) => { + setExpandedTurns((prev) => { + const next = new Set(prev); + if (next.has(turnNumber)) { + next.delete(turnNumber); + } else { + next.add(turnNumber); + } + return next; + }); + }, []); + return (
{turns.map((turn, idx) => ( - + handleToggle(turn.turn)} + /> {/* Connector line between turns */} {idx < turns.length - 1 && (