diff --git a/web/src/components/workflows/ActionPalette.tsx b/web/src/components/workflows/ActionPalette.tsx
index 2aaf4fb..471588d 100644
--- a/web/src/components/workflows/ActionPalette.tsx
+++ b/web/src/components/workflows/ActionPalette.tsx
@@ -1,5 +1,11 @@
import { useState, useMemo } from "react";
-import { Search, X, ChevronDown, ChevronRight, GripVertical } from "lucide-react";
+import {
+ Search,
+ X,
+ ChevronDown,
+ ChevronRight,
+ GripVertical,
+} from "lucide-react";
import type { PaletteAction } from "@/types/workflow";
interface ActionPaletteProps {
@@ -24,7 +30,7 @@ export default function ActionPalette({
action.label?.toLowerCase().includes(query) ||
action.ref?.toLowerCase().includes(query) ||
action.description?.toLowerCase().includes(query) ||
- action.pack_ref?.toLowerCase().includes(query)
+ action.pack_ref?.toLowerCase().includes(query),
);
}, [actions, searchQuery]);
@@ -38,7 +44,7 @@ export default function ActionPalette({
grouped.get(packRef)!.push(action);
});
return new Map(
- [...grouped.entries()].sort((a, b) => a[0].localeCompare(b[0]))
+ [...grouped.entries()].sort((a, b) => a[0].localeCompare(b[0])),
);
}, [filteredActions]);
@@ -55,7 +61,7 @@ export default function ActionPalette({
};
return (
-
+
Action Palette
@@ -93,7 +99,9 @@ export default function ActionPalette({
) : filteredActions.length === 0 ? (
-
No actions match your search
+
+ No actions match your search
+
setSearchQuery("")}
className="mt-1 text-xs text-blue-600 hover:text-blue-800"
@@ -158,7 +166,7 @@ export default function ActionPalette({
)}
);
- }
+ },
)}
)}
diff --git a/web/src/components/workflows/WorkflowInputsPanel.tsx b/web/src/components/workflows/WorkflowInputsPanel.tsx
new file mode 100644
index 0000000..eb8d2d1
--- /dev/null
+++ b/web/src/components/workflows/WorkflowInputsPanel.tsx
@@ -0,0 +1,246 @@
+import { useState } from "react";
+import { Pencil, Plus, X, LogIn, LogOut } from "lucide-react";
+import SchemaBuilder from "@/components/common/SchemaBuilder";
+import type { ParamDefinition } from "@/types/workflow";
+
+interface WorkflowInputsPanelProps {
+ parameters: Record
;
+ output: Record;
+ onParametersChange: (parameters: Record) => void;
+ onOutputChange: (output: Record) => void;
+}
+
+type ModalTarget = "parameters" | "output" | null;
+
+function ParamSummaryList({
+ schema,
+ emptyMessage,
+ onEdit,
+}: {
+ schema: Record;
+ emptyMessage: string;
+ onEdit: () => void;
+}) {
+ const entries = Object.entries(schema);
+
+ if (entries.length === 0) {
+ return (
+
+
+
+ {emptyMessage}
+
+
+ );
+ }
+
+ return (
+
+ {entries.map(([name, def]) => (
+
+
+
+
+ {name}
+
+ {def.required && (
+ *
+ )}
+
+
+ {def.type}
+ {def.secret && (
+ secret
+ )}
+ {def.default !== undefined && (
+
+ = {JSON.stringify(def.default)}
+
+ )}
+
+
+
+ ))}
+
+
+ Edit
+
+
+ );
+}
+
+export default function WorkflowInputsPanel({
+ parameters,
+ output,
+ onParametersChange,
+ onOutputChange,
+}: WorkflowInputsPanelProps) {
+ const [modalTarget, setModalTarget] = useState(null);
+
+ // Draft state for the modal so changes only apply on confirm
+ const [draftSchema, setDraftSchema] = useState<
+ Record
+ >({});
+
+ const openModal = (target: ModalTarget) => {
+ if (target === "parameters") {
+ setDraftSchema({ ...parameters });
+ } else if (target === "output") {
+ setDraftSchema({ ...output });
+ }
+ setModalTarget(target);
+ };
+
+ const handleConfirm = () => {
+ if (modalTarget === "parameters") {
+ onParametersChange(draftSchema);
+ } else if (modalTarget === "output") {
+ onOutputChange(draftSchema);
+ }
+ setModalTarget(null);
+ };
+
+ const handleCancel = () => {
+ setModalTarget(null);
+ };
+
+ const modalLabel =
+ modalTarget === "parameters" ? "Input Parameters" : "Output";
+
+ return (
+ <>
+
+
+ {/* Input Parameters */}
+
+
+
+
+
+ Inputs
+
+
+ {Object.keys(parameters).length > 0 && (
+
+ {Object.keys(parameters).length}
+
+ )}
+
+
+ Referenced via{" "}
+
+ {"{{ params. }}"}
+
+
+
openModal("parameters")}
+ />
+
+
+ {/* Output Schema */}
+
+
+
+
+
+ Output
+
+
+ {Object.keys(output).length > 0 && (
+
+ {Object.keys(output).length}
+
+ )}
+
+
+ Values this workflow produces on completion.
+
+
openModal("output")}
+ />
+
+
+
+
+ {/* Full-screen modal for SchemaBuilder editing */}
+ {modalTarget && (
+
+ {/* Backdrop */}
+
+ {/* Modal */}
+
+ {/* Header */}
+
+
+
+ {modalLabel}
+
+
+ {modalTarget === "parameters"
+ ? "Define the inputs this workflow accepts when executed."
+ : "Define the outputs this workflow produces upon completion."}
+
+
+
+
+
+
+
+ {/* Body */}
+
+
+ setDraftSchema(schema as Record)
+ }
+ placeholder={
+ modalTarget === "parameters"
+ ? '{"message": {"type": "string", "required": true}}'
+ : '{"result": {"type": "string"}}'
+ }
+ />
+
+
+ {/* Footer */}
+
+
+ Cancel
+
+
+ Apply
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/web/src/pages/actions/WorkflowBuilderPage.tsx b/web/src/pages/actions/WorkflowBuilderPage.tsx
index 2a14140..b69b835 100644
--- a/web/src/pages/actions/WorkflowBuilderPage.tsx
+++ b/web/src/pages/actions/WorkflowBuilderPage.tsx
@@ -9,10 +9,13 @@ import {
Code,
LayoutDashboard,
X,
+ Zap,
+ Settings2,
} from "lucide-react";
import yaml from "js-yaml";
import type { WorkflowYamlDefinition } from "@/types/workflow";
import ActionPalette from "@/components/workflows/ActionPalette";
+import WorkflowInputsPanel from "@/components/workflows/WorkflowInputsPanel";
import WorkflowCanvas from "@/components/workflows/WorkflowCanvas";
import type { EdgeHoverInfo } from "@/components/workflows/WorkflowEdges";
import TaskInspector from "@/components/workflows/TaskInspector";
@@ -83,6 +86,7 @@ export default function WorkflowBuilderPage() {
const [saveSuccess, setSaveSuccess] = useState(false);
const [initialized, setInitialized] = useState(false);
const [showYamlPreview, setShowYamlPreview] = useState(false);
+ const [sidebarTab, setSidebarTab] = useState<"actions" | "inputs">("actions");
const [highlightedTransition, setHighlightedTransition] = useState<{
taskId: string;
transitionIndex: number;
@@ -747,12 +751,59 @@ export default function WorkflowBuilderPage() {
) : (
<>
- {/* Left: Action Palette */}
-
+ {/* Left sidebar: tabbed Actions / Inputs */}
+
+ {/* Tab header */}
+
+ setSidebarTab("actions")}
+ className={`flex-1 flex items-center justify-center gap-1.5 px-3 py-2 text-xs font-medium transition-colors ${
+ sidebarTab === "actions"
+ ? "text-blue-600 border-b-2 border-blue-600 bg-blue-50/50"
+ : "text-gray-500 hover:text-gray-700 hover:bg-gray-50"
+ }`}
+ >
+
+ Actions
+
+ setSidebarTab("inputs")}
+ className={`flex-1 flex items-center justify-center gap-1.5 px-3 py-2 text-xs font-medium transition-colors ${
+ sidebarTab === "inputs"
+ ? "text-blue-600 border-b-2 border-blue-600 bg-blue-50/50"
+ : "text-gray-500 hover:text-gray-700 hover:bg-gray-50"
+ }`}
+ >
+
+ Inputs
+ {Object.keys(state.parameters).length > 0 && (
+
+ {Object.keys(state.parameters).length}
+
+ )}
+
+
+
+ {/* Tab content */}
+ {sidebarTab === "actions" ? (
+
+ ) : (
+
+ setState((prev) => ({ ...prev, parameters }))
+ }
+ onOutputChange={(output) =>
+ setState((prev) => ({ ...prev, output }))
+ }
+ />
+ )}
+
{/* Center: Canvas */}