diff --git a/skyvern-frontend/src/components/BrowserStream.tsx b/skyvern-frontend/src/components/BrowserStream.tsx index 624923fe..e2ddb445 100644 --- a/skyvern-frontend/src/components/BrowserStream.tsx +++ b/skyvern-frontend/src/components/BrowserStream.tsx @@ -1,7 +1,9 @@ import RFB from "@novnc/novnc/lib/rfb.js"; import { ExitIcon, HandIcon } from "@radix-ui/react-icons"; import { useEffect, useState, useRef, useCallback } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { getClient } from "@/api/AxiosClient"; import { Status } from "@/api/types"; import type { TaskApiResponse, @@ -24,6 +26,11 @@ import { cn } from "@/util/utils"; import { RotateThrough } from "./RotateThrough"; import "./browser-stream.css"; +interface BrowserSession { + browser_session_id: string; + completed_at?: string; +} + interface CommandTakeControl { kind: "take-control"; } @@ -73,11 +80,41 @@ function BrowserStream({ entity = "task"; } else if (workflow) { runId = workflow.run.workflow_run_id; + browserSessionId = workflow.run.browser_session_id ?? undefined; showStream = statusIsNotFinalized(workflow.run); entity = "workflow"; } else { - throw new Error("No browser session, task or workflow provided"); + throw new Error("No browser session id, task or workflow provided"); } + + useQuery({ + queryKey: ["browserSession", browserSessionId], + queryFn: async () => { + const client = await getClient(credentialGetter, "sans-api-v1"); + + try { + const response = await client.get( + `/browser_sessions/${browserSessionId}`, + ); + const browserSession = response.data; + + if (!browserSession || browserSession.completed_at) { + setHasBrowserSession(false); + return false; + } + + setHasBrowserSession(true); + return true; + } catch (error) { + setHasBrowserSession(false); + return false; + } + }, + enabled: !!browserSessionId, + refetchInterval: 5000, + }); + + const [hasBrowserSession, setHasBrowserSession] = useState(true); // be optimistic const [userIsControlling, setUserIsControlling] = useState(interactive); const [commandSocket, setCommandSocket] = useState(null); const [vncDisconnectedTrigger, setVncDisconnectedTrigger] = useState(0); @@ -87,8 +124,6 @@ function BrowserStream({ useState(0); const prevCommandConnectedRef = useRef(false); const [isCommandConnected, setIsCommandConnected] = useState(false); - // goes up a level - // const queryClient = useQueryClient(); const [canvasContainer, setCanvasContainer] = useState( null, ); @@ -172,6 +207,11 @@ function BrowserStream({ rfbRef.current.disconnect(); } + if (!hasBrowserSession) { + setIsVncConnected(false); + return; + } + const canvas = canvasContainer; if (!canvas) { @@ -191,6 +231,8 @@ function BrowserStream({ rfb.addEventListener("disconnect", async (/* e: RfbEvent */) => { setIsVncConnected(false); }); + + // setIsVncConnected(true); // be optimistic } setupVnc(); @@ -206,11 +248,13 @@ function BrowserStream({ // cannot include isVncConnected in deps as it will cause infinite loop // eslint-disable-next-line react-hooks/exhaustive-deps [ + browserSessionId, + entity, canvasContainer, + hasBrowserSession, + runId, showStream, vncDisconnectedTrigger, // will re-run on disconnects - runId, - entity, ], ); @@ -238,6 +282,11 @@ function BrowserStream({ throw new Error("No command url"); } + if (!hasBrowserSession) { + setIsCommandConnected(false); + return; + } + ws = new WebSocket(commandUrl); ws.onopen = () => { @@ -261,10 +310,12 @@ function BrowserStream({ } }; }, [ + browserSessionId, canvasContainer, commandDisconnectedTrigger, entity, getWebSocketParams, + hasBrowserSession, runId, showStream, ]); @@ -351,7 +402,7 @@ function BrowserStream({ )} ref={setCanvasContainerRef} > - {isVncConnected && ( + {isVncConnected && hasBrowserSession && (
{showControlButtons && (
@@ -387,18 +438,24 @@ function BrowserStream({
)} {!isVncConnected && ( -
- - Hm, working on the connection... - Hang tight, we're almost there... - Just a moment... - Backpropagating... - Attention is all I need... - Consulting the manual... - Looking for the bat phone... - Where's Shu?... - - +
+ {browserSessionId && !hasBrowserSession ? ( +
This live browser session is no longer streaming.
+ ) : ( + <> + + Hm, working on the connection... + Hang tight, we're almost there... + Just a moment... + Backpropagating... + Attention is all I need... + Consulting the manual... + Looking for the bat phone... + Where's Shu?... + + + + )}
)}
diff --git a/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx b/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx index c933839c..a468035e 100644 --- a/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx @@ -8,12 +8,12 @@ import { } from "react"; import { nanoid } from "nanoid"; import { + CheckIcon, ChevronRightIcon, ChevronLeftIcon, + CopyIcon, GlobeIcon, ReloadIcon, - CheckIcon, - CopyIcon, } from "@radix-ui/react-icons"; import { useParams, useSearchParams } from "react-router-dom"; import { useEdgesState, useNodesState, Edge } from "@xyflow/react"; @@ -124,6 +124,8 @@ function CopyText({ text }: { text: string }) { ); } +export { CopyText }; + function Workspace({ initialNodes, initialEdges,