import { useEffect, useMemo, useRef, useState } from "react"; import { ChevronDown } from "lucide-react"; import { AgentPromptBar } from "@/components/agents/AgentPromptBar"; import { AgentsShell } from "@/components/agents/AgentsSidebar"; import { MessageView, summarizeChangedFiles, type MessageViewScrollControl, } from "@/components/agents/ported"; import type { SessionUser } from "@/lib/api"; import type { AgentThread, Message } from "@/lib/agents/types"; import { useSendAgentMessage } from "@/lib/agents/queries"; import { dropPendingPrompts, getPendingPrompts, type PendingPrompt, } from "@/lib/agents/pendingPrompts"; import { useAgentThreadStream } from "@/lib/agents/useThreadStream"; import { useModelOptions, type ModelSelection } from "@/lib/agents/useModelOptions"; interface AgentThreadViewProps { user: SessionUser; thread: AgentThread; } const PROMPT_OVERLAY_INSET = 128; export function AgentThreadView({ user, thread }: AgentThreadViewProps) { const sendMessage = useSendAgentMessage(thread.id); useAgentThreadStream(thread.id, thread.status === "running"); const scrollControlRef = useRef(null); const [showScrollToBottom, setShowScrollToBottom] = useState(false); const [pendingPrompts, setPendingPrompts] = useState(() => getPendingPrompts(thread.id), ); const { models, defaultSelection } = useModelOptions(); const threadSelection = useMemo(() => { if (!thread.model || !thread.effort) return null; const supported = models.some( (m) => m.id === thread.model && m.efforts.includes(thread.effort ?? ""), ); if (!supported) return null; return { modelId: thread.model, effort: thread.effort }; }, [models, thread.model, thread.effort]); const [selection, setSelection] = useState(null); useEffect(() => { if (selection !== null) return; if (threadSelection) setSelection(threadSelection); else if (defaultSelection) setSelection(defaultSelection); }, [defaultSelection, selection, threadSelection]); const userMessageTexts = useMemo(() => { return new Set( thread.messages .filter((m) => m.author === "user") .map((m) => m.chunks .filter((c) => c.kind === "text") .map((c) => (c as { kind: "text"; text: string }).text) .join(""), ), ); }, [thread.messages]); useEffect(() => { setPendingPrompts((prev) => { if (prev.length === 0) return prev; const next = dropPendingPrompts(thread.id, (entry) => userMessageTexts.has(entry.prompt), ); return next.length === prev.length ? prev : next; }); }, [thread.id, userMessageTexts]); const displayMessages = useMemo(() => { if (pendingPrompts.length === 0) return thread.messages; const baseTimestamp = new Date().toISOString(); const result = thread.messages.slice(); pendingPrompts.forEach((entry, i) => { const synth: Message = { id: `pending-user-${i}`, author: "user", timestamp: baseTimestamp, chunks: [{ kind: "text", text: entry.prompt }], }; const at = Math.min(Math.max(entry.insertAt, 0), result.length); result.splice(at, 0, synth); }); return result; }, [thread.messages, pendingPrompts]); const changedFiles = useMemo(() => { const agentMessages = thread.messages.filter((m) => m.author === "agent"); const allChunks = agentMessages.flatMap((m) => m.chunks); return summarizeChangedFiles(allChunks); }, [thread.messages]); const hasMessages = displayMessages.length > 0; const isStreaming = thread.status === "running" || pendingPrompts.length > 0; return (
{hasMessages ? (
{changedFiles.length > 0 && (
{changedFiles.length} Files Changed
{changedFiles.map((file) => (
{file.filePath} +{file.additions}
))}
)}
{showScrollToBottom && ( )} sendMessage.mutate({ content, model_id: selection?.modelId ?? null, effort: selection?.effort ?? null, }) } models={models} selection={selection ?? threadSelection ?? defaultSelection} onSelectionChange={setSelection} />
) : (

This thread has no messages yet.

sendMessage.mutate({ content, model_id: selection?.modelId ?? null, effort: selection?.effort ?? null, }) } models={models} selection={selection ?? threadSelection ?? defaultSelection} onSelectionChange={setSelection} />
)}
); }