Cycle Browser in Debugger (#3082)

This commit is contained in:
Jonathan Dobson
2025-08-01 09:16:54 -04:00
committed by GitHub
parent 67717aa987
commit e0e3fd1622
4 changed files with 239 additions and 10 deletions

View File

@@ -1,12 +1,30 @@
import { ReactFlowProvider } from "@xyflow/react";
import { useParams } from "react-router-dom";
import { useQueryClient } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { ReloadIcon } from "@radix-ui/react-icons";
import { useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { ReactFlowProvider } from "@xyflow/react";
import { getClient } from "@/api/AxiosClient";
import { DebugSessionApiResponse } from "@/api/types";
import { AnimatedWave } from "@/components/AnimatedWave";
import { BrowserStream } from "@/components/BrowserStream";
import { FloatingWindow } from "@/components/FloatingWindow";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogClose,
} from "@/components/ui/dialog";
import { Skeleton } from "@/components/ui/skeleton";
import { toast } from "@/components/ui/use-toast";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { useMountEffect } from "@/hooks/useMountEffect";
import { useUser } from "@/hooks/useUser";
import { statusIsFinalized } from "@/routes/tasks/types.ts";
import { useSidebarStore } from "@/store/SidebarStore";
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
@@ -19,9 +37,16 @@ import { getElements } from "./workflowEditorUtils";
import { getInitialParameters } from "./utils";
function WorkflowDebugger() {
const { workflowPermanentId } = useParams();
const { blockLabel, workflowPermanentId } = useParams();
const [openDialogue, setOpenDialogue] = useState(false);
const [activeDebugSession, setActiveDebugSession] =
useState<DebugSessionApiResponse | null>(null);
const credentialGetter = useCredentialGetter();
const queryClient = useQueryClient();
const [shouldFetchDebugSession, setShouldFetchDebugSession] = useState(false);
const user = useUser().get();
const email = user?.email;
const isSkyvernUser = email?.toLowerCase().endsWith("@skyvern.com") ?? false;
const { data: workflowRun } = useWorkflowRunQuery();
const { data: workflow } = useWorkflowQuery({
@@ -41,6 +66,10 @@ function WorkflowDebugger() {
(state) => state.setHasChanges,
);
const handleOnCycle = () => {
setOpenDialogue(true);
};
useMountEffect(() => {
setCollapsed(true);
setHasChanges(false);
@@ -53,6 +82,37 @@ function WorkflowDebugger() {
}
});
const cycleBrowser = useMutation({
mutationFn: async (id: string) => {
const client = await getClient(credentialGetter, "sans-api-v1");
return client.post<DebugSessionApiResponse>(`/debug-session/${id}/new`);
},
onSuccess: (response) => {
const newDebugSession = response.data;
setActiveDebugSession(newDebugSession);
queryClient.invalidateQueries({
queryKey: ["debugSession", workflowPermanentId],
});
toast({
title: "Browser cycled",
variant: "success",
description: "Your browser has been cycled.",
});
setOpenDialogue(false);
},
onError: (error: AxiosError) => {
toast({
variant: "destructive",
title: "Failed to cycle browser",
description: error.message,
});
setOpenDialogue(false);
},
});
const intervalRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
@@ -71,6 +131,10 @@ function WorkflowDebugger() {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (debugSession) {
setActiveDebugSession(debugSession);
}
}
return () => {
@@ -107,6 +171,52 @@ function WorkflowDebugger() {
return (
<div className="relative flex h-screen w-full">
<Dialog
open={openDialogue}
onOpenChange={(open) => {
if (!open && cycleBrowser.isPending) {
return;
}
setOpenDialogue(open);
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Cycle (Get a new browser)</DialogTitle>
<DialogDescription>
<div className="pb-2 pt-4 text-sm text-slate-400">
{cycleBrowser.isPending ? (
<>
Cooking you up a fresh browser...
<AnimatedWave text=".‧₊˚ ⋅ ✨★ ‧₊˚ ⋅" />
</>
) : (
"Abandon this browser for a new one. Are you sure?"
)}
</div>
</DialogDescription>
</DialogHeader>
<DialogFooter>
{!cycleBrowser.isPending && (
<DialogClose asChild>
<Button variant="secondary">Cancel</Button>
</DialogClose>
)}
<Button
variant="default"
onClick={() => {
cycleBrowser.mutate(workflowPermanentId!);
}}
disabled={cycleBrowser.isPending}
>
Yes, Continue{" "}
{cycleBrowser.isPending && (
<ReloadIcon className="ml-2 size-4 animate-spin" />
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<ReactFlowProvider>
<FlowRenderer
initialEdges={elements.edges}
@@ -117,7 +227,7 @@ function WorkflowDebugger() {
/>
</ReactFlowProvider>
{debugSession && (
{activeDebugSession && (
<FloatingWindow
title={browserTitle}
bounded={false}
@@ -125,12 +235,15 @@ function WorkflowDebugger() {
initialHeight={360}
showMaximizeButton={true}
showMinimizeButton={true}
showPowerButton={blockLabel === undefined && isSkyvernUser}
showReloadButton={true}
// --
onCycle={handleOnCycle}
>
{debugSession && debugSession.browser_session_id ? (
{activeDebugSession && activeDebugSession.browser_session_id ? (
<BrowserStream
interactive={interactor === "human"}
browserSessionId={debugSession.browser_session_id}
browserSessionId={activeDebugSession.browser_session_id}
/>
) : (
<Skeleton className="h-full w-full" />