From 92d67413773b91f4088b3ac7d3c236d859de976b Mon Sep 17 00:00:00 2001
From: taitsengstock
Date: Fri, 19 Dec 2025 16:44:55 +1100
Subject: [PATCH 1/8] Improved UX for wallet integration
---
.../overlays/add-connection-overlay.tsx | 15 +++++++++++
.../overlays/edit-connection-overlay.tsx | 15 +++++++++++
components/workflow/config/action-config.tsx | 27 ++++++++++---------
keeperhub/plugins/web3/index.ts | 18 ++++---------
plugins/registry.ts | 5 ++++
5 files changed, 55 insertions(+), 25 deletions(-)
diff --git a/components/overlays/add-connection-overlay.tsx b/components/overlays/add-connection-overlay.tsx
index 999cc2db0..2449d7f55 100644
--- a/components/overlays/add-connection-overlay.tsx
+++ b/components/overlays/add-connection-overlay.tsx
@@ -14,6 +14,9 @@ import {
aiGatewayTeamsLoadingAtom,
} from "@/lib/ai-gateway/state";
import { api } from "@/lib/api-client";
+// start keeperhub
+import { getCustomIntegrationFormHandler } from "@/lib/extension-registry";
+// end keeperhub
import type { IntegrationType } from "@/lib/types/integration";
import {
getIntegration,
@@ -361,6 +364,18 @@ export function ConfigureConnectionOverlay({
// Render config fields
const renderConfigFields = () => {
+ // start keeperhub - check for custom form handlers (e.g., web3 wallet)
+ const customHandler = getCustomIntegrationFormHandler(type);
+ if (customHandler) {
+ return customHandler({
+ integrationType: type,
+ isEditMode: false,
+ config,
+ updateConfig,
+ });
+ }
+ // end keeperhub
+
if (type === "database") {
return (
{
+ // start keeperhub - check for custom form handlers (e.g., web3 wallet display)
+ const customHandler = getCustomIntegrationFormHandler(integration.type);
+ if (customHandler) {
+ return customHandler({
+ integrationType: integration.type,
+ isEditMode: true,
+ config,
+ updateConfig,
+ });
+ }
+ // end keeperhub
+
if (integration.type === "database") {
return (
- {hasExistingConnections && (
-
- )}
+ {/* start keeperhub - hide + button for singleConnection integrations */}
+ {hasExistingConnections &&
+ !getIntegration(integrationType)?.singleConnection && (
+
+ )}
+ {/* end keeperhub */}
Date: Tue, 23 Dec 2025 01:24:40 -0600
Subject: [PATCH 2/8] auto-migrate (#170)
---
package.json | 2 +-
scripts/migrate-prod.ts | 16 ++++++++++++++++
2 files changed, 17 insertions(+), 1 deletion(-)
create mode 100644 scripts/migrate-prod.ts
diff --git a/package.json b/package.json
index 3e86c840c..6d4e4659c 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"description": "A template for building your own AI-driven workflow automation platform",
"scripts": {
"dev": "pnpm discover-plugins && next dev",
- "build": "pnpm discover-plugins && next build",
+ "build": "tsx scripts/migrate-prod.ts && pnpm discover-plugins && next build",
"start": "next start",
"type-check": "tsc --noEmit",
"check": "npx ultracite@latest check",
diff --git a/scripts/migrate-prod.ts b/scripts/migrate-prod.ts
new file mode 100644
index 000000000..e15c1f23b
--- /dev/null
+++ b/scripts/migrate-prod.ts
@@ -0,0 +1,16 @@
+import { execSync } from "child_process";
+
+const VERCEL_ENV = process.env.VERCEL_ENV;
+
+if (VERCEL_ENV === "production") {
+ console.log("Running database migrations for production...");
+ try {
+ execSync("pnpm db:migrate", { stdio: "inherit" });
+ console.log("Migrations completed successfully");
+ } catch (error) {
+ console.error("Migration failed:", error);
+ process.exit(1);
+ }
+} else {
+ console.log(`Skipping migrations (VERCEL_ENV=${VERCEL_ENV ?? "not set"})`);
+}
From 63559be230fd04aa1e74973e918adb5db8cb930c Mon Sep 17 00:00:00 2001
From: taitsengstock
Date: Mon, 5 Jan 2026 13:48:23 +1100
Subject: [PATCH 3/8] feat: KEEP-1164 Persistent toolbar across pages
---
app/hub/page.tsx | 30 +++++
app/layout.tsx | 2 +
components/overlays/configuration-overlay.tsx | 15 ++-
components/workflow/node-config-panel.tsx | 65 +++++++++-
components/workflow/workflow-canvas.tsx | 6 -
components/workflow/workflow-toolbar.tsx | 120 ++++++++++++++++--
.../components/hub/workflow-template-grid.tsx | 103 +++++++++++++++
keeperhub/components/persistent-header.tsx | 71 +++++++++++
8 files changed, 385 insertions(+), 27 deletions(-)
create mode 100644 app/hub/page.tsx
create mode 100644 keeperhub/components/hub/workflow-template-grid.tsx
create mode 100644 keeperhub/components/persistent-header.tsx
diff --git a/app/hub/page.tsx b/app/hub/page.tsx
new file mode 100644
index 000000000..3db0fa598
--- /dev/null
+++ b/app/hub/page.tsx
@@ -0,0 +1,30 @@
+import { desc, eq } from "drizzle-orm";
+import { WorkflowTemplateGrid } from "@/keeperhub/components/hub/workflow-template-grid";
+import { db } from "@/lib/db";
+import { workflows } from "@/lib/db/schema";
+
+export default async function HubPage() {
+ const publicWorkflows = await db
+ .select()
+ .from(workflows)
+ .where(eq(workflows.visibility, "public"))
+ .orderBy(desc(workflows.updatedAt));
+
+ const mappedWorkflows = publicWorkflows.map((workflow) => ({
+ id: workflow.id,
+ name: workflow.name,
+ description: workflow.description ?? undefined,
+ nodes: workflow.nodes,
+ edges: workflow.edges,
+ visibility: workflow.visibility,
+ createdAt: workflow.createdAt.toISOString(),
+ updatedAt: workflow.updatedAt.toISOString(),
+ }));
+
+ return (
+
+
Public Workflows
+
+
+ );
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index f78df8c77..7cde23946 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -13,6 +13,7 @@ import { OverlayProvider } from "@/components/overlays/overlay-provider";
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner";
import { PersistentCanvas } from "@/components/workflow/persistent-canvas";
+import { WorkflowToolbar } from "@/components/workflow/workflow-toolbar";
import { KeeperHubExtensionLoader } from "@/keeperhub/components/extension-loader";
import { mono, sans } from "@/lib/fonts";
import { cn } from "@/lib/utils";
@@ -39,6 +40,7 @@ type RootLayoutProps = {
function LayoutContent({ children }: { children: ReactNode }) {
return (
+
{children}
diff --git a/components/overlays/configuration-overlay.tsx b/components/overlays/configuration-overlay.tsx
index 0a7ea007e..0baaf5ee7 100644
--- a/components/overlays/configuration-overlay.tsx
+++ b/components/overlays/configuration-overlay.tsx
@@ -94,6 +94,7 @@ export function ConfigurationOverlay({ overlayId }: ConfigurationOverlayProps) {
currentWorkflowNameAtom
);
const isOwner = useAtomValue(isWorkflowOwnerAtom);
+
const updateNodeData = useSetAtom(updateNodeDataAtom);
const deleteNode = useSetAtom(deleteNodeAtom);
const deleteEdge = useSetAtom(deleteEdgeAtom);
@@ -111,6 +112,7 @@ export function ConfigurationOverlay({ overlayId }: ConfigurationOverlayProps) {
// Auto-fix invalid integration references
const globalIntegrations = useAtomValue(integrationsAtom);
+
useEffect(() => {
if (!(selectedNode && isOwner)) {
return;
@@ -465,17 +467,26 @@ export function ConfigurationOverlay({ overlayId }: ConfigurationOverlayProps) {
)}
- {isOwner && (
+ {isOwner ? (
-
+ ) : (
+
+ DEBUG: isOwner is false - Delete button not rendered
+
)}
)}
diff --git a/components/workflow/node-config-panel.tsx b/components/workflow/node-config-panel.tsx
index 0383c5d23..3bfb2b557 100644
--- a/components/workflow/node-config-panel.tsx
+++ b/components/workflow/node-config-panel.tsx
@@ -10,6 +10,8 @@ import {
} from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
+import { ConfirmOverlay } from "@/components/overlays/confirm-overlay";
+import { useOverlay } from "@/components/overlays/overlay-provider";
import {
AlertDialog,
AlertDialogAction,
@@ -30,6 +32,7 @@ import type { IntegrationType } from "@/lib/types/integration";
import { generateWorkflowCode } from "@/lib/workflow-codegen";
import {
clearNodeStatusesAtom,
+ clearWorkflowAtom,
currentWorkflowIdAtom,
currentWorkflowNameAtom,
deleteEdgeAtom,
@@ -48,9 +51,7 @@ import {
showDeleteDialogAtom,
updateNodeDataAtom,
} from "@/lib/workflow-store";
-
import { findActionById } from "@/plugins";
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import { ActionConfig } from "./config/action-config";
import { ActionGrid } from "./config/action-grid";
@@ -161,9 +162,61 @@ export const PanelInner = () => {
const deleteNode = useSetAtom(deleteNodeAtom);
const deleteEdge = useSetAtom(deleteEdgeAtom);
const deleteSelectedItems = useSetAtom(deleteSelectedItemsAtom);
- const setShowClearDialog = useSetAtom(showClearDialogAtom);
- const setShowDeleteDialog = useSetAtom(showDeleteDialogAtom);
+ const [showClearDialog, setShowClearDialog] = useAtom(showClearDialogAtom);
+ const [showDeleteDialog, setShowDeleteDialog] = useAtom(showDeleteDialogAtom);
const clearNodeStatuses = useSetAtom(clearNodeStatusesAtom);
+ const clearWorkflow = useSetAtom(clearWorkflowAtom);
+ const { open: openOverlay } = useOverlay();
+
+ // Watch showDeleteDialog atom and open overlay when it becomes true
+ useEffect(() => {
+ if (showDeleteDialog) {
+ openOverlay(ConfirmOverlay, {
+ title: "Delete Workflow",
+ message: `Are you sure you want to delete "${currentWorkflowName}"? This will permanently delete the workflow. This cannot be undone.`,
+ confirmLabel: "Delete Workflow",
+ confirmVariant: "destructive" as const,
+ destructive: true,
+ onConfirm: async () => {
+ if (!currentWorkflowId) {
+ return;
+ }
+ try {
+ await api.workflow.delete(currentWorkflowId);
+ toast.success("Workflow deleted successfully");
+ window.location.href = "/";
+ } catch (error) {
+ toast.error("Failed to delete workflow. Please try again.");
+ }
+ },
+ });
+ setShowDeleteDialog(false);
+ }
+ }, [
+ showDeleteDialog,
+ currentWorkflowId,
+ currentWorkflowName,
+ openOverlay,
+ setShowDeleteDialog,
+ ]);
+
+ // Watch showClearDialog atom and open overlay when it becomes true
+ useEffect(() => {
+ if (showClearDialog) {
+ openOverlay(ConfirmOverlay, {
+ title: "Clear Workflow",
+ message:
+ "Are you sure you want to clear all nodes and connections? This action cannot be undone.",
+ confirmLabel: "Clear Workflow",
+ confirmVariant: "destructive" as const,
+ destructive: true,
+ onConfirm: () => {
+ clearWorkflow();
+ },
+ });
+ setShowClearDialog(false);
+ }
+ }, [showClearDialog, openOverlay, clearWorkflow, setShowClearDialog]);
const setPendingIntegrationNodes = useSetAtom(pendingIntegrationNodesAtom);
const [newlyCreatedNodeId, setNewlyCreatedNodeId] = useAtom(
newlyCreatedNodeIdAtom
@@ -632,7 +685,9 @@ export const PanelInner = () => {
setShowDeleteDialog(true)}
+ onClick={() => {
+ setShowDeleteDialog(true);
+ }}
size="sm"
variant="ghost"
>
diff --git a/components/workflow/workflow-canvas.tsx b/components/workflow/workflow-canvas.tsx
index d7bf11af1..154478e3f 100644
--- a/components/workflow/workflow-canvas.tsx
+++ b/components/workflow/workflow-canvas.tsx
@@ -17,7 +17,6 @@ import { Canvas } from "@/components/ai-elements/canvas";
import { Connection } from "@/components/ai-elements/connection";
import { Controls } from "@/components/ai-elements/controls";
import { AIPrompt } from "@/components/ai-elements/prompt";
-import { WorkflowToolbar } from "@/components/workflow/workflow-toolbar";
import "@xyflow/react/dist/style.css";
import { PlayCircle, Zap } from "lucide-react";
@@ -458,11 +457,6 @@ export function WorkflowCanvas() {
: "opacity 300ms",
}}
>
- {/* Toolbar */}
-
-
-
-
{/* React Flow Canvas */}