import { useState, useEffect, useRef } from "react"; import type { SessionEvent, EventPayload } from "../types"; import { esc, truncate, cn, extractEventText, isConversationClearedStatus } from "../lib/utils"; // ============================================================ // Tool Trace State // ============================================================ interface TraceHost { id: string; kind: "assistant" | "orphan"; assistantContent: string; entryKinds: ("use" | "result")[]; } interface ToolTraceState { nextHostId: number; activeHostId: string | null; hosts: TraceHost[]; } function createTraceState(): ToolTraceState { return { nextHostId: 1, activeHostId: null, hosts: [] }; } function addAssistantHost(state: ToolTraceState, content: string): { state: ToolTraceState; host: TraceHost } { const host: TraceHost = { id: `trace-${state.nextHostId}`, kind: "assistant", assistantContent: content, entryKinds: [], }; return { state: { nextHostId: state.nextHostId + 1, activeHostId: host.id, hosts: [...state.hosts, host], }, host, }; } function clearActiveHost(state: ToolTraceState): ToolTraceState { if (!state.activeHostId) return state; return { ...state, activeHostId: null }; } function addTraceEntry(state: ToolTraceState, entryKind: "use" | "result"): { state: ToolTraceState; host: TraceHost; createdHost: TraceHost | null; } { let host = state.hosts.find((h) => h.id === state.activeHostId); let createdHost: TraceHost | null = null; if (!host) { createdHost = { id: `trace-${state.nextHostId}`, kind: "orphan", assistantContent: "", entryKinds: [], }; host = createdHost; } const updatedHost = { ...host, entryKinds: [...host.entryKinds, entryKind] }; const newHosts = state.hosts.map((h) => (h.id === updatedHost.id ? updatedHost : h)); if (createdHost) newHosts.push(createdHost); return { state: { nextHostId: createdHost ? state.nextHostId + 1 : state.nextHostId, activeHostId: (createdHost || host).id, hosts: newHosts, }, host: updatedHost, createdHost, }; } // ============================================================ // Message Types // ============================================================ interface UserMessage { kind: "user"; content: string; } interface AssistantMessage { kind: "assistant"; content: string; traceEntries: TraceEntry[]; traceExpanded: boolean; traceId: string; } interface TraceEntry { entryKind: "use" | "result"; toolName?: string; toolInput?: unknown; content?: string; output?: string; isError?: boolean; } interface SystemMessage { kind: "system"; content: string; } interface PermissionMessage { kind: "permission"; requestId: string; toolName: string; toolInput: unknown; description: string; } interface AskUserMessage { kind: "ask_user"; requestId: string; questions: import("../types").Question[]; description: string; } interface PlanMessage { kind: "plan"; requestId: string; planContent: string; description: string; } interface LoadingMessage { kind: "loading"; verb: string; startTime: number; } type DisplayMessage = | UserMessage | AssistantMessage | SystemMessage | PermissionMessage | AskUserMessage | PlanMessage | LoadingMessage; // ============================================================ // Spinner // ============================================================ const SPINNER_FRAMES = ["·", "✢", "✱", "✶", "✻", "✽"]; const SPINNER_CYCLE = [...SPINNER_FRAMES, ...SPINNER_FRAMES.slice().reverse()]; const SPINNER_VERBS = [ "Accomplishing", "Baking", "Calculating", "Clauding", "Cogitating", "Computing", "Considering", "Contemplating", "Cooking", "Crafting", "Creating", "Crunching", "Deliberating", "Doing", "Effecting", "Generating", "Hatching", "Ideating", "Imagining", "Inferring", "Manifesting", "Mulling", "Pondering", "Processing", "Ruminating", "Simmering", "Synthesizing", "Thinking", "Tinkering", "Working", ]; // ============================================================ // EventStream Component // ============================================================ interface EventStreamProps { messages: DisplayMessage[]; onApprovePermission: (requestId: string) => void; onRejectPermission: (requestId: string) => void; onSubmitAnswers: (requestId: string, answers: Record, questions: import("../types").Question[]) => void; onSubmitPlanResponse: (requestId: string, value: string, feedback?: string) => void; } export function EventStream({ messages, onApprovePermission, onRejectPermission, onSubmitAnswers, onSubmitPlanResponse, }: EventStreamProps) { const scrollRef = useRef(null); useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [messages]); return (
{messages.map((msg, i) => ( ))}
); } function MessageRow({ message, onApprovePermission, onRejectPermission, onSubmitAnswers, onSubmitPlanResponse }: { message: DisplayMessage; onApprovePermission: (requestId: string) => void; onRejectPermission: (requestId: string) => void; onSubmitAnswers: (requestId: string, answers: Record, questions: import("../types").Question[]) => void; onSubmitPlanResponse: (requestId: string, value: string, feedback?: string) => void; }) { switch (message.kind) { case "user": return ; case "assistant": return ; case "system": return ; case "permission": return ( onApprovePermission(message.requestId)} onReject={() => onRejectPermission(message.requestId)} /> ); case "ask_user": return ( onSubmitAnswers(message.requestId, answers, message.questions)} onSkip={() => onRejectPermission(message.requestId)} /> ); case "plan": return ( onSubmitPlanResponse(message.requestId, value, feedback)} /> ); case "loading": return ; default: return null; } } // ============================================================ // Sub-Components // ============================================================ function UserBubble({ content }: { content: string }) { return (
{esc(content)}
); } function formatAssistantContent(content: string): string { let html = esc(content); // Code blocks html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, _lang, code) => { return `
${code.trim()}
`; }); // Inline code html = html.replace(/`([^`]+)`/g, '$1'); // Bold html = html.replace(/\*\*([^*]+)\*\*/g, "$1"); return html; } function AssistantBubble({ content, traceEntries }: { content: string; traceEntries: TraceEntry[] }) { const [expanded, setExpanded] = useState(false); return (
{content && (
)} {traceEntries.length > 0 && (
{expanded && (
{traceEntries.map((entry, i) => ( ))}
)}
)}
); } function ToolCard({ entry }: { entry: TraceEntry }) { const [expanded, setExpanded] = useState(false); if (entry.entryKind === "use") { const inputStr = typeof entry.toolInput === "string" ? entry.toolInput : JSON.stringify(entry.toolInput, null, 2); return (
setExpanded(!expanded)} >
{entry.toolName || "tool"}
{expanded && (
            {truncate(inputStr, 2000)}
          
)}
); } const contentStr = typeof entry.output === "string" ? entry.output : JSON.stringify(entry.output, null, 2); return (
setExpanded(!expanded)} >
{entry.isError ? "✕" : "✓"} {entry.isError ? "Error" : "Result"}
{expanded && (
          {truncate(contentStr, 2000)}
        
)}
); } function SystemBubble({ content }: { content: string }) { return (
{esc(content)}
); } function PermissionPrompt({ requestId, toolName, toolInput, description, onApprove, onReject, }: { requestId: string; toolName: string; toolInput: unknown; description: string; onApprove: () => void; onReject: () => void; }) { const inputStr = typeof toolInput === "string" ? toolInput : JSON.stringify(toolInput, null, 2); return (
Permission Request
{description &&
{esc(description)}
}
{esc(toolName)}
{toolName !== "AskUserQuestion" && (
          {truncate(inputStr, 500)}
        
)}
); } function AskUserPanel({ questions, description, onSubmit, onSkip, }: { requestId: string; questions: import("../types").Question[]; description: string; onSubmit: (answers: Record) => void; onSkip: () => void; }) { const [answers, setAnswers] = useState>({}); const [otherTexts, setOtherTexts] = useState>({}); const handleSelect = (qIdx: number, oIdx: number, multiSelect: boolean) => { if (multiSelect) { const current = (answers[qIdx] as number[]) || []; const next = current.includes(oIdx) ? current.filter((i) => i !== oIdx) : [...current, oIdx]; setAnswers({ ...answers, [qIdx]: next }); } else { setAnswers({ ...answers, [qIdx]: oIdx }); } }; const handleOtherSubmit = (qIdx: number) => { const text = otherTexts[qIdx]?.trim(); if (!text) return; setAnswers({ ...answers, [qIdx]: text }); setOtherTexts({ ...otherTexts, [qIdx]: "" }); }; const handleSubmit = () => { const mapped: Record = {}; for (const [qIdx, val] of Object.entries(answers)) { const q = questions[parseInt(qIdx, 10)]; if (!q) continue; if (typeof val === "number") { mapped[qIdx] = q.options?.[val]?.label || String(val); } else if (Array.isArray(val)) { mapped[qIdx] = val.map((i) => q.options?.[i]?.label || String(i)); } else { mapped[qIdx] = val; } } onSubmit(mapped); }; if (questions.length <= 1) { const q = questions[0] || { question: description, options: [], multiSelect: false }; const selectedIdx = answers[0]; const multiSelect = q.multiSelect || false; return (
{esc(description || q.question || "Question")}
{(q.options || []).map((opt, j) => { const isSelected = multiSelect ? ((answers[0] as number[]) || []).includes(j) : selectedIdx === j; return ( ); })}
setOtherTexts({ ...otherTexts, [0]: e.target.value })} placeholder="Other..." className="flex-1 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-brand focus:outline-none" onKeyDown={(e) => e.key === "Enter" && handleOtherSubmit(0)} />
); } // Multiple questions — tab layout const [activeTab, setActiveTab] = useState(0); return (
{esc(description || "Questions")}
{questions.map((q, i) => ( ))}
{questions[activeTab] && ( setOtherTexts({ ...otherTexts, [qIdx]: text })} onOtherSubmit={handleOtherSubmit} /> )}
{activeTab + 1} / {questions.length}
); } function QuestionTab({ question, qIdx, answers, otherTexts, onSelect, onOtherTextChange, onOtherSubmit, }: { question: import("../types").Question; qIdx: number; answers: Record; otherTexts: Record; onSelect: (qIdx: number, oIdx: number, multiSelect: boolean) => void; onOtherTextChange: (qIdx: number, text: string) => void; onOtherSubmit: (qIdx: number) => void; }) { const multiSelect = question.multiSelect || false; return (
{esc(question.question)}
{(question.options || []).map((opt, j) => { const isSelected = multiSelect ? ((answers[qIdx] as number[]) || []).includes(j) : answers[qIdx] === j; return ( ); })}
onOtherTextChange(qIdx, e.target.value)} placeholder="Other..." className="flex-1 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-brand focus:outline-none" onKeyDown={(e) => e.key === "Enter" && onOtherSubmit(qIdx)} />
); } function PlanPanel({ planContent, description, onSubmit, }: { requestId: string; planContent: string; description: string; onSubmit: (value: string, feedback?: string) => void; }) { const [selected, setSelected] = useState(null); const [feedback, setFeedback] = useState(""); const isEmpty = !planContent || !planContent.trim(); const handleSubmit = () => { if (!selected) return; onSubmit(selected, selected === "no" ? feedback : undefined); }; return (
{isEmpty ? "Exit plan mode?" : "Ready to code?"}
{!isEmpty && (
)}
{isEmpty ? ( <> setSelected("yes-default")} label="Yes" /> setSelected("no")} label="No" /> ) : ( <> setSelected("yes-accept-edits")} label="Yes, auto-accept edits" desc="Approve plan and auto-accept file edits" /> setSelected("yes-default")} label="Yes, manually approve edits" desc="Approve plan but confirm each edit" /> setSelected("no")} label="No, keep planning" desc="Provide feedback to refine the plan" /> )}
{selected === "no" && (