diff --git a/skyvern-frontend/package-lock.json b/skyvern-frontend/package-lock.json
index d7b8da26..c961e99c 100644
--- a/skyvern-frontend/package-lock.json
+++ b/skyvern-frontend/package-lock.json
@@ -48,8 +48,10 @@
"nanoid": "^5.0.7",
"open": "^10.1.0",
"posthog-js": "^1.138.0",
+ "re-resizable": "^6.11.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-draggable": "^4.5.0",
"react-github-btn": "^1.4.0",
"react-hook-form": "^7.51.1",
"react-router-dom": "^6.22.3",
@@ -4692,9 +4694,10 @@
}
},
"node_modules/clsx": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
- "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -7372,6 +7375,17 @@
}
}
},
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -7453,6 +7467,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/re-resizable": {
+ "version": "6.11.2",
+ "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz",
+ "integrity": "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@@ -7476,6 +7500,20 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-draggable": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz",
+ "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==",
+ "license": "MIT",
+ "dependencies": {
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "react": ">= 16.3.0",
+ "react-dom": ">= 16.3.0"
+ }
+ },
"node_modules/react-github-btn": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/react-github-btn/-/react-github-btn-1.4.0.tgz",
@@ -7502,6 +7540,12 @@
"react": "^16.8.0 || ^17 || ^18"
}
},
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
"node_modules/react-remove-scroll": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
diff --git a/skyvern-frontend/package.json b/skyvern-frontend/package.json
index ee81961b..34dbe4e3 100644
--- a/skyvern-frontend/package.json
+++ b/skyvern-frontend/package.json
@@ -56,8 +56,10 @@
"nanoid": "^5.0.7",
"open": "^10.1.0",
"posthog-js": "^1.138.0",
+ "re-resizable": "^6.11.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-draggable": "^4.5.0",
"react-github-btn": "^1.4.0",
"react-hook-form": "^7.51.1",
"react-router-dom": "^6.22.3",
diff --git a/skyvern-frontend/src/components/BrowserStream.tsx b/skyvern-frontend/src/components/BrowserStream.tsx
index 25af7d53..a62dafdd 100644
--- a/skyvern-frontend/src/components/BrowserStream.tsx
+++ b/skyvern-frontend/src/components/BrowserStream.tsx
@@ -1,6 +1,6 @@
import { Status } from "@/api/types";
import { useEffect, useState, useRef, useCallback } from "react";
-import { HandIcon, PlayIcon } from "@radix-ui/react-icons";
+import { HandIcon, StopIcon } from "@radix-ui/react-icons";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { statusIsNotFinalized } from "@/routes/tasks/types";
@@ -317,13 +317,15 @@ function BrowserStream({
diff --git a/skyvern-frontend/src/components/Timer.tsx b/skyvern-frontend/src/components/Timer.tsx
new file mode 100644
index 00000000..415eb0fc
--- /dev/null
+++ b/skyvern-frontend/src/components/Timer.tsx
@@ -0,0 +1,54 @@
+import { useEffect, useState } from "react";
+
+interface HMS {
+ hour: number;
+ minute: number;
+ second: number;
+}
+
+interface Props {
+ startAt?: HMS;
+}
+
+function Timer({ startAt }: Props) {
+ const [time, setTime] = useState
({
+ hour: 0,
+ minute: 0,
+ second: 0,
+ });
+
+ useEffect(() => {
+ const start = performance.now();
+
+ const loop = () => {
+ const elapsed = performance.now() - start;
+ let seconds = Math.floor(elapsed / 1000);
+ let minutes = Math.floor(seconds / 60);
+ let hours = Math.floor(minutes / 60);
+ seconds = seconds % 60;
+ minutes = minutes % 60;
+ hours = hours % 24;
+ setTime(() => ({
+ hour: hours + (startAt?.hour ?? 0),
+ minute: minutes + (startAt?.minute ?? 0),
+ second: seconds + (startAt?.second ?? 0),
+ }));
+
+ rAF = requestAnimationFrame(loop);
+ };
+
+ let rAF = requestAnimationFrame(loop);
+
+ return () => cancelAnimationFrame(rAF);
+ }, [startAt]);
+
+ return (
+
+ {String(time.hour).padStart(2, "0")}:
+ {String(time.minute).padStart(2, "0")}:
+ {String(time.second).padStart(2, "0")}
+
+ );
+}
+
+export { Timer };
diff --git a/skyvern-frontend/src/hooks/useOnChange.ts b/skyvern-frontend/src/hooks/useOnChange.ts
new file mode 100644
index 00000000..592df4d6
--- /dev/null
+++ b/skyvern-frontend/src/hooks/useOnChange.ts
@@ -0,0 +1,17 @@
+import { useEffect, useRef } from "react";
+
+function useOnChange(
+ value: T,
+ callback: (newValue: T, prevValue: T | undefined) => void,
+) {
+ const prevValue = useRef(value);
+
+ useEffect(() => {
+ if (prevValue.current !== undefined) {
+ callback(value, prevValue.current);
+ }
+ prevValue.current = value;
+ }, [value, callback]);
+}
+
+export { useOnChange };
diff --git a/skyvern-frontend/src/router.tsx b/skyvern-frontend/src/router.tsx
index 91bc5d0c..0b5725dc 100644
--- a/skyvern-frontend/src/router.tsx
+++ b/skyvern-frontend/src/router.tsx
@@ -22,11 +22,16 @@ import { WorkflowPostRunParameters } from "./routes/workflows/workflowRun/Workfl
import { WorkflowRunOutput } from "./routes/workflows/workflowRun/WorkflowRunOutput";
import { WorkflowRunOverview } from "./routes/workflows/workflowRun/WorkflowRunOverview";
import { WorkflowRunRecording } from "./routes/workflows/workflowRun/WorkflowRunRecording";
+import { DebugStoreProvider } from "@/store/DebugStoreContext";
const router = createBrowserRouter([
{
path: "/",
- element: ,
+ element: (
+
+
+
+ ),
children: [
{
index: true,
@@ -98,6 +103,14 @@ const router = createBrowserRouter([
index: true,
element: ,
},
+ {
+ path: "debug",
+ element: ,
+ },
+ {
+ path: ":workflowRunId/:blockLabel/debug",
+ element: ,
+ },
{
path: "edit",
element: ,
diff --git a/skyvern-frontend/src/routes/root/Header.tsx b/skyvern-frontend/src/routes/root/Header.tsx
index ef7d1cb5..e018f4c8 100644
--- a/skyvern-frontend/src/routes/root/Header.tsx
+++ b/skyvern-frontend/src/routes/root/Header.tsx
@@ -1,10 +1,15 @@
import { DiscordLogoIcon } from "@radix-ui/react-icons";
import GitHubButton from "react-github-btn";
-import { Link, useMatch } from "react-router-dom";
+import { Link, useMatch, useSearchParams } from "react-router-dom";
import { NavigationHamburgerMenu } from "./NavigationHamburgerMenu";
function Header() {
- const match = useMatch("/workflows/:workflowPermanentId/edit");
+ const [searchParams] = useSearchParams();
+ const embed = searchParams.get("embed");
+ const match =
+ useMatch("/workflows/:workflowPermanentId/edit") ||
+ location.pathname.includes("debug") ||
+ embed === "true";
if (match) {
return null;
diff --git a/skyvern-frontend/src/routes/root/RootLayout.tsx b/skyvern-frontend/src/routes/root/RootLayout.tsx
index 9235f769..0fcee1a2 100644
--- a/skyvern-frontend/src/routes/root/RootLayout.tsx
+++ b/skyvern-frontend/src/routes/root/RootLayout.tsx
@@ -4,18 +4,24 @@ import { cn } from "@/util/utils";
import { Outlet } from "react-router-dom";
import { Header } from "./Header";
import { Sidebar } from "./Sidebar";
+import { useDebugStore } from "@/store/useDebugStore";
function RootLayout() {
const collapsed = useSidebarStore((state) => state.collapsed);
+ const embed = new URLSearchParams(window.location.search).get("embed");
+ const isEmbedded = embed === "true";
+ const debugStore = useDebugStore();
return (
<>
+ {!isEmbedded && }
-
diff --git a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx
index 11f1c50c..9391d1cb 100644
--- a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx
+++ b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx
@@ -30,6 +30,8 @@ import { WorkflowParameterInput } from "./WorkflowParameterInput";
import { AxiosError } from "axios";
import { getLabelForWorkflowParameterType } from "./editor/workflowEditorUtils";
import { MAX_SCREENSHOT_SCROLLS_DEFAULT } from "./editor/nodes/Taskv2Node/types";
+import { lsKeys } from "@/util/env";
+
type Props = {
workflowParameters: Array;
initialValues: Record;
@@ -145,7 +147,7 @@ function RunWorkflowForm({
const navigate = useNavigate();
const queryClient = useQueryClient();
const browserSessionIdDefault = useLocalStorageFormDefault(
- "skyvern.browserSessionId",
+ lsKeys.browserSessionId,
(initialValues.browserSessionId as string | undefined) ?? null,
);
const form = useForm({
@@ -162,11 +164,7 @@ function RunWorkflowForm({
});
const apiCredential = useApiCredential();
- useSyncFormFieldToStorage(
- form,
- "browserSessionId",
- "skyvern.browserSessionId",
- );
+ useSyncFormFieldToStorage(form, "browserSessionId", lsKeys.browserSessionId);
const runWorkflowMutation = useMutation({
mutationFn: async (values: RunWorkflowFormType) => {
diff --git a/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx b/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx
index a9013894..c93772c5 100644
--- a/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx
+++ b/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx
@@ -41,6 +41,8 @@ import { type ApiCommandOptions } from "@/util/apiCommands";
function WorkflowRun() {
const [searchParams, setSearchParams] = useSearchParams();
+ const embed = searchParams.get("embed");
+ const isEmbedded = embed === "true";
const active = searchParams.get("active");
const { workflowRunId, workflowPermanentId } = useParams();
const credentialGetter = useCredentialGetter();
@@ -169,92 +171,94 @@ function WorkflowRun() {
return (