import { useEffect, useMemo, useState } from "react"; import type { SessionUser } from "@/lib/api"; import type { PendingPrompt } from "@/lib/agents/pendingPrompts"; import type { AgentThread, Message } from "@/lib/agents/types"; import type { ModelSelection } from "@/lib/agents/useModelOptions"; import { AgentPromptBar } from "@/components/agents/AgentPromptBar"; import { AgentsShell } from "@/components/agents/AgentsSidebar"; import { MessageView } from "@/components/agents/ported"; import { useCancelAgentThread, useSendAgentMessage } from "@/lib/agents/queries"; import { dropPendingPrompts, getPendingPrompts } from "@/lib/agents/pendingPrompts"; import { useAgentThreadStream } from "@/lib/agents/useThreadStream"; import { useModelOptions } from "@/lib/agents/useModelOptions"; interface AgentThreadViewProps { user: SessionUser; thread: AgentThread; } function messageText(message: Message): string { return message.chunks .filter((chunk) => chunk.kind === "text") .map((chunk) => chunk.text) .join(""); } function messageImageKey(message: Message): string { return message.chunks .filter((chunk) => chunk.kind === "image") .map((chunk) => `${chunk.mimeType}:${chunk.base64}`) .join("\u0000"); } function pendingImageKey(entry: PendingPrompt): string { return (entry.images ?? []) .map((image) => `${image.mimeType}:${image.base64}`) .join("\u0000"); } function isPendingPromptConfirmed(entry: PendingPrompt, messages: Array): boolean { return messages.slice(entry.insertAt).some((message) => { if (message.author !== "user") return false; return messageText(message) === entry.prompt && messageImageKey(message) === pendingImageKey(entry); }); } export function AgentThreadView({ user, thread }: AgentThreadViewProps) { const sendMessage = useSendAgentMessage(thread.id); const cancelThread = useCancelAgentThread(thread.id); useAgentThreadStream(thread.id, thread.status === "running"); 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); const activeSelection = selection ?? threadSelection ?? defaultSelection; useEffect(() => { setPendingPrompts((prev) => { if (prev.length === 0) return prev; const next = dropPendingPrompts(thread.id, (entry) => isPendingPromptConfirmed(entry, thread.messages), ); return next.length === prev.length ? prev : next; }); }, [thread.id, thread.messages]); 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 chunks: Message["chunks"] = [...(entry.images ?? [])]; if (entry.prompt) chunks.push({ kind: "text", text: entry.prompt }); const synth: Message = { id: `pending-user-${i}`, author: "user", timestamp: baseTimestamp, chunks, }; const at = Math.min(Math.max(entry.insertAt, 0), result.length); result.splice(at, 0, synth); }); return result; }, [thread.messages, pendingPrompts]); const hasMessages = displayMessages.length > 0; const hasActiveRun = thread.status === "running"; const isStreaming = hasActiveRun || pendingPrompts.length > 0; return (
{hasMessages ? (
sendMessage.mutate({ content, images, model_id: activeSelection?.modelId ?? null, effort: activeSelection?.effort ?? null, }) } onStop={() => cancelThread.mutate()} stopping={cancelThread.isPending} models={models} selection={activeSelection} onSelectionChange={setSelection} />
) : (

This thread has no messages yet.

sendMessage.mutate({ content, images, model_id: activeSelection?.modelId ?? null, effort: activeSelection?.effort ?? null, }) } onStop={() => cancelThread.mutate()} stopping={cancelThread.isPending} models={models} selection={activeSelection} onSelectionChange={setSelection} />
)}
); }