// ======================================== // NativeSessionPanel Component // ======================================== // Dialog for displaying native CLI session content (Gemini/Codex/Qwen) import * as React from 'react'; import { useIntl } from 'react-intl'; import { Copy, Clock, Hash, FolderOpen, FileJson, Loader2, AlertCircle, } from 'lucide-react'; import { Button } from '@/components/ui/Button'; import { Badge } from '@/components/ui/Badge'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/Dialog'; import { useNativeSession } from '@/hooks/useNativeSession'; import { useNativeSessionByPath } from '@/hooks/useNativeSessionByPath'; import { SessionTimeline } from './SessionTimeline'; import { getToolVariant } from '@/lib/cli-tool-theme'; import type { NativeSessionListItem } from '@/lib/api'; // ========== Types ========== export interface NativeSessionPanelProps { /** Legacy: CCW execution ID for lookup */ executionId?: string; /** New: Session metadata with path for direct file loading */ session?: NativeSessionListItem | null; open: boolean; onOpenChange: (open: boolean) => void; } // ========== Helpers ========== /** * Truncate a string to a max length with ellipsis */ function truncate(str: string, maxLen: number): string { if (str.length <= maxLen) return str; return str.slice(0, maxLen) + '...'; } /** * Copy text to clipboard */ async function copyToClipboard(text: string): Promise { try { await navigator.clipboard.writeText(text); return true; } catch { return false; } } // ========== Main Component ========== /** * NativeSessionPanel - Dialog for displaying native CLI session content * * Shows session metadata, token summary, and all conversation turns * with thoughts and tool calls for Gemini/Codex/Qwen native sessions. * * Supports two modes: * - executionId: Look up session via CCW database * - session: Load session directly from file path */ export function NativeSessionPanel({ executionId, session, open, onOpenChange, }: NativeSessionPanelProps) { const { formatMessage } = useIntl(); // Use appropriate hook based on what's provided // Priority: session (path-based) > executionId (lookup-based) const pathBasedResult = useNativeSessionByPath( open && session ? session.filePath : null, session?.tool ); const idBasedResult = useNativeSession( open && !session && executionId ? executionId : null ); // Determine which result to use const { data, isLoading, error } = session ? pathBasedResult : idBasedResult; const [copiedField, setCopiedField] = React.useState(null); const handleCopy = React.useCallback(async (text: string, field: string) => { const ok = await copyToClipboard(text); if (ok) { setCopiedField(field); setTimeout(() => setCopiedField(null), 2000); } }, []); return (
{formatMessage({ id: 'nativeSession.title', defaultMessage: 'Native Session' })} {(data || session) && (
{(data?.tool || session?.tool || 'unknown').toUpperCase()} {data?.model && ( {data.model} )} {(data?.sessionId || session?.sessionId) && ( {truncate(data?.sessionId || session?.sessionId || '', 16)} )}
)}
{(data || session) && (
{new Date(data?.startTime || session?.createdAt || '').toLocaleString()} {(data?.workingDir || session?.projectHash) && ( {data?.workingDir || session?.projectHash} )} {data?.projectHash && ( {truncate(data.projectHash, 12)} )} {data && {data.turns.length} {formatMessage({ id: 'nativeSession.meta.turns', defaultMessage: 'turns' })}}
)}
{/* Content Area with SessionTimeline */} {isLoading ? (
{formatMessage({ id: 'nativeSession.loading', defaultMessage: 'Loading session...' })}
) : error ? (
{formatMessage({ id: 'nativeSession.error', defaultMessage: 'Failed to load session' })}

{formatMessage({ id: 'nativeSession.errorHint', defaultMessage: 'The session file may have been moved or deleted.' })}

) : data ? (
) : (
{formatMessage({ id: 'nativeSession.empty', defaultMessage: 'No session data available' })}
)} {/* Footer Actions */} {data && (
)}
); }