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(); }}
+ >
-
- {turn.turn === 1 ? '\u25B6' : '\u21B3'} {/* ▶ or ↳ */}
-
+ {isExpanded ? (
+
+ ) : (
+
+ )}
{formatMessage({ id: 'cli.details.turn' })} {turn.turn}
{isLatest && (
@@ -190,66 +202,92 @@ function TurnSection({ turn, isLatest }: TurnSectionProps) {
- {/* Turn Body */}
-
- {/* User Prompt */}
-
-
-
- {formatMessage({ id: 'cli-manager.streamPanel.userPrompt' })}
-
-
- {turn.prompt}
-
-
-
- {/* Assistant Response */}
- {turn.output.stdout && (
+ {/* Turn Body - Collapsible */}
+ {isExpanded && (
+
+ {/* User Prompt */}
-
- {formatMessage({ id: 'cli-manager.streamPanel.assistantResponse' })}
+
+ {formatMessage({ id: 'cli-manager.streamPanel.userPrompt' })}
-
- {turn.output.stdout}
+
+ {turn.prompt}
- )}
- {/* Errors */}
- {turn.output.stderr && (
-
-
-
- {formatMessage({ id: 'cli-manager.streamPanel.errors' })}
-
-
- {turn.output.stderr}
-
-
- )}
+ {/* Assistant Response */}
+ {turn.output.stdout && (
+
+
+
+ {formatMessage({ id: 'cli-manager.streamPanel.assistantResponse' })}
+
+
+ {turn.output.stdout}
+
+
+ )}
- {/* Truncated Notice */}
- {turn.output.truncated && (
-
-
- {formatMessage({ id: 'cli-manager.streamPanel.truncatedNotice' })}
-
- )}
-
+ {/* Errors */}
+ {turn.output.stderr && (
+
+
+
+ {formatMessage({ id: 'cli-manager.streamPanel.errors' })}
+
+
+ {turn.output.stderr}
+
+
+ )}
+
+ {/* Truncated Notice */}
+ {turn.output.truncated && (
+
+
+ {formatMessage({ id: 'cli-manager.streamPanel.truncatedNotice' })}
+
+ )}
+
+ )}
);
}
/**
- * 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 && (
@@ -296,14 +334,239 @@ function ConcatenatedView({ prompt, format, onFormatChange }: ConcatenatedViewPr
);
}
+// ========== Native View Components ==========
+
+/**
+ * Format token count with compact notation
+ */
+function formatTokenCount(count: number | undefined): string {
+ if (count == null || count === 0) return '0';
+ if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
+ if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`;
+ return count.toLocaleString();
+}
+
+/**
+ * NativeTokenDisplay - Compact token info line
+ */
+function NativeTokenDisplay({ tokens, className }: { tokens: NativeTokenInfo; className?: string }) {
+ return (
+
+
+
+ {formatTokenCount(tokens.total)}
+
+ {tokens.input != null && (
+ in: {formatTokenCount(tokens.input)}
+ )}
+ {tokens.output != null && (
+ out: {formatTokenCount(tokens.output)}
+ )}
+
+ );
+}
+
+/**
+ * NativeToolCallItem - Single tool call display
+ */
+function NativeToolCallItem({ toolCall, index }: { toolCall: NativeToolCall; index: number }) {
+ return (
+
+
+
+ {toolCall.name}
+ #{index + 1}
+
+
+ {toolCall.arguments && (
+
+
Input
+
+ {toolCall.arguments}
+
+
+ )}
+ {toolCall.output && (
+
+
Output
+
+ {toolCall.output}
+
+
+ )}
+
+
+ );
+}
+
+interface NativeTurnCardProps {
+ turn: NativeSessionTurn;
+ isLatest: boolean;
+ isExpanded: boolean;
+ onToggle: () => void;
+}
+
+/**
+ * NativeTurnCard - Single native conversation turn with collapsible content
+ */
+function NativeTurnCard({ turn, isLatest, isExpanded, onToggle }: NativeTurnCardProps) {
+ const { formatMessage } = useIntl();
+ const isUser = turn.role === 'user';
+ const RoleIcon = isUser ? User : Bot;
+
+ return (
+
+ {/* Header - Clickable */}
+ { if (e.key === 'Enter' || e.key === ' ') onToggle(); }}
+ >
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+ {turn.role}
+ #{turn.turnNumber}
+ {isLatest && (
+
+ {formatMessage({ id: 'cli-manager.streamPanel.latest' })}
+
+ )}
+
+
+ {turn.timestamp && (
+
+
+ {new Date(turn.timestamp).toLocaleTimeString()}
+
+ )}
+ {turn.tokens && }
+
+
+
+ {/* Content - Collapsible */}
+ {isExpanded && (
+
+ {turn.content && (
+
+ {turn.content}
+
+ )}
+
+ {/* Thoughts Section */}
+ {turn.thoughts && turn.thoughts.length > 0 && (
+
+
+
+ Thoughts
+ ({turn.thoughts.length})
+
+
+ {turn.thoughts.map((thought, i) => (
+ - {thought}
+ ))}
+
+
+ )}
+
+ {/* Tool Calls Section */}
+ {turn.toolCalls && turn.toolCalls.length > 0 && (
+
+
+
+ Tool Calls
+ ({turn.toolCalls.length})
+
+
+ {turn.toolCalls.map((tc, i) => (
+
+ ))}
+
+
+ )}
+
+ )}
+
+ );
+}
+
+interface NativeTurnViewProps {
+ turns: NativeSessionTurn[];
+}
+
+/**
+ * NativeTurnView - Display native session turns with collapsible cards
+ */
+function NativeTurnView({ turns }: NativeTurnViewProps) {
+ // 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].turnNumber]);
+ });
+
+ 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;
+ });
+ }, []);
+
+ if (turns.length === 0) {
+ return (
+
+ No native session data available
+
+ );
+ }
+
+ return (
+
+ {turns.map((turn, idx) => (
+
+ handleToggle(turn.turnNumber)}
+ />
+ {idx < turns.length - 1 && (
+
+ )}
+
+ ))}
+
+ );
+}
+
// ========== Main Component ==========
/**
* CliStreamPanel component - Elegant turn-based conversation view
*
* Displays CLI execution details with:
- * - Per-turn view with timeline layout
+ * - Per-turn view with collapsible timeline layout
* - Concatenated view for resume context
+ * - Native view for detailed CLI session data
* - Format selection (Plain/YAML/JSON)
*/
export function CliStreamPanel({
@@ -318,6 +581,11 @@ export function CliStreamPanel({
const { data: execution, isLoading } = useCliExecutionDetail(open ? executionId : null);
+ // Load native session only when native view is selected
+ const { data: nativeSession, isLoading: nativeLoading } = useNativeSession(
+ viewMode === 'native' && open ? executionId : null
+ );
+
// Build concatenated prompt
const concatenatedPrompt = React.useMemo(() => {
if (!execution?.turns) return '';
@@ -386,18 +654,18 @@ export function CliStreamPanel({
) : execution?.turns && execution.turns.length > 0 ? (
<>
- {/* View Toggle - Only show for multi-turn conversations */}
- {execution.turns.length > 1 && (
-
-
+ {/* View Toggle - Show for all conversations */}
+
+
+ {execution.turns.length > 1 && (
{formatMessage({ id: 'cli-manager.streamPanel.concatenatedView' })}
-
- )}
+ )}
+
+
{/* Content */}
{viewMode === 'per-turn' ? (
- ) : (
+ ) : viewMode === 'concatenated' ? (
- )}
+ ) : viewMode === 'native' ? (
+ nativeLoading ? (
+
+
+ Loading native session...
+
+ ) : nativeSession ? (
+
+ ) : (
+
+ No native session data available
+
+ )
+ ) : null}
{/* Footer Actions */}