From 8a40e69fae074160f65c7eb4500dd73134a3f806 Mon Sep 17 00:00:00 2001 From: medevs Date: Sun, 12 Apr 2026 11:05:17 +0200 Subject: [PATCH 1/3] fix(web): allow deleting nodes from Workflow Builder (#971) Three independent gaps prevented users from deleting nodes added to the Workflow Builder canvas: dropped nodes were never auto-selected so keyboard shortcuts silently no-oped, no right-click context menu existed, and the Delete Node button was buried in the Advanced tab (hidden below the viewport for Prompt/Command, completely absent for Bash since bash nodes have no Advanced tab). Fixes #971. --- .../components/workflows/NodeInspector.tsx | 27 +++--- .../components/workflows/WorkflowBuilder.tsx | 23 +++-- .../components/workflows/WorkflowCanvas.tsx | 85 ++++++++++++++++++- packages/web/src/hooks/useBuilderKeyboard.ts | 5 ++ 4 files changed, 120 insertions(+), 20 deletions(-) diff --git a/packages/web/src/components/workflows/NodeInspector.tsx b/packages/web/src/components/workflows/NodeInspector.tsx index 927e5edf91..1b8c61cd1c 100644 --- a/packages/web/src/components/workflows/NodeInspector.tsx +++ b/packages/web/src/components/workflows/NodeInspector.tsx @@ -625,11 +625,9 @@ function JsonTextareaField({ function AdvancedTab({ node, onUpdate, - onDelete, }: { node: DagNodeData; onUpdate: (updates: Partial) => void; - onDelete: () => void; }): React.ReactElement { return (
@@ -679,12 +677,6 @@ function AdvancedTab({ onUpdate({ hooks: v }); }} /> - -
- -
); } @@ -701,14 +693,25 @@ function DagInspector({ return (
{/* Header */} -
- +
+ {node.label || node.id} + {/* Delete button visible for ALL node types — including bash, which has no + Advanced tab. Fixes #971 (bash nodes had no UI delete affordance). */} + +
+ )}
); } diff --git a/packages/web/src/hooks/useBuilderKeyboard.ts b/packages/web/src/hooks/useBuilderKeyboard.ts index 192f29bd2b..d513ee77c7 100644 --- a/packages/web/src/hooks/useBuilderKeyboard.ts +++ b/packages/web/src/hooks/useBuilderKeyboard.ts @@ -101,6 +101,11 @@ export function useBuilderKeyboard(actions: BuilderKeyboardActions, enabled = tr actions.onAddBash(); break; case 'Delete': + case 'Backspace': + // Backspace is the natural delete key on macOS keyboards, which lack + // a dedicated Delete key. The isInputTarget() guard above prevents + // this from interfering with text fields. Fixes #971. + e.preventDefault(); actions.onDeleteSelected(); break; case 'f': From b9c75dc05adf385292684738a02ad19ffab87890 Mon Sep 17 00:00:00 2001 From: medevs Date: Sun, 12 Apr 2026 16:38:36 +0200 Subject: [PATCH 2/3] fix(web): push undo snapshot before adding nodes on canvas Call onPushSnapshot() before setNodes() in both onDrop and quick-add handlers so that node additions are captured by undo/redo history. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/components/workflows/WorkflowCanvas.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/web/src/components/workflows/WorkflowCanvas.tsx b/packages/web/src/components/workflows/WorkflowCanvas.tsx index 52e6f866a7..c40111bbec 100644 --- a/packages/web/src/components/workflows/WorkflowCanvas.tsx +++ b/packages/web/src/components/workflows/WorkflowCanvas.tsx @@ -172,13 +172,14 @@ export function WorkflowCanvas({ }, }; + onPushSnapshot?.(); setNodes(nds => [...nds, newNode]); // Auto-select the new node so keyboard shortcuts (Delete/Backspace) and the // inspector panel work immediately without an extra click. Fixes #971. onNodeSelect(id); onDirty(); }, - [screenToFlowPosition, setNodes, onNodeSelect, onDirty] + [screenToFlowPosition, setNodes, onNodeSelect, onDirty, onPushSnapshot] ); // Track whether we've already pushed a snapshot for the current drag gesture @@ -289,13 +290,14 @@ export function WorkflowCanvas({ }, }; + onPushSnapshot?.(); setNodes(nds => [...nds, newNode]); // Auto-select for the same reason as onDrop above (#971). onNodeSelect(id); onDirty(); setQuickAddPosition(null); }, - [quickAddPosition, setNodes, onNodeSelect, onDirty] + [quickAddPosition, setNodes, onNodeSelect, onDirty, onPushSnapshot] ); const handleQuickAddClose = useCallback(() => { From fc677406ae5c2aa2b7db067fac8a68edc9386b82 Mon Sep 17 00:00:00 2001 From: medevs Date: Tue, 21 Apr 2026 18:57:51 +0200 Subject: [PATCH 3/3] fix(web): address PR #1113 review feedback - Hold nodes/edges in refs so handleNodeDeleteById and onPushSnapshot can't capture stale pre-drop state (fixes undo-stack correctness). - Clamp context-menu x/y to viewport so right-click near edges stays fully on-screen. - Drop non-conformant role=menu/menuitem from the single-item context menu; rely on the native button for accessibility. - Extend isInputTarget() to cover ARIA combobox/textbox/searchbox so Backspace in Radix/shadcn widgets never nukes a node. - Extract handleBuilderKeydown as a pure function and add tests covering the Delete/Backspace + isInputTarget invariant. - Remove issue-number references from code comments per CLAUDE.md. - Document the new delete affordances in the Workflow Builder docs. - Inline context-menu dismissal, rename pointer handler, drop unused deps in keyboardActions useMemo. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../docs-web/src/content/docs/adapters/web.md | 1 + packages/web/package.json | 2 +- .../components/workflows/NodeInspector.tsx | 2 - .../components/workflows/WorkflowBuilder.tsx | 33 +-- .../components/workflows/WorkflowCanvas.tsx | 39 ++-- .../web/src/hooks/useBuilderKeyboard.test.ts | 136 +++++++++++++ packages/web/src/hooks/useBuilderKeyboard.ts | 191 +++++++++--------- 7 files changed, 275 insertions(+), 129 deletions(-) create mode 100644 packages/web/src/hooks/useBuilderKeyboard.test.ts diff --git a/packages/docs-web/src/content/docs/adapters/web.md b/packages/docs-web/src/content/docs/adapters/web.md index 7a3aeebb86..0025ca0219 100644 --- a/packages/docs-web/src/content/docs/adapters/web.md +++ b/packages/docs-web/src/content/docs/adapters/web.md @@ -172,6 +172,7 @@ The Workflow Builder at `/workflows/builder` provides a visual editor for creati - **Command picker** -- Browse available commands when configuring command nodes - **Validation panel** -- Real-time validation feedback as you build - **Undo/redo** -- Full undo/redo stack with keyboard shortcuts +- **Delete node** -- Remove a selected node with `Delete` or `Backspace`, the Delete button in the inspector header, or the right-click context menu on any node - **Save** -- Saves the workflow YAML to your project's `.archon/workflows/` directory You can also browse existing workflows on the `/workflows` page and open any of them in the builder to edit. diff --git a/packages/web/package.json b/packages/web/package.json index 5b02e1bbe6..d94f57c5ae 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -8,7 +8,7 @@ "build": "tsc --noEmit && vite build", "preview": "vite preview", "type-check": "tsc --noEmit", - "test": "bun test src/lib/ && bun test src/stores/", + "test": "bun test src/lib/ && bun test src/stores/ && bun test src/hooks/", "generate:types": "openapi-typescript http://localhost:3090/api/openapi.json -o src/lib/api.generated.d.ts" }, "dependencies": { diff --git a/packages/web/src/components/workflows/NodeInspector.tsx b/packages/web/src/components/workflows/NodeInspector.tsx index e6703135f5..1d4748fecc 100644 --- a/packages/web/src/components/workflows/NodeInspector.tsx +++ b/packages/web/src/components/workflows/NodeInspector.tsx @@ -714,8 +714,6 @@ function DagInspector({ {node.label || node.id} - {/* Delete button visible for ALL node types — including bash, which has no - Advanced tab. Fixes #971 (bash nodes had no UI delete affordance). */}
diff --git a/packages/web/src/components/workflows/WorkflowCanvas.tsx b/packages/web/src/components/workflows/WorkflowCanvas.tsx index c40111bbec..e1c6170b16 100644 --- a/packages/web/src/components/workflows/WorkflowCanvas.tsx +++ b/packages/web/src/components/workflows/WorkflowCanvas.tsx @@ -174,8 +174,6 @@ export function WorkflowCanvas({ onPushSnapshot?.(); setNodes(nds => [...nds, newNode]); - // Auto-select the new node so keyboard shortcuts (Delete/Backspace) and the - // inspector panel work immediately without an extra click. Fixes #971. onNodeSelect(id); onDirty(); }, @@ -292,7 +290,6 @@ export function WorkflowCanvas({ onPushSnapshot?.(); setNodes(nds => [...nds, newNode]); - // Auto-select for the same reason as onDrop above (#971). onNodeSelect(id); onDirty(); setQuickAddPosition(null); @@ -304,29 +301,29 @@ export function WorkflowCanvas({ setQuickAddPosition(null); }, []); - // Right-click on a node opens a small context menu with a Delete option (#971). - // We must call preventDefault() to suppress the browser's native menu. + // Approximate menu size used for viewport-edge clamping. + const CONTEXT_MENU_WIDTH = 160; + const CONTEXT_MENU_HEIGHT = 40; + const handleNodeContextMenu = useCallback( (e: React.MouseEvent, node: DagFlowNode) => { e.preventDefault(); onNodeSelect(node.id); - setContextMenu({ x: e.clientX, y: e.clientY, nodeId: node.id }); + const x = Math.min(e.clientX, window.innerWidth - CONTEXT_MENU_WIDTH); + const y = Math.min(e.clientY, window.innerHeight - CONTEXT_MENU_HEIGHT); + setContextMenu({ x, y, nodeId: node.id }); }, [onNodeSelect] ); - const closeContextMenu = useCallback(() => { - setContextMenu(null); - }, []); - // Dismiss the context menu on Escape or any click/contextmenu outside it. useEffect(() => { if (!contextMenu) return; const onKey = (e: KeyboardEvent): void => { - if (e.key === 'Escape') closeContextMenu(); + if (e.key === 'Escape') setContextMenu(null); }; - const onPointer = (e: MouseEvent): void => { + const onClickOutside = (e: MouseEvent): void => { if ( contextMenuRef.current && e.target instanceof Node && @@ -334,19 +331,19 @@ export function WorkflowCanvas({ ) { return; } - closeContextMenu(); + setContextMenu(null); }; window.addEventListener('keydown', onKey); // Use capture so we beat ReactFlow's own handlers and any stopPropagation. - window.addEventListener('mousedown', onPointer, true); - window.addEventListener('contextmenu', onPointer, true); + window.addEventListener('mousedown', onClickOutside, true); + window.addEventListener('contextmenu', onClickOutside, true); return (): void => { window.removeEventListener('keydown', onKey); - window.removeEventListener('mousedown', onPointer, true); - window.removeEventListener('contextmenu', onPointer, true); + window.removeEventListener('mousedown', onClickOutside, true); + window.removeEventListener('contextmenu', onClickOutside, true); }; - }, [contextMenu, closeContextMenu]); + }, [contextMenu]); return (
@@ -385,21 +382,17 @@ export function WorkflowCanvas({ /> )} - {/* Right-click context menu (#971). Positioned with fixed viewport coords. */} {contextMenu && (