diff --git a/skyvern-frontend/src/routes/workflows/copilot/workflowCopilotTypes.ts b/skyvern-frontend/src/routes/workflows/copilot/workflowCopilotTypes.ts
index cce6ab72..e4fe3b90 100644
--- a/skyvern-frontend/src/routes/workflows/copilot/workflowCopilotTypes.ts
+++ b/skyvern-frontend/src/routes/workflows/copilot/workflowCopilotTypes.ts
@@ -38,6 +38,13 @@ export interface WorkflowCopilotChatHistoryMessage {
export interface WorkflowCopilotChatHistoryResponse {
workflow_copilot_chat_id: string | null;
chat_history: WorkflowCopilotChatHistoryMessage[];
+ proposed_workflow?: WorkflowApiResponse | null;
+ auto_accept?: boolean | null;
+}
+
+export interface WorkflowCopilotClearProposedWorkflowRequest {
+ workflow_copilot_chat_id: string;
+ auto_accept: boolean;
}
export type WorkflowCopilotStreamMessageType =
diff --git a/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx b/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx
index 9f79e252..27d76b3d 100644
--- a/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx
+++ b/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx
@@ -94,7 +94,11 @@ import {
import { WorkflowHeader } from "./WorkflowHeader";
import { WorkflowHistoryPanel } from "./panels/WorkflowHistoryPanel";
import { WorkflowVersion } from "../hooks/useWorkflowVersionsQuery";
-import { WorkflowSettings } from "../types/workflowTypes";
+import {
+ Parameter,
+ WorkflowBlock,
+ WorkflowSettings,
+} from "../types/workflowTypes";
import { ProxyLocation } from "@/api/types";
import {
nodeAdderNode,
@@ -107,6 +111,7 @@ import {
import { constructCacheKeyValue, getInitialParameters } from "./utils";
import { WorkflowCopilotChat } from "../copilot/WorkflowCopilotChat";
import { WorkflowCopilotButton } from "../copilot/WorkflowCopilotButton";
+import type { CopilotReviewStatus } from "./panels/WorkflowComparisonPanel";
import "./workspace-styles.css";
const Constants = {
@@ -981,6 +986,40 @@ function Workspace({
}
};
+ const applyWorkflowUpdate = (workflowData: WorkflowVersion) => {
+ const settings: WorkflowSettings = {
+ proxyLocation: workflowData.proxy_location ?? ProxyLocation.Residential,
+ webhookCallbackUrl: workflowData.webhook_callback_url || "",
+ persistBrowserSession: workflowData.persist_browser_session ?? false,
+ model: workflowData.model ?? null,
+ maxScreenshotScrolls: workflowData.max_screenshot_scrolls || 3,
+ extraHttpHeaders: workflowData.extra_http_headers
+ ? JSON.stringify(workflowData.extra_http_headers)
+ : null,
+ runWith: workflowData.run_with ?? null,
+ scriptCacheKey: workflowData.cache_key ?? null,
+ aiFallback: workflowData.ai_fallback ?? true,
+ runSequentially: workflowData.run_sequentially ?? false,
+ sequentialKey: workflowData.sequential_key ?? null,
+ finallyBlockLabel:
+ workflowData.workflow_definition?.finally_block_label ?? null,
+ };
+
+ const elements = getElements(
+ workflowData.workflow_definition.blocks,
+ settings,
+ true,
+ );
+
+ setNodes(elements.nodes);
+ setEdges(elements.edges);
+
+ const initialParameters = getInitialParameters(workflowData);
+ useWorkflowParametersStore.getState().setParameters(initialParameters);
+
+ workflowChangesStore.setHasChanges(true);
+ };
+
const handleSelectState = (selectedVersion: WorkflowVersion) => {
// Close panels
setWorkflowPanelState({
@@ -1239,7 +1278,9 @@ function Workspace({
@@ -1248,6 +1289,10 @@ function Workspace({
version1={workflowPanelState.data.version1}
version2={workflowPanelState.data.version2}
onSelectState={handleSelectState}
+ mode={workflowPanelState.data.mode}
+ onCopilotReviewClose={
+ workflowPanelState.data.onCopilotReviewClose
+ }
/>
@@ -1698,43 +1743,109 @@ function Workspace({
onClose={() => setIsCopilotOpen(false)}
onMessageCountChange={setCopilotMessageCount}
buttonRef={copilotButtonRef}
+ onReviewWorkflow={(pendingWorkflow, clearPending) => {
+ const saveData = workflowChangesStore.getSaveData?.();
+ if (!saveData) return;
+
+ // Construct fake WorkflowVersion for current state
+ const currentVersion: WorkflowVersion = {
+ workflow_id: saveData.workflow.workflow_id,
+ organization_id: "",
+ is_saved_task: saveData.workflow.is_saved_task ?? false,
+ is_template: false,
+ title: "Current",
+ workflow_permanent_id: saveData.workflow.workflow_permanent_id,
+ version: saveData.workflow.version ?? 0,
+ description: saveData.workflow.description ?? "",
+ workflow_definition: {
+ parameters: saveData.parameters as unknown as Parameter[],
+ blocks: saveData.blocks as unknown as WorkflowBlock[],
+ finally_block_label: saveData.settings.finallyBlockLabel,
+ },
+ proxy_location: saveData.settings.proxyLocation,
+ webhook_callback_url: saveData.settings.webhookCallbackUrl,
+ extra_http_headers: saveData.settings.extraHttpHeaders
+ ? JSON.parse(saveData.settings.extraHttpHeaders)
+ : null,
+ persist_browser_session: saveData.settings.persistBrowserSession,
+ model: saveData.settings.model,
+ totp_verification_url: saveData.workflow.totp_verification_url,
+ totp_identifier: null,
+ max_screenshot_scrolls: saveData.settings.maxScreenshotScrolls,
+ status: saveData.workflow.status,
+ created_at: new Date().toISOString(),
+ modified_at: new Date().toISOString(),
+ deleted_at: null,
+ run_with: saveData.settings.runWith,
+ cache_key: saveData.settings.scriptCacheKey,
+ ai_fallback: saveData.settings.aiFallback,
+ run_sequentially: saveData.settings.runSequentially,
+ sequential_key: saveData.settings.sequentialKey,
+ folder_id: null,
+ import_error: null,
+ };
+
+ // Construct fake WorkflowVersion for pending copilot suggestion
+ const pendingVersion: WorkflowVersion = {
+ ...pendingWorkflow,
+ title: "Copilot Suggestion",
+ };
+
+ // Handle copilot review close with status
+ const handleCopilotReviewClose = (status: CopilotReviewStatus) => {
+ if (status === "approve") {
+ try {
+ applyWorkflowUpdate(pendingWorkflow);
+ } catch (error) {
+ console.error(
+ "Failed to apply copilot workflow",
+ error,
+ pendingWorkflow,
+ );
+ toast({
+ title: "Update failed",
+ description:
+ "Failed to apply workflow update. Please try again.",
+ variant: "destructive",
+ });
+ }
+ }
+
+ // Close the panel and reopen copilot chat
+ setWorkflowPanelState({
+ active: false,
+ content: "history",
+ data: {
+ showComparison: false,
+ version1: undefined,
+ version2: undefined,
+ },
+ });
+ setIsCopilotOpen(true);
+
+ // Clear pending for approve and reject, but not for close
+ if (status !== "close") {
+ clearPending();
+ }
+ };
+
+ // Hide chat and show comparison
+ setIsCopilotOpen(false);
+ setWorkflowPanelState({
+ active: false,
+ content: "history",
+ data: {
+ version1: currentVersion,
+ version2: pendingVersion,
+ showComparison: true,
+ mode: "copilot",
+ onCopilotReviewClose: handleCopilotReviewClose,
+ },
+ });
+ }}
onWorkflowUpdate={(workflowData) => {
try {
- const settings: WorkflowSettings = {
- proxyLocation:
- workflowData.proxy_location ?? ProxyLocation.Residential,
- webhookCallbackUrl: workflowData.webhook_callback_url || "",
- persistBrowserSession:
- workflowData.persist_browser_session ?? false,
- model: workflowData.model ?? null,
- maxScreenshotScrolls: workflowData.max_screenshot_scrolls || 3,
- extraHttpHeaders: workflowData.extra_http_headers
- ? JSON.stringify(workflowData.extra_http_headers)
- : null,
- runWith: workflowData.run_with ?? null,
- scriptCacheKey: workflowData.cache_key ?? null,
- aiFallback: workflowData.ai_fallback ?? true,
- runSequentially: workflowData.run_sequentially ?? false,
- sequentialKey: workflowData.sequential_key ?? null,
- finallyBlockLabel:
- workflowData.workflow_definition?.finally_block_label ?? null,
- };
-
- const elements = getElements(
- workflowData.workflow_definition.blocks,
- settings,
- true,
- );
-
- setNodes(elements.nodes);
- setEdges(elements.edges);
-
- const initialParameters = getInitialParameters(workflowData);
- useWorkflowParametersStore
- .getState()
- .setParameters(initialParameters);
-
- workflowChangesStore.setHasChanges(true);
+ applyWorkflowUpdate(workflowData);
} catch (error) {
console.error(
"Failed to parse and apply workflow",
diff --git a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowComparisonPanel.tsx b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowComparisonPanel.tsx
index eff44658..6444baca 100644
--- a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowComparisonPanel.tsx
+++ b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowComparisonPanel.tsx
@@ -1,6 +1,7 @@
-import { useCallback, useMemo } from "react";
+import { useCallback, useEffect, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
+import { Cross2Icon } from "@radix-ui/react-icons";
import {
ReactFlowProvider,
useNodesState,
@@ -22,10 +23,16 @@ type BlockComparison = {
identifier: string;
};
+type ComparisonMode = "history" | "copilot";
+
+export type CopilotReviewStatus = "approve" | "reject" | "close";
+
type Props = {
version1: WorkflowVersion;
version2: WorkflowVersion;
onSelectState?: (version: WorkflowVersion) => void;
+ mode?: ComparisonMode;
+ onCopilotReviewClose?: (status: CopilotReviewStatus) => void;
};
// Mapping from WorkflowBlock.block_type to ReactFlow node.type
@@ -255,7 +262,13 @@ function WorkflowComparisonRenderer({
);
}
-function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
+function WorkflowComparisonPanel({
+ version1,
+ version2,
+ onSelectState,
+ mode = "history",
+ onCopilotReviewClose,
+}: Props) {
const comparisons = useMemo(() => {
const blocks1 = version1?.workflow_definition?.blocks || [];
const blocks2 = version2?.workflow_definition?.blocks || [];
@@ -265,6 +278,20 @@ function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
version2?.workflow_definition?.blocks,
]);
+ // ESC key handler for copilot mode - close without rejecting
+ useEffect(() => {
+ if (mode !== "copilot" || !onCopilotReviewClose) return;
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === "Escape") {
+ onCopilotReviewClose("close");
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [mode, onCopilotReviewClose]);
+
// Statistics
const stats = useMemo(
() => ({
@@ -321,7 +348,18 @@ function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
return (
{/* Header */}
-
+
+ {/* Close button for copilot mode */}
+ {mode === "copilot" && onCopilotReviewClose && (
+
+ )}
{/* 3x3 Grid Layout */}
{/* Row 1: Workflow Names and Title */}
@@ -329,17 +367,21 @@ function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
{version1.title}
- Version Comparison
+ {mode === "copilot" ? "Copilot Review" : "Version Comparison"}
{version2.title}
{/* Row 2: Version Details and Statistics */}
-
- [Version {version1.version}] •{" "}
- {new Date(version1.modified_at).toLocaleDateString()}
-
+ {mode === "history" ? (
+
+ [Version {version1.version}] •{" "}
+ {new Date(version1.modified_at).toLocaleDateString()}
+
+ ) : (
+
+ )}
@@ -358,14 +400,18 @@ function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
Removed ({stats.removed})
-
- [Version {version2.version}] •{" "}
- {new Date(version2.modified_at).toLocaleDateString()}
-
+ {mode === "history" ? (
+
+ [Version {version2.version}] •{" "}
+ {new Date(version2.modified_at).toLocaleDateString()}
+
+ ) : (
+
+ )}
- {/* Row 3: Select Buttons */}
+ {/* Row 3: Action Buttons */}
- {onSelectState && (
+ {mode === "history" && onSelectState && (
)}
-
+
+ {mode === "copilot" && (
+ <>
+
+
+ >
+ )}
+
- {onSelectState && (
+ {mode === "history" && onSelectState && (