import { useEffect, useMemo, useState } from "react" import { useQueryClient } from "@tanstack/react-query" 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 { MessageView } from "@/components/agents/ported" import { agentThreadKeys, 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 { 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({ thread }: AgentThreadViewProps) { const queryClient = useQueryClient() 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]) useEffect(() => { queryClient.setQueryData | undefined>( agentThreadKeys.all, (threads) => threads?.map((item) => item.id === thread.id ? { ...item, ...thread, messages: item.messages } : item ) ) }, [queryClient, thread]) 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} />
)}
) }