import React, { createContext, useContext, ReactNode, useState, useEffect, useRef, } from "react"; import { useStream } from "@langchain/langgraph-sdk/react"; import { uiMessageReducer, isUIMessage, isRemoveUIMessage, type UIMessage, type RemoveUIMessage, } from "@langchain/langgraph-sdk/react-ui"; import { useQueryState } from "nuqs"; import { LangGraphLogoSVG } from "@/components/icons/langgraph"; import { useThreads } from "./Thread"; import { TooltipIconButton } from "@/components/ui/tooltip-icon-button"; import { Copy, CopyCheck, ArrowRight } from "lucide-react"; import { motion } from "framer-motion"; import { Button } from "@/components/ui/button"; import { GitHubSVG } from "@/components/icons/github"; import { useGitHubToken } from "@/hooks/useGitHubToken"; import { GraphState, GraphUpdate } from "@open-swe/shared/open-swe/types"; const useTypedStream = useStream< GraphState, { UpdateType: GraphUpdate; CustomEventType: UIMessage | RemoveUIMessage; } >; type StreamContextType = ReturnType; const StreamContext = createContext(undefined); async function sleep(ms = 4000) { return new Promise((resolve) => setTimeout(resolve, ms)); } const StreamSession = ({ children, apiUrl, assistantId, githubToken, }: { children: ReactNode; apiUrl: string; assistantId: string; githubToken: string; }) => { const [threadId, setThreadId] = useQueryState("threadId"); const { refreshThreads, updateThreadFromStream } = useThreads(); const streamValue = useTypedStream({ apiUrl, assistantId, reconnectOnMount: true, threadId: threadId ?? null, onCustomEvent: (event, options) => { if (isUIMessage(event) || isRemoveUIMessage(event)) { options.mutate((prev) => { const ui = uiMessageReducer(prev.ui ?? [], event); return { ...prev, ui }; }); } }, onThreadId: (id) => { setThreadId(id); // Refetch threads list when thread ID changes. // Wait for some seconds before fetching so we're able to get the new thread that was created. sleep().then(() => { refreshThreads().catch(console.error); }); }, }); // Listen for stream updates to update all thread properties in real-time // Use a ref to track the last update to prevent excessive calls const lastUpdateRef = useRef<{ threadId: string; valuesHash: string } | null>( null, ); useEffect(() => { if (threadId && streamValue.values) { // Create a simple hash of the values to detect actual changes const valuesHash = JSON.stringify({ plan: (streamValue.values as any).plan, proposedPlan: (streamValue.values as any).proposedPlan, targetRepository: (streamValue.values as any).targetRepository, messages: ( streamValue.values as any ).messages?.[0]?.content?.[0]?.text?.substring(0, 50), }); // Only update if thread or values actually changed if ( !lastUpdateRef.current || lastUpdateRef.current.threadId !== threadId || lastUpdateRef.current.valuesHash !== valuesHash ) { lastUpdateRef.current = { threadId, valuesHash }; updateThreadFromStream(threadId, streamValue.values); } } }, [threadId, streamValue.values, updateThreadFromStream]); // Real-time updates via stream - no polling needed return ( {children} ); }; export const StreamProvider: React.FC<{ children: ReactNode }> = ({ children, }) => { const baseCopyTooltipText = "Copy environment variables"; const [copyTooltipText, setCopyTooltipText] = useState(baseCopyTooltipText); const apiUrl: string | undefined = process.env.NEXT_PUBLIC_API_URL ?? ""; const assistantId: string | undefined = process.env.NEXT_PUBLIC_ASSISTANT_ID ?? ""; const [isAuth, setIsAuth] = useState(null); const [isLoading, setIsLoading] = useState(false); // Initialize from localStorage to prevent modal flash const [hasGitHubAppInstalled, setHasGitHubAppInstalled] = useState< boolean | null >(() => { if (typeof window === "undefined") return null; const cached = localStorage.getItem("github_app_installed"); return cached === "true" ? true : cached === "false" ? false : null; }); const [isCheckingAppInstallation, setIsCheckingAppInstallation] = useState(false); const { token: githubToken, fetchToken: fetchGitHubToken, isLoading: isTokenLoading, } = useGitHubToken(); useEffect(() => { checkAuthStatus(); }, []); useEffect(() => { if (isAuth) { // Check if we already have cached installation status const cachedInstallationStatus = localStorage.getItem( "github_app_installed", ); if (cachedInstallationStatus === "true") { setHasGitHubAppInstalled(true); // Fetch token if we don't have one yet if (!githubToken && !isTokenLoading) { fetchGitHubToken(); } } else if (cachedInstallationStatus === "false") { setHasGitHubAppInstalled(false); } else { // Only check installation if we don't have cached status checkGitHubAppInstallation(); } } }, [isAuth, githubToken]); const checkAuthStatus = async () => { try { const response = await fetch("/api/auth/status"); const data = await response.json(); setIsAuth(data.authenticated); } catch (error) { console.error("Error checking auth status:", error); setIsAuth(false); } }; const checkGitHubAppInstallation = async () => { setIsCheckingAppInstallation(true); try { const response = await fetch("/api/github/repositories"); if (response.ok) { setHasGitHubAppInstalled(true); localStorage.setItem("github_app_installed", "true"); // If the app is installed, fetch a token await fetchGitHubToken(); } else { const errorData = await response.json(); if (errorData.error.includes("installation")) { setHasGitHubAppInstalled(false); localStorage.setItem("github_app_installed", "false"); } else { // If there's a different error, we'll assume the app is not installed setHasGitHubAppInstalled(false); localStorage.setItem("github_app_installed", "false"); } } } catch (error) { console.error("Error checking GitHub App installation:", error); setHasGitHubAppInstalled(false); localStorage.setItem("github_app_installed", "false"); } finally { setIsCheckingAppInstallation(false); } }; const handleLogin = () => { setIsLoading(true); window.location.href = "/api/auth/github/login"; }; const handleInstallGitHubApp = () => { setIsLoading(true); // Clear cached status so we check again after installation localStorage.removeItem("github_app_installed"); window.location.href = "/api/github/installation"; }; if (!apiUrl || !assistantId) { return (

Environment Variables Missing

Whoops, looks like you don't have an API URL or assistant ID set in your environment variables. Please make sure you have both of these set before continuing.

{ const textToCopy = `NEXT_PUBLIC_API_URL=${apiUrl}\nNEXT_PUBLIC_ASSISTANT_ID=${assistantId}`; navigator.clipboard.writeText(textToCopy).then(() => { setCopyTooltipText("Copied!"); setTimeout( () => setCopyTooltipText(baseCopyTooltipText), 2000, ); }); }} className="absolute top-2 right-2 cursor-pointer" tooltip={copyTooltipText} > {copyTooltipText === baseCopyTooltipText ? ( ) : ( )} NEXT_PUBLIC_API_URL={apiUrl} NEXT_PUBLIC_ASSISTANT_ID={assistantId}
); } if (!isAuth) { return (

Get started

Connect your GitHub account to get started with Open SWE.

); } // Step 2: GitHub App Installation (only show if authenticated but app not installed) // Only show modal if we've definitively determined the app is not installed // Don't show while we're still loading or have cached positive status if (isAuth && hasGitHubAppInstalled === false && !isTokenLoading) { return (

One more step

1. GitHub Login ✓ 2. Repository Access

Great! Now we need access to your GitHub repositories. Install our GitHub App to grant access to specific repositories.

You'll be redirected to GitHub where you can select which repositories to grant access to.

); } // Only render StreamSession when we have a valid token if (!githubToken) { return (

Loading...

Setting up your GitHub integration...

); } return ( {children} ); }; // Create a custom hook to use the context export const useStreamContext = (): StreamContextType => { const context = useContext(StreamContext); if (context === undefined) { throw new Error("useStreamContext must be used within a StreamProvider"); } return context; }; export default StreamContext;