import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { apiFetchSession, apiSendControl, apiInterrupt } from '../api/client'; import type { Session, SessionEvent } from '../types'; import { isClosedSessionStatus, formatTime, cn } from '../lib/utils'; import { Info } from 'lucide-react'; import { RCSChatAdapter } from '../lib/rcs-chat-adapter'; import type { ThreadEntry, PendingPermission } from '../lib/types'; import { StatusBadge } from '../components/Navbar'; import { TaskPanel } from '../components/TaskPanel'; import { PermissionPromptView, AskUserPanelView, PlanPanelView } from '../components/PermissionViews'; // Unified chat components import { ChatView } from '../../components/chat/ChatView'; import { ChatInput } from '../../components/chat/ChatInput'; import { TooltipProvider } from '../../components/ui/tooltip'; // ACP chat components import { ACPClient, DisconnectRequestedError } from '../acp/client'; import { createRelayClient } from '../acp/relay-client'; import { ACPMain } from '../../components/ACPMain'; interface SessionDetailProps { sessionId: string; } export function SessionDetail({ sessionId }: SessionDetailProps) { const [session, setSession] = useState(null); const [sessionStatus, setSessionStatus] = useState(null); const [error, setError] = useState(''); const [taskPanelOpen, setTaskPanelOpen] = useState(false); const [showMeta, setShowMeta] = useState(false); const [entries, setEntries] = useState([]); const [isLoading, setIsLoading] = useState(false); const [pendingPermissions, setPendingPermissions] = useState([]); const adapterRef = useRef(null); // Create RCSChatAdapter const adapter = useMemo( () => new RCSChatAdapter(sessionId, setEntries, { onStatusChange: status => { setSessionStatus(status); }, onError: err => { console.error('[RCSChatAdapter] error:', err); }, onPermissionRequest: permission => { setPendingPermissions(prev => { if (prev.some(p => p.requestId === permission.requestId)) return prev; return [...prev, permission]; }); }, }), [sessionId], ); useEffect(() => { adapterRef.current = adapter; return () => { adapter.disconnect(); }; }, [adapter]); // Load session data and initialize adapter useEffect(() => { let cancelled = false; async function load() { setError(''); try { const sess = await apiFetchSession(sessionId); if (cancelled) return; setSession(sess); setSessionStatus(sess.status); } catch (err) { if (cancelled) return; setError(err instanceof Error ? err.message : 'Failed to load session'); return; } try { await adapter.init(); } catch (err) { console.warn('Failed to init adapter:', err); } } load(); return () => { cancelled = true; }; }, [sessionId, adapter]); const closed = isClosedSessionStatus(sessionStatus); // Send message via ChatInput const handleSubmit = useCallback( async (message: import('../../src/lib/types').ChatInputMessage) => { const text = message.text.trim(); if (!text || closed) return; setIsLoading(true); try { await adapter.sendMessage(text, message.images); } catch (err) { console.error('Send failed:', err); } }, [adapter, closed], ); // Interrupt const handleInterrupt = useCallback(async () => { try { await adapter.interrupt(); } catch (err) { console.error('Interrupt failed:', err); } finally { setIsLoading(false); } }, [adapter]); // Mark loading done when last assistant message stops streaming useEffect(() => { if (entries.length === 0) return; const last = entries[entries.length - 1]; if (last?.type === 'assistant_message' || last?.type === 'tool_call') { // If the last entry is no longer a streaming tool, consider loading done if (last.type === 'tool_call' && last.toolCall.status === 'running') return; setIsLoading(false); } }, [entries]); // Permission actions const handleApprovePermission = useCallback( async (requestId: string) => { try { await adapter.respondPermission(requestId, true); } catch (err) { console.error('Failed to approve:', err); } setPendingPermissions(prev => prev.filter(p => p.requestId !== requestId)); }, [adapter], ); const handleRejectPermission = useCallback( async (requestId: string) => { try { await adapter.respondPermission(requestId, false); } catch (err) { console.error('Failed to reject:', err); } setPendingPermissions(prev => prev.filter(p => p.requestId !== requestId)); }, [adapter], ); const handleSubmitAnswers = useCallback( async (requestId: string, answers: Record, questions: import('../types').Question[]) => { try { await apiSendControl(sessionId, { type: 'permission_response', approved: true, request_id: requestId, updated_input: { questions, answers }, }); } catch (err) { console.error('Failed to submit answers:', err); } setPendingPermissions(prev => prev.filter(p => p.requestId !== requestId)); }, [sessionId], ); const handleSubmitPlanResponse = useCallback( async (requestId: string, value: string, feedback?: string) => { try { if (value === 'no') { await apiSendControl(sessionId, { type: 'permission_response', approved: false, request_id: requestId, ...(feedback ? { message: feedback } : {}), }); } else { const modeMap: Record = { 'yes-accept-edits': 'acceptEdits', 'yes-default': 'default', }; await apiSendControl(sessionId, { type: 'permission_response', approved: true, request_id: requestId, updated_permissions: [{ type: 'setMode', mode: modeMap[value] || 'default', destination: 'session' }], }); } } catch (err) { console.error('Failed to submit plan response:', err); } setPendingPermissions(prev => prev.filter(p => p.requestId !== requestId)); }, [sessionId], ); if (error) { return ( ); } if (!session) { return (
Loading session...
); } // ACP session — render ACP relay chat if (session.source === 'acp' && session.environment_id) { return ; } return (

{session.title || session.id}

{/* Session Header */}

{session.title || session.id}

{sessionStatus && } {formatTime(session.created_at)}
{showMeta && (
Session {session.id}
{session.environment_id && (
Environment{' '} {session.environment_id}
)}
)}
{/* Chat messages — unified ChatView */} {/* Unified Permission Panel — above input */} {pendingPermissions.length > 0 && (
{pendingPermissions.map(req => ( handleApprovePermission(req.requestId)} onReject={() => handleRejectPermission(req.requestId)} onSubmitAnswers={handleSubmitAnswers} onSubmitPlan={handleSubmitPlanResponse} /> ))}
)} {/* Unified ChatInput — claude.ai style */} {/* Task Panel */} {taskPanelOpen && setTaskPanelOpen(false)} />}
); } // ============================================================ // Permission Event View — routes to correct UI // ============================================================ function PermissionEventView({ request, onApprove, onReject, onSubmitAnswers, onSubmitPlan, }: { request: PendingPermission; onApprove: () => void; onReject: () => void; onSubmitAnswers: ( requestId: string, answers: Record, questions: import('../types').Question[], ) => void; onSubmitPlan: (requestId: string, value: string, feedback?: string) => void; }) { const toolName = request.toolName; const toolInput = request.toolInput; const description = request.description || ''; if (toolName === 'AskUserQuestion') { const questions = (toolInput.questions as import('../types').Question[]) || []; return ( onSubmitAnswers(request.requestId, answers, questions)} onSkip={onReject} /> ); } if (toolName === 'ExitPlanMode') { const planContent = (toolInput.plan as string) || ''; return ( onSubmitPlan(request.requestId, value, feedback)} /> ); } return ( ); } // ============================================================ // ACP Session Detail — renders ACP relay chat in session page // ============================================================ function ACPSessionDetail({ sessionId, agentId }: { sessionId: string; agentId: string }) { const [client, setClient] = useState(null); const [connectionState, setConnectionState] = useState<'disconnected' | 'connecting' | 'connected' | 'error'>( 'disconnected', ); const [error, setError] = useState(null); const clientRef = useRef(null); useEffect(() => { const relayClient = createRelayClient(agentId); relayClient.setConnectionStateHandler((state, err) => { setConnectionState(state); setError(err || null); }); clientRef.current = relayClient; setClient(relayClient); relayClient.connect().catch(e => { if (e instanceof DisconnectRequestedError) return; setError((e as Error).message); setConnectionState('error'); }); return () => { relayClient.disconnect(); clientRef.current = null; setClient(null); setConnectionState('disconnected'); }; }, [agentId]); return (
{error && connectionState === 'error' && (
{error}
)} {connectionState === 'connecting' && (

Connecting to agent...

)} {connectionState === 'error' && !client && (

Connection Failed

{error}

)} {client && connectionState === 'connected' && (
)}
); }