import { useCallback, useMemo, useState } from "react" import { Link } from "@tanstack/react-router" import { useStreamContext as useAgentThreadStream } from "@langchain/react" import { Map as MapIcon } from "lucide-react" import type { AgentThread, Message } from "@/lib/agents/types" import type { ModelSelection } from "@/lib/agents/provider/useModelOptions" import { AgentGitPanel, PANEL_MIN_CHAT_WIDTH, readStoredPanelCollapsed, writeStoredPanelCollapsed, } from "@/components/agents/AgentGitPanel" import { AgentPromptBar } from "@/components/agents/AgentPromptBar" import { Messages } from "@/components/agents/messages" import { streamMessagesToUi } from "@/lib/agents/streamMessagesToUi" import { messageArrivalTimestamp } from "@/lib/agents/messageTimestamps" import { useSubmitAgentMessage } from "@/lib/agents/provider/useSubmitAgentMessage" import { useModelOptions } from "@/lib/agents/provider/useModelOptions" import { useIsMobile } from "@/lib/useIsMobile" import { cn } from "@/lib/utils" interface AgentThreadViewProps { thread: AgentThread } // The stream lives at the `/agents` layout (one persistent provider that // survives the home → thread navigation), so this view only consumes it. export function AgentThreadView({ thread }: AgentThreadViewProps) { const sendMessage = useSubmitAgentMessage(thread.id) const stream = useAgentThreadStream() const isMobile = useIsMobile() 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 const [planMode, setPlanMode] = useState(null) const activePlanMode = planMode ?? thread.planMode ?? false // Own the git panel's collapsed state so the plan banner can reserve space for // the floating expand button the panel renders while collapsed. const [panelCollapsed, setPanelCollapsed] = useState(() => readStoredPanelCollapsed() ) const handlePanelCollapsedChange = useCallback((next: boolean) => { setPanelCollapsed(next) writeStoredPanelCollapsed(next) }, []) const baseMessages = useMemo>(() => { const live = streamMessagesToUi( stream.messages, stream.toolCalls, stream.subagents, messageArrivalTimestamp ) if (live.length > 0) return live // Optimistic transcript seeded by `AgentsHome` on thread creation (the // only case where a fetched thread carries messages — `getThread` returns // none). Bridges the brief gap before the SDK's optimistic `submit` echo // lands in `stream.messages`. if (thread.messages.length > 0) return thread.messages return live }, [stream.messages, stream.toolCalls, stream.subagents, thread.messages]) const hasMessages = baseMessages.length > 0 const isStreaming = thread.status === "running" || stream.isLoading const isThinking = stream.isLoading const settingUpSandbox = isThinking && baseMessages.length === 0 // The transcript hydrates from the SDK (`GET …/state` → `stream.messages`). // Show a loading state during that one-time fetch instead of the empty state. const isHydrating = stream.isThreadLoading && !hasMessages return (
{thread.planStatus && thread.planStatus !== "approved" && thread.planStatus !== "cancelled" && ( {thread.planStatus === "ready" ? "A plan is ready for your review." : thread.planStatus === "revising" ? "The agent is revising the plan." : "The agent is writing a plan."} Review plan → )} {hasMessages ? (
sendMessage.mutateAsync({ content, images, model_id: activeSelection?.modelId ?? null, effort: activeSelection?.effort ?? null, plan_mode: activePlanMode, }) } models={models} selection={activeSelection} onSelectionChange={setSelection} planMode={activePlanMode} onPlanModeChange={setPlanMode} />
) : isHydrating ? (

Loading conversation…

) : (

This thread has no messages yet.

sendMessage.mutateAsync({ content, images, model_id: activeSelection?.modelId ?? null, effort: activeSelection?.effort ?? null, }) } models={models} selection={activeSelection} onSelectionChange={setSelection} />
)}
) }