Jon/sky 6375 alter how show all code works (#3445)

This commit is contained in:
Jonathan Dobson
2025-09-16 16:32:15 -04:00
committed by GitHub
parent d57fccfaaf
commit f2146080ce
6 changed files with 273 additions and 172 deletions

View File

@@ -341,30 +341,21 @@ function Splitter({
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
useMountEffect(() => { useMountEffect(() => {
// small delay here, to allow for arbitrary layout thrashing to settle; if (containerRef.current) {
// otherwise we have to rely on an observer for the container size, and const newPosition = normalizeUnitsToPercent(
// resetting whenever the container resizes it likely incorrect behaviour containerRef,
setTimeout(() => { direction,
if (containerRef.current) { firstSizingTarget,
const newPosition = normalizeUnitsToPercent( firstSizing,
containerRef, storageKey,
direction, );
firstSizingTarget,
firstSizing,
storageKey,
);
setSplitPosition(newPosition); setSplitPosition(newPosition);
if (storageKey) { if (storageKey) {
setStoredSizing( setStoredSizing(firstSizingTarget, storageKey, newPosition.toString());
firstSizingTarget,
storageKey,
newPosition.toString(),
);
}
} }
}, 100); }
}); });
useOnChange(isDragging, (newValue, oldValue) => { useOnChange(isDragging, (newValue, oldValue) => {

View File

@@ -1,6 +1,7 @@
import { import {
ChevronDownIcon, ChevronDownIcon,
ChevronUpIcon, ChevronUpIcon,
CodeIcon,
CopyIcon, CopyIcon,
PlayIcon, PlayIcon,
ReloadIcon, ReloadIcon,
@@ -41,12 +42,14 @@ type Props = {
cacheKeyValuesPanelOpen: boolean; cacheKeyValuesPanelOpen: boolean;
parametersPanelOpen: boolean; parametersPanelOpen: boolean;
saving: boolean; saving: boolean;
showAllCode: boolean;
workflow: WorkflowApiResponse; workflow: WorkflowApiResponse;
onCacheKeyValueAccept: (cacheKeyValue: string | null) => void; onCacheKeyValueAccept: (cacheKeyValue: string | null) => void;
onCacheKeyValuesBlurred: (cacheKeyValue: string | null) => void; onCacheKeyValuesBlurred: (cacheKeyValue: string | null) => void;
onCacheKeyValuesFilter: (cacheKeyValue: string) => void; onCacheKeyValuesFilter: (cacheKeyValue: string) => void;
onCacheKeyValuesKeydown: (e: React.KeyboardEvent<HTMLInputElement>) => void; onCacheKeyValuesKeydown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
onParametersClick: () => void; onParametersClick: () => void;
onShowAllCodeClick?: () => void;
onCacheKeyValuesClick: () => void; onCacheKeyValuesClick: () => void;
onSave: () => void; onSave: () => void;
onRun?: () => void; onRun?: () => void;
@@ -58,12 +61,14 @@ function WorkflowHeader({
cacheKeyValuesPanelOpen, cacheKeyValuesPanelOpen,
parametersPanelOpen, parametersPanelOpen,
saving, saving,
showAllCode,
workflow, workflow,
onCacheKeyValueAccept, onCacheKeyValueAccept,
onCacheKeyValuesBlurred, onCacheKeyValuesBlurred,
onCacheKeyValuesFilter, onCacheKeyValuesFilter,
onCacheKeyValuesKeydown, onCacheKeyValuesKeydown,
onParametersClick, onParametersClick,
onShowAllCodeClick,
onCacheKeyValuesClick, onCacheKeyValuesClick,
onSave, onSave,
onRun, onRun,
@@ -87,6 +92,10 @@ function WorkflowHeader({
input: useRef<HTMLInputElement>(null), input: useRef<HTMLInputElement>(null),
}; };
const handleShowAllCode = () => {
onShowAllCodeClick?.();
};
useEffect(() => { useEffect(() => {
if (cacheKeyValue === chosenCacheKeyValue) { if (cacheKeyValue === chosenCacheKeyValue) {
return; return;
@@ -125,62 +134,75 @@ function WorkflowHeader({
<div className="flex h-full items-center justify-end gap-4"> <div className="flex h-full items-center justify-end gap-4">
{user && workflow.generate_script && ( {user && workflow.generate_script && (
// (cacheKeyValues?.total_count ?? 0) > 0 && ( // (cacheKeyValues?.total_count ?? 0) > 0 && (
<div <>
tabIndex={1} {debugStore.isDebugMode && (
className="flex max-w-[15rem] items-center justify-center gap-1 rounded-md border border-input pr-1 focus-within:ring-1 focus-within:ring-ring" <Button
> className="pl-2 pr-3"
<Input size="lg"
ref={dom.input} variant={!showAllCode ? "tertiary" : "default"}
className="focus-visible:transparent focus-visible:none h-[2.75rem] text-ellipsis whitespace-nowrap border-none focus-visible:outline-none focus-visible:ring-0" onClick={handleShowAllCode}
onChange={(e) => { >
setChosenCacheKeyValue(e.target.value); <CodeIcon className="mr-2 h-6 w-6" />
onCacheKeyValuesFilter(e.target.value); Show Code
}} </Button>
onMouseDown={() => { )}
if (!cacheKeyValuesPanelOpen) { <div
onCacheKeyValuesClick(); tabIndex={1}
} className="flex max-w-[10rem] items-center justify-center gap-1 rounded-md border border-input pr-1 focus-within:ring-1 focus-within:ring-ring"
}} >
onKeyDown={(e) => { <Input
if (e.key === "Enter") { ref={dom.input}
const numFiltered = cacheKeyValues?.values?.length ?? 0; className="focus-visible:transparent focus-visible:none h-[2.75rem] text-ellipsis whitespace-nowrap border-none focus-visible:outline-none focus-visible:ring-0"
onChange={(e) => {
if (numFiltered === 1) { setChosenCacheKeyValue(e.target.value);
const first = cacheKeyValues?.values?.[0]; onCacheKeyValuesFilter(e.target.value);
if (first) { }}
setChosenCacheKeyValue(first); onMouseDown={() => {
onCacheKeyValueAccept(first); if (!cacheKeyValuesPanelOpen) {
} onCacheKeyValuesClick();
return;
} }
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
const numFiltered = cacheKeyValues?.values?.length ?? 0;
setChosenCacheKeyValue(chosenCacheKeyValue); if (numFiltered === 1) {
onCacheKeyValueAccept(chosenCacheKeyValue); const first = cacheKeyValues?.values?.[0];
} if (first) {
onCacheKeyValuesKeydown(e); setChosenCacheKeyValue(first);
}} onCacheKeyValueAccept(first);
placeholder="Code Key Value" }
value={chosenCacheKeyValue ?? undefined} return;
onBlur={(e) => { }
onCacheKeyValuesBlurred(e.target.value);
setChosenCacheKeyValue(e.target.value); setChosenCacheKeyValue(chosenCacheKeyValue);
}} onCacheKeyValueAccept(chosenCacheKeyValue);
/> }
{cacheKeyValuesPanelOpen ? ( onCacheKeyValuesKeydown(e);
<ChevronUpIcon }}
className="h-6 w-6 cursor-pointer" placeholder="Code Key Value"
onClick={onCacheKeyValuesClick} value={chosenCacheKeyValue ?? undefined}
/> onBlur={(e) => {
) : ( onCacheKeyValuesBlurred(e.target.value);
<ChevronDownIcon setChosenCacheKeyValue(e.target.value);
className="h-6 w-6 cursor-pointer"
onClick={() => {
dom.input.current?.focus();
onCacheKeyValuesClick();
}} }}
/> />
)} {cacheKeyValuesPanelOpen ? (
</div> <ChevronUpIcon
className="h-6 w-6 cursor-pointer"
onClick={onCacheKeyValuesClick}
/>
) : (
<ChevronDownIcon
className="h-6 w-6 cursor-pointer"
onClick={() => {
dom.input.current?.focus();
onCacheKeyValuesClick();
}}
/>
)}
</div>
</>
)} )}
{isGlobalWorkflow ? ( {isGlobalWorkflow ? (
<Button <Button

View File

@@ -1,11 +1,13 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState, MutableRefObject } from "react";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { import {
ChevronRightIcon, ChevronRightIcon,
ChevronLeftIcon, ChevronLeftIcon,
GlobeIcon, GlobeIcon,
ReloadIcon, ReloadIcon,
CheckIcon,
CopyIcon,
} from "@radix-ui/react-icons"; } from "@radix-ui/react-icons";
import { useParams, useSearchParams } from "react-router-dom"; import { useParams, useSearchParams } from "react-router-dom";
import { useEdgesState, useNodesState, Edge } from "@xyflow/react"; import { useEdgesState, useNodesState, Edge } from "@xyflow/react";
@@ -42,6 +44,7 @@ import {
import { toast } from "@/components/ui/use-toast"; import { toast } from "@/components/ui/use-toast";
import { BrowserStream } from "@/components/BrowserStream"; import { BrowserStream } from "@/components/BrowserStream";
import { statusIsFinalized } from "@/routes/tasks/types.ts"; import { statusIsFinalized } from "@/routes/tasks/types.ts";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { DebuggerRun } from "@/routes/workflows/debugger/DebuggerRun"; import { DebuggerRun } from "@/routes/workflows/debugger/DebuggerRun";
import { DebuggerRunMinimal } from "@/routes/workflows/debugger/DebuggerRunMinimal"; import { DebuggerRunMinimal } from "@/routes/workflows/debugger/DebuggerRunMinimal";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
@@ -50,6 +53,7 @@ import {
useWorkflowHasChangesStore, useWorkflowHasChangesStore,
useWorkflowSave, useWorkflowSave,
} from "@/store/WorkflowHasChangesStore"; } from "@/store/WorkflowHasChangesStore";
import { getCode, getOrderedBlockLabels } from "@/routes/workflows/utils";
import { cn } from "@/util/utils"; import { cn } from "@/util/utils";
@@ -69,7 +73,6 @@ import {
startNode, startNode,
} from "./workflowEditorUtils"; } from "./workflowEditorUtils";
import { constructCacheKeyValue } from "./utils"; import { constructCacheKeyValue } from "./utils";
import "./workspace-styles.css"; import "./workspace-styles.css";
const Constants = { const Constants = {
@@ -90,6 +93,26 @@ export type AddNodeProps = {
connectingEdgeType: string; connectingEdgeType: string;
}; };
interface Dom {
splitLeft: MutableRefObject<HTMLInputElement | null>;
}
function CopyText({ text }: { text: string }) {
const [wasCopied, setWasCopied] = useState(false);
function handleCopy(code: string) {
navigator.clipboard.writeText(code);
setWasCopied(true);
setTimeout(() => setWasCopied(false), 2000);
}
return (
<Button size="icon" variant="link" onClick={() => handleCopy(text)}>
{wasCopied ? <CheckIcon /> : <CopyIcon />}
</Button>
);
}
function Workspace({ function Workspace({
initialNodes, initialNodes,
initialEdges, initialEdges,
@@ -146,6 +169,15 @@ function Workspace({
: constructCacheKeyValue({ codeKey: cacheKey, workflow }), : constructCacheKeyValue({ codeKey: cacheKey, workflow }),
); );
const [showAllCode, setShowAllCode] = useState(false);
const [leftSideLayoutMode, setLeftSideLayoutMode] = useState<
"single" | "side-by-side"
>("single");
const dom: Dom = {
splitLeft: useRef<HTMLInputElement>(null),
};
useEffect(() => { useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") { if (event.key === "Escape") {
@@ -400,6 +432,32 @@ function Workspace({
}; };
}, [debugSession, shouldFetchDebugSession, workflowPermanentId, queryClient]); }, [debugSession, shouldFetchDebugSession, workflowPermanentId, queryClient]);
useEffect(() => {
const splitLeft = dom.splitLeft.current;
if (!splitLeft) {
return;
}
const parent = splitLeft.parentElement;
if (!parent) {
return;
}
const observer = new ResizeObserver(() => {
setLeftSideLayoutMode(
parent.offsetWidth < 1100 ? "single" : "side-by-side",
);
});
observer.observe(parent);
return () => {
observer.disconnect();
};
}, [dom.splitLeft]);
function doLayout(nodes: Array<AppNode>, edges: Array<Edge>) { function doLayout(nodes: Array<AppNode>, edges: Array<Edge>) {
const layoutedElements = layout(nodes, edges); const layoutedElements = layout(nodes, edges);
setNodes(layoutedElements.nodes); setNodes(layoutedElements.nodes);
@@ -508,6 +566,9 @@ function Workspace({
} }
} }
const orderedBlockLabels = getOrderedBlockLabels(workflow);
const code = getCode(orderedBlockLabels, blockScripts).join("");
return ( return (
<div className="relative h-full w-full"> <div className="relative h-full w-full">
{/* cycle browser dialog */} {/* cycle browser dialog */}
@@ -631,6 +692,7 @@ function Workspace({
workflowPanelState.active && workflowPanelState.active &&
workflowPanelState.content === "parameters" workflowPanelState.content === "parameters"
} }
showAllCode={showAllCode}
workflow={workflow} workflow={workflow}
onCacheKeyValueAccept={(v) => { onCacheKeyValueAccept={(v) => {
setCacheKeyValue(v ?? ""); setCacheKeyValue(v ?? "");
@@ -696,6 +758,9 @@ function Workspace({
onRun={() => { onRun={() => {
closeWorkflowPanel(); closeWorkflowPanel();
}} }}
onShowAllCodeClick={() => {
setShowAllCode(!showAllCode);
}}
/> />
</div> </div>
@@ -806,7 +871,7 @@ function Workspace({
</div> </div>
)} )}
{/* infinite canvas, browser, and timeline when in debug mode */} {/* code, infinite canvas, browser, and timeline when in debug mode */}
{showBrowser && ( {showBrowser && (
<div className="relative flex h-full w-full overflow-hidden overflow-x-hidden"> <div className="relative flex h-full w-full overflow-hidden overflow-x-hidden">
<Splitter <Splitter
@@ -816,21 +881,64 @@ function Workspace({
split={{ left: workflowWidth }} split={{ left: workflowWidth }}
onResize={() => setContainerResizeTrigger((prev) => prev + 1)} onResize={() => setContainerResizeTrigger((prev) => prev + 1)}
> >
{/* infinite canvas */} {/* code and infinite canvas */}
<div className="skyvern-split-left h-full w-full"> <div className="relative h-full w-full">
<FlowRenderer <div
hideBackground={true} className={cn(
hideControls={true} "skyvern-split-left flex h-full w-[200%] translate-x-[-50%] transition-none duration-300",
nodes={nodes} {
edges={edges} "w-[100%] translate-x-0":
setNodes={setNodes} leftSideLayoutMode === "side-by-side",
setEdges={setEdges} },
onNodesChange={onNodesChange} {
onEdgesChange={onEdgesChange} "translate-x-0": showAllCode,
initialTitle={initialTitle} },
workflow={workflow} )}
onContainerResize={containerResizeTrigger} ref={dom.splitLeft}
/> >
{/* code */}
<div
className={cn("h-full w-[50%]", {
"w-[0%]":
leftSideLayoutMode === "side-by-side" && !showAllCode,
})}
>
<div className="relative mt-[8.5rem] w-full p-6 pr-5 pt-0">
<div className="absolute right-[1.25rem] top-0 z-20">
<CopyText text={code} />
</div>
<CodeEditor
className="w-full overflow-y-scroll"
language="python"
value={code}
lineWrap={false}
readOnly
fontSize={10}
/>
</div>
</div>
{/* infinite canvas */}
<div
className={cn("h-full w-[50%]", {
"w-[100%]":
leftSideLayoutMode === "side-by-side" && !showAllCode,
})}
>
<FlowRenderer
hideBackground={true}
hideControls={true}
nodes={nodes}
edges={edges}
setNodes={setNodes}
setEdges={setEdges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
initialTitle={initialTitle}
workflow={workflow}
onContainerResize={containerResizeTrigger}
/>
</div>
</div>
</div> </div>
{/* browser & timeline */} {/* browser & timeline */}

View File

@@ -1,13 +1,6 @@
import { getClient } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { Handle, Node, NodeProps, Position, useReactFlow } from "@xyflow/react"; import { Handle, Node, NodeProps, Position, useReactFlow } from "@xyflow/react";
import type { StartNode } from "./types"; import type { StartNode } from "./types";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
@@ -42,7 +35,6 @@ import { useRerender } from "@/hooks/useRerender";
import { useBlockScriptStore } from "@/store/BlockScriptStore"; import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor"; import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
import { cn } from "@/util/utils"; import { cn } from "@/util/utils";
import { LightningBoltIcon } from "@radix-ui/react-icons";
function StartNode({ id, data }: NodeProps<StartNode>) { function StartNode({ id, data }: NodeProps<StartNode>) {
const workflowSettingsStore = useWorkflowSettingsStore(); const workflowSettingsStore = useWorkflowSettingsStore();
@@ -115,19 +107,20 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
); );
} }
function showAllScripts() { // NOTE(jdo): keeping for reference; we seem to revert stuff all the time
for (const node of reactFlowInstance.getNodes()) { // function showAllScripts() {
const label = node.data.label; // for (const node of reactFlowInstance.getNodes()) {
// const label = node.data.label;
label && // label &&
nodeIsFlippable(node) && // nodeIsFlippable(node) &&
typeof label === "string" && // typeof label === "string" &&
toggleScriptForNodeCallback({ // toggleScriptForNodeCallback({
label, // label,
show: true, // show: true,
}); // });
} // }
} // }
function hideAllScripts() { function hideAllScripts() {
for (const node of reactFlowInstance.getNodes()) { for (const node of reactFlowInstance.getNodes()) {
@@ -160,20 +153,6 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
)} )}
> >
<div className="relative"> <div className="relative">
<div className="absolute right-[-0.5rem] top-[-0.25rem]">
<div>
<Button variant="link" size="icon" onClick={showAllScripts}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<LightningBoltIcon className="h-4 w-4 text-[gold]" />
</TooltipTrigger>
<TooltipContent>Show all code</TooltipContent>
</Tooltip>
</TooltipProvider>
</Button>
</div>
</div>
<header className="mb-6 mt-2">Start</header> <header className="mb-6 mt-2">Start</header>
<Separator /> <Separator />
<Accordion <Accordion

View File

@@ -1,5 +1,6 @@
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import type { WorkflowParameter } from "./types/workflowTypes"; import type { WorkflowParameter } from "./types/workflowTypes";
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
type Location = ReturnType<typeof useLocation>; type Location = ReturnType<typeof useLocation>;
@@ -77,3 +78,49 @@ export const formatDuration = (duration: Duration): string => {
return `${duration.second}s`; return `${duration.second}s`;
} }
}; };
export const getOrderedBlockLabels = (workflow?: WorkflowApiResponse) => {
if (!workflow) {
return [];
}
const blockLabels = workflow.workflow_definition.blocks.map(
(block) => block.label,
);
return blockLabels;
};
const getCommentForBlockWithoutCode = (blockLabel: string) => {
return `
# block '${blockLabel}' code goes here
`;
};
export const getCode = (
orderedBlockLabels: string[],
blockScripts?: {
[blockName: string]: string;
},
): string[] => {
const blockCode: string[] = [];
const startBlockCode = blockScripts?.__start_block__;
if (startBlockCode) {
blockCode.push(startBlockCode);
}
for (const blockLabel of orderedBlockLabels) {
const code = blockScripts?.[blockLabel];
if (!code) {
blockCode.push(getCommentForBlockWithoutCode(blockLabel));
continue;
}
blockCode.push(`${code}
`);
}
return blockCode;
};

View File

@@ -18,58 +18,12 @@ import { useCacheKeyValuesQuery } from "@/routes/workflows/hooks/useCacheKeyValu
import { useWorkflowQuery } from "@/routes/workflows/hooks/useWorkflowQuery"; import { useWorkflowQuery } from "@/routes/workflows/hooks/useWorkflowQuery";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
import { constructCacheKeyValue } from "@/routes/workflows/editor/utils"; import { constructCacheKeyValue } from "@/routes/workflows/editor/utils";
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes"; import { getCode, getOrderedBlockLabels } from "@/routes/workflows/utils";
interface Props { interface Props {
showCacheKeyValueSelector?: boolean; showCacheKeyValueSelector?: boolean;
} }
const getOrderedBlockLabels = (workflow?: WorkflowApiResponse) => {
if (!workflow) {
return [];
}
const blockLabels = workflow.workflow_definition.blocks.map(
(block) => block.label,
);
return blockLabels;
};
const getCommentForBlockWithoutCode = (blockLabel: string) => {
return `
# If the "Generate Code" option is turned on for this workflow when it runs, AI will execute block '${blockLabel}', and generate code for it.
`;
};
const getCode = (
orderedBlockLabels: string[],
blockScripts?: {
[blockName: string]: string;
},
): string[] => {
const blockCode: string[] = [];
const startBlockCode = blockScripts?.__start_block__;
if (startBlockCode) {
blockCode.push(startBlockCode);
}
for (const blockLabel of orderedBlockLabels) {
const code = blockScripts?.[blockLabel];
if (!code) {
blockCode.push(getCommentForBlockWithoutCode(blockLabel));
continue;
}
blockCode.push(`${code}
`);
}
return blockCode;
};
function WorkflowRunCode(props?: Props) { function WorkflowRunCode(props?: Props) {
const showCacheKeyValueSelector = props?.showCacheKeyValueSelector ?? false; const showCacheKeyValueSelector = props?.showCacheKeyValueSelector ?? false;
const queryClient = useQueryClient(); const queryClient = useQueryClient();