"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 } 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 { StickToBottom } from "use-stick-to-bottom"; import { StickyToBottomContent, ScrollToBottom, } from "../../utils/scroll-utils"; import { ManagerChat } from "./manager-chat"; import { CancelStreamButton } from "./cancel-stream-button"; 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 [errorMessage, setErrorMessage] = useState(""); 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 errorMessage = typeof stream.error === "object" && "message" in stream.error ? (stream.error.message as string) : "An unknown error occurred in the manager"; setErrorMessage(errorMessage); } else { setErrorMessage(""); } }, [stream.error]); 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 plannerCancelRef = useRef<(() => void) | null>(null); const programmerCancelRef = useRef<(() => void) | null>(null); 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" && plannerCancelRef.current && ( )} {selectedTab === "programmer" && programmerCancelRef.current && ( )}
{plannerSession && ( graphId={PLANNER_GRAPH_ID} threadId={plannerSession.threadId} runId={plannerSession.runId} setProgrammerSession={setProgrammerSession} programmerSession={programmerSession} setSelectedTab={setSelectedTab} onStreamReady={(cancelFn) => { if (cancelFn) { plannerCancelRef.current = cancelFn; } else { plannerCancelRef.current = null; } }} /> )} {!plannerSession && (
No planner session
)}
{programmerSession && ( graphId={PROGRAMMER_GRAPH_ID} threadId={programmerSession.threadId} runId={programmerSession.runId} onStreamReady={(cancelFn) => { if (cancelFn) { programmerCancelRef.current = cancelFn; } else { programmerCancelRef.current = null; } }} /> )} {!programmerSession && (
No programmer session
)}
} footer={
} />
); }