"use client"; import { v4 as uuidv4 } from "uuid"; import { useState, useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { ArrowLeft, GitBranch, Terminal, Clock } from "lucide-react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ThreadSwitcher } from "./thread-switcher"; import { ThreadMetadata } from "./types"; import { useStream } from "@langchain/langgraph-sdk/react"; import { ManagerGraphState } from "@open-swe/shared/open-swe/manager/types"; import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types"; import { GraphState, CacheMetrics } from "@open-swe/shared/open-swe/types"; import { ActionsRenderer } from "./actions-renderer"; import { ThemeToggle } from "../theme-toggle"; import { HumanMessage } from "@langchain/core/messages"; import { DO_NOT_RENDER_ID_PREFIX, PROGRAMMER_GRAPH_ID, PLANNER_GRAPH_ID, } from "@open-swe/shared/constants"; import { useThreadStatus } from "@/hooks/useThreadStatus"; import { cn } from "@/lib/utils"; import { StickyToBottomContent, ScrollToBottom, } from "../../utils/scroll-utils"; import { ManagerChat } from "./manager-chat"; import { CancelStreamButton } from "./cancel-stream-button"; import { ErrorState } from "./types"; import { CustomNodeEvent, isCustomNodeEvent, } from "@open-swe/shared/open-swe/custom-node-events"; import { StickToBottom } from "use-stick-to-bottom"; import { TokenUsage } from "./token-usage"; interface ThreadViewProps { stream: ReturnType>; displayThread: ThreadMetadata; allDisplayThreads: ThreadMetadata[]; onBackToHome: () => void; } export function ThreadView({ stream, displayThread, allDisplayThreads, onBackToHome, }: ThreadViewProps) { const [chatInput, setChatInput] = useState(""); const [selectedTab, setSelectedTab] = useState<"planner" | "programmer">( "planner", ); const [plannerSession, setPlannerSession] = useState(); const [programmerSession, setProgrammerSession] = useState(); const [errorState, setErrorState] = useState(null); const [customPlannerNodeEvents, setCustomPlannerNodeEvents] = useState< CustomNodeEvent[] >([]); const [customProgrammerNodeEvents, setCustomProgrammerNodeEvents] = useState< CustomNodeEvent[] >([]); const plannerStream = useStream({ apiUrl: process.env.NEXT_PUBLIC_API_URL, assistantId: PLANNER_GRAPH_ID, reconnectOnMount: true, threadId: plannerSession?.threadId, onCustomEvent: (event) => { if (isCustomNodeEvent(event)) { setCustomPlannerNodeEvents((prev) => [...prev, event]); } }, fetchStateHistory: false, }); const joinedPlannerRunId = useRef(undefined); useEffect(() => { if ( plannerSession?.runId && plannerSession.runId !== joinedPlannerRunId.current ) { joinedPlannerRunId.current = plannerSession.runId; plannerStream.joinStream(plannerSession.runId).catch(console.error); } else if (!plannerSession?.runId) { joinedPlannerRunId.current = undefined; } }, [plannerSession]); const programmerStream = useStream({ apiUrl: process.env.NEXT_PUBLIC_API_URL, assistantId: PROGRAMMER_GRAPH_ID, reconnectOnMount: true, threadId: programmerSession?.threadId, onCustomEvent: (event) => { if (isCustomNodeEvent(event)) { setCustomProgrammerNodeEvents((prev) => [...prev, event]); } }, fetchStateHistory: false, }); const joinedProgrammerRunId = useRef(undefined); useEffect(() => { if ( programmerSession?.runId && programmerSession.runId !== joinedProgrammerRunId.current ) { joinedProgrammerRunId.current = programmerSession.runId; plannerStream.joinStream(programmerSession.runId).catch(console.error); } else if (!programmerSession?.runId) { joinedProgrammerRunId.current = undefined; } }, [programmerSession]); useEffect(() => { if ( stream?.values?.plannerSession && plannerSession?.runId !== stream.values.plannerSession.runId ) { // State shouldn't update before we use it below, but still create a copy to avoid race conditions const prevPlannerSession = plannerSession; setPlannerSession(stream.values.plannerSession); if (prevPlannerSession && selectedTab === "programmer") { // If we already has a planner session, and the user is currently on the programmer tab, bring them back to the planner tab setSelectedTab("planner"); } } }, [stream?.values]); useEffect(() => { if (stream.error) { const rawErrorMessage = typeof stream.error === "object" && "message" in stream.error ? (stream.error.message as string) : "An unknown error occurred in the manager"; if (rawErrorMessage.includes("overloaded_error")) { setErrorState({ message: "An Anthropic overloaded error occurred. This error occurs when Anthropic APIs experience high traffic across all users.", details: rawErrorMessage, }); } else { setErrorState({ message: rawErrorMessage, }); } } else { setErrorState(null); } }, [stream.error]); useEffect(() => { if ( plannerStream.values.programmerSession && (plannerStream.values.programmerSession.runId !== programmerSession?.runId || plannerStream.values.programmerSession.threadId !== programmerSession?.threadId) ) { setProgrammerSession?.(plannerStream.values.programmerSession); // Only switch tabs from the planner ActionsRenderer to ensure proper timing // This allows the accepted plan step to be visible before switching if (selectedTab === PLANNER_GRAPH_ID) { // Add a small delay to allow the accepted plan step to render first setTimeout(() => { setSelectedTab?.("programmer"); }, 2000); } } }, [plannerStream.values, selectedTab]); const { status: realTimeStatus } = useThreadStatus(displayThread.id); const getStatusDotColor = (status: string) => { switch (status) { case "running": return "bg-blue-500 dark:bg-blue-400"; case "completed": return "bg-green-500 dark:bg-green-400"; case "paused": return "bg-yellow-500 dark:bg-yellow-400"; case "error": return "bg-red-500 dark:bg-red-400"; default: return "bg-gray-500 dark:bg-gray-400"; } }; const cancelRun = () => { // TODO: ideally this calls stream.client.runs.cancel(threadId, runId) stream.stop(); }; const handleSendMessage = () => { if (chatInput.trim()) { const newHumanMessage = new HumanMessage({ id: uuidv4(), content: chatInput, }); stream.submit( { messages: [newHumanMessage], }, { streamResumable: true, optimisticValues: (prev) => ({ ...prev, messages: [...(prev.messages ?? []), newHumanMessage], }), }, ); setChatInput(""); } }; const filteredMessages = stream.messages.filter((message) => { return !message.id?.startsWith(DO_NOT_RENDER_ID_PREFIX); }); return (
{/* Header */}
{displayThread.title} {displayThread.repository && ( <> • {displayThread.repository} )}
{/* Main Content - Split Layout */}
{/* Right Side - Actions & Plan */}
setSelectedTab(value as "planner" | "programmer") } >
Planner Programmer
{selectedTab === "planner" && plannerStream.isLoading && ( )} {selectedTab === "programmer" && programmerStream.isLoading && ( )}
{plannerSession ? (
runId={plannerSession.runId} customNodeEvents={customPlannerNodeEvents} setCustomNodeEvents={ setCustomPlannerNodeEvents } stream={plannerStream} />
) : (
No planner session
)} } footer={
} />
{programmerSession ? (
runId={programmerSession.runId} customNodeEvents={customProgrammerNodeEvents} setCustomNodeEvents={ setCustomProgrammerNodeEvents } stream={programmerStream} />
) : (
No programmer session
)} } footer={
} />
); }