Skip to content

fix(web): allow deleting nodes from Workflow Builder (#971)#1113

Merged
Wirasm merged 4 commits intocoleam00:devfrom
medevs:fix/971-workflow-builder-delete
Apr 22, 2026
Merged

fix(web): allow deleting nodes from Workflow Builder (#971)#1113
Wirasm merged 4 commits intocoleam00:devfrom
medevs:fix/971-workflow-builder-delete

Conversation

@medevs
Copy link
Copy Markdown
Contributor

@medevs medevs commented Apr 12, 2026

Summary

  • Problem: Nodes added to the Workflow Builder canvas cannot be deleted — Delete/Backspace keys do nothing after drag-and-drop, no right-click context menu exists, and the Delete Node button is either hidden below the viewport (Prompt/Command) or entirely absent (Bash nodes).
  • Why it matters: Bash nodes are impossible to remove through any UI mechanism once placed. Other node types require obscure scrolling to find the delete button.
  • What changed: Auto-select dropped nodes, added right-click context menu, moved Delete button to inspector header, added Backspace key support.
  • What did not change (scope boundary): No new dependencies, no changes to workflow execution, no backend changes. Purely frontend UX fix in @archon/web.

UX Journey

Before

User                        Workflow Builder             ReactFlow
────                        ────────────────             ─────────
drags Prompt node ────────▶ onDrop() fires
                            creates node
                            [X] never calls onNodeSelect()
                            selectedNodeId stays null
presses Delete ───────────▶ useBuilderKeyboard
                            [X] if (!selectedNodeId) return
                            (silent no-op)
right-clicks node ────────▶ [X] no onContextMenu handler
                            (no menu rendered)
opens inspector ──────────▶ NodeInspector renders
navigates to Advanced tab ▶ Delete button present
                            [X] button below viewport
                            (requires scroll to discover)

drags Bash node ──────────▶ onDrop() fires, creates node
opens inspector ──────────▶ NodeInspector renders
                            isBash = true
                            [X] Advanced tab hidden
                            Delete button absent from DOM
user cannot delete ◀─────── zero UI mechanism for deletion

After

User                        Workflow Builder             ReactFlow
────                        ────────────────             ─────────
drags any node ───────────▶ onDrop() fires
                            creates node
                            *calls onNodeSelect(id)*
                            selectedNodeId = new node
presses Delete/Backspace ─▶ useBuilderKeyboard
                            *selectedNodeId is set*
                            *node deleted* ✓
right-clicks node ────────▶ *handleNodeContextMenu*
                            *context menu rendered*
clicks "Delete node" ─────▶ *onNodeDelete(nodeId)*
                            *node deleted* ✓
opens inspector ──────────▶ NodeInspector renders
                            *Delete button in header*
                            *visible for ALL types* ✓
clicks header Delete ─────▶ *node deleted* ✓

Architecture Diagram

Before

WorkflowBuilder
├── WorkflowCanvas (onDrop creates node, no select)
│   └── ReactFlow (no onNodeContextMenu)
├── NodeInspector
│   └── AdvancedTab (Delete button here, hidden for bash)
└── useBuilderKeyboard (Delete key only, needs selectedNodeId)

After

WorkflowBuilder
├── WorkflowCanvas (onDrop creates node + [~] auto-selects)
│   ├── ReactFlow ([+] onNodeContextMenu)
│   └── [+] Context menu div (Delete node button)
├── NodeInspector
│   ├── [~] Header (Delete button moved here, all node types)
│   └── AdvancedTab ([-] Delete button removed)
└── useBuilderKeyboard ([~] Delete + Backspace keys)

Connection inventory:

From To Status Notes
WorkflowBuilder WorkflowCanvas modified New onNodeDelete prop
WorkflowCanvas ReactFlow modified Added onNodeContextMenu handler
WorkflowCanvas Context menu new Inline positioned div with delete action
WorkflowBuilder NodeInspector unchanged onDelete prop unchanged
NodeInspector AdvancedTab modified Removed onDelete prop from AdvancedTab
useBuilderKeyboard WorkflowBuilder modified Now handles Backspace alongside Delete

Label Snapshot

  • Risk: risk: low
  • Size: size: S
  • Scope: web
  • Module: web:WorkflowCanvas, web:NodeInspector, web:WorkflowBuilder, web:useBuilderKeyboard

Change Metadata

  • Change type: bug
  • Primary scope: web

Linked Issue

Validation Evidence (required)

bun run type-check  ✅ clean across all 9 packages
bun run lint        ✅ clean (--max-warnings 0)
bun run format:check ✅ all source files pass (only HANDOFF.md flagged — not part of PR)
bun run test        ✅ all tests pass across all 9 packages
  • Evidence provided: Full bun run validate output and manual browser testing
  • If any command is intentionally skipped, explain why: N/A — all passed

Security Impact (required)

  • New permissions/capabilities? No
  • New external network calls? No
  • Secrets/tokens handling changed? No
  • File system access scope changed? No

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Database migration needed? No

Human Verification (required)

What was personally validated beyond CI:

  • Verified scenarios:
    • Drag Prompt node → press Delete → node removed
    • Drag Prompt node → press Backspace → node removed
    • Drag Command node → right-click → "Delete node" → node removed
    • Drag Bash node → press Delete → node removed
    • Drag Bash node → open inspector → header Delete button visible → click → node removed
    • Right-click → Escape → menu dismissed, node remains
    • Right-click → click canvas → menu dismissed, node remains
    • Type in NodeInspector textarea → Backspace edits text (does NOT delete node)
    • Delete connected node → edges also removed
  • Edge cases checked:
    • Backspace in text fields does not trigger node deletion (isInputTarget guard)
    • Context menu positioned correctly and doesn't overflow viewport
    • Dark mode styling uses existing Tailwind tokens
  • What was not verified: Automated component tests (no Vitest/RTL setup exists in @archon/web)

Side Effects / Blast Radius (required)

  • Affected subsystems/workflows: Workflow Builder UI only
  • Potential unintended effects: None — changes are isolated to 4 frontend files, no backend or workflow engine impact
  • Guardrails/monitoring for early detection: Type-check + lint + existing unit tests all pass

Rollback Plan (required)

  • Fast rollback command/path: git revert <commit-sha> — single atomic commit
  • Feature flags or config toggles (if any): None
  • Observable failure symptoms: Node deletion not working in the Workflow Builder (same as current state before fix)

Risks and Mitigations

  • Risk: e.preventDefault() on Backspace could cause unexpected behavior if focus escapes a text input
    • Mitigation: isInputTarget() guard in useBuilderKeyboard checks for input, textarea, and [contenteditable] before the keydown handler fires
  • Risk: Inline context menu styling may look off in certain themes
    • Mitigation: Uses existing project Tailwind tokens (bg-surface-elevated, border-border, text-error) consistent with the rest of the UI

Summary by CodeRabbit

  • New Features

    • Right-click context menu on the workflow canvas to delete nodes.
    • Prominent Delete button moved to the inspector header (visible for all node types).
    • Newly created nodes are auto-selected for immediate editing.
  • Bug Fixes / Improvements

    • Backspace now works alongside Delete to remove selected nodes.
    • Removed duplicate Delete control from the Advanced tab.
  • Documentation

    • Workflow Builder docs updated to describe node deletion options.

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 coleam00#971.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

Moved deletion UI out of the Advanced tab into a persistent inspector header, added a canvas right-click context menu with Delete action, auto-select newly created nodes on drop/quick-add, and made Backspace and Delete both trigger node deletion.

Changes

Cohort / File(s) Summary
Inspector UI
packages/web/src/components/workflows/NodeInspector.tsx
Removed Advanced-tab inline "Delete Node"; added persistent destructive "Delete" button in DagInspector header (always shown, aria-label="Delete node").
Builder deletion logic
packages/web/src/components/workflows/WorkflowBuilder.tsx
Introduced nodesRef/edgesRef and pushSnapshotLatest(); added handleNodeDeleteById(nodeId: string) to remove node + incident edges, clear selection, mark dirty; refactored existing delete calls to delegate to it.
Canvas interactions & context menu
packages/web/src/components/workflows/WorkflowCanvas.tsx
WorkflowCanvasProps now requires onNodeDelete(nodeId); added right-click context menu with "Delete node" action, onNodeContextMenu registration, dismissal on Escape/outside clicks, and auto-selects newly created nodes (drop & quick-add) while calling onPushSnapshot.
Keyboard shortcuts & exports
packages/web/src/hooks/useBuilderKeyboard.ts, packages/web/src/hooks/useBuilderKeyboard.test.ts
Exported BuilderKeyboardActions, isInputTarget, and new handleBuilderKeydown; isInputTarget recognizes ARIA roles (combobox,textbox,searchbox); both Delete and Backspace now invoke onDeleteSelected() and call preventDefault(); added tests covering new behaviors.
Docs & scripts
packages/docs-web/src/content/docs/adapters/web.md, packages/web/package.json
Documented node deletion UX and added src/hooks/ to the test script.

Sequence Diagram

sequenceDiagram
    participant User
    participant Canvas as WorkflowCanvas
    participant Builder as WorkflowBuilder
    participant Inspector as NodeInspector
    participant Keyboard as useBuilderKeyboard

    rect rgba(200, 150, 255, 0.5)
    Note over User,Canvas: Drag-and-Drop Node
    User->>Canvas: Drag node onto canvas
    Canvas->>Canvas: onDrop() creates node
    Canvas->>Canvas: onNodeSelect(id) — auto-select new node
    Canvas->>Inspector: open/refresh inspector for selected node
    end

    rect rgba(100, 200, 255, 0.5)
    Note over User,Keyboard: Delete via Keyboard
    User->>Keyboard: Press Delete or Backspace
    Keyboard->>Keyboard: preventDefault()
    Keyboard->>Builder: actions.onDeleteSelected()
    Builder->>Builder: handleNodeDeleteById(selectedId)
    Builder->>Canvas: remove node & edges, clear selection
    end

    rect rgba(150, 200, 150, 0.5)
    Note over User,Canvas: Delete via Context Menu
    User->>Canvas: Right-click node
    Canvas->>Canvas: onNodeContextMenu() opens menu, auto-select node
    User->>Canvas: Click "Delete node"
    Canvas->>Builder: onNodeDelete(nodeId)
    Builder->>Builder: handleNodeDeleteById(nodeId)
    Builder->>Canvas: remove node & edges, clear selection
    end

    rect rgba(200, 200, 100, 0.5)
    Note over User,Inspector: Delete via Header Button
    User->>Inspector: Click "Delete" in DagInspector header
    Inspector->>Builder: onDelete()
    Builder->>Builder: handleNodeDelete() → handleNodeDeleteById()
    Builder->>Canvas: remove node & edges, clear selection
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I hopped through nodes both new and old,
A right-click menu, a button bold.
Backspace joins Delete in the dance,
New nodes auto-selected—give them a chance!
Hooray for tidy DAGs, carrots in advance 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The PR addresses all coding requirements from issue #971 (auto-select, context menu, Delete button visibility, Backspace support) but the pr_objectives note that critical review feedback (C1: stale undo snapshot, C2: context menu viewport overflow, I1: ARIA role violations, I3: isInputTarget improvements) was partially addressed in a follow-up commit. The current state of fixes for these items is unclear from the provided context. Verify that critical issues C1 and C2 regarding stale undo snapshots and viewport-clamped menu positioning have been fully resolved in the current commit state, and confirm that ARIA and isInputTarget improvements align with the implemented fixes.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'fix(web): allow deleting nodes from Workflow Builder (#971)' clearly and concisely describes the main change — enabling node deletion in the Workflow Builder — and directly addresses the linked issue.
Description check ✅ Passed The PR description comprehensively covers all required template sections: problem statement, justification, detailed changes with UX journeys and architecture diagrams, validation evidence (type-check, lint, format, test), security assessment, compatibility notes, human verification with specific scenarios, side effects analysis, and rollback plan.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the node deletion feature: WorkflowBuilder/Canvas/Inspector keyboard/menu/button changes, useBuilderKeyboard enhancements, documentation updates, and test additions. No unrelated refactoring, dependency changes, backend modifications, or workflow execution logic changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/web/src/components/workflows/WorkflowCanvas.tsx`:
- Around line 175-181: The canvas-created node flows call setNodes(...) and then
onNodeSelect/onDirty but never call onPushSnapshot, so undo/redo won't capture
this addition; update both places that append a node (the handler using setNodes
and the other occurrence around lines 292-299) to invoke onPushSnapshot?.()
immediately before mutating state (i.e., before calling setNodes) so the new
node addition is pushed as a snapshot; keep the existing onNodeSelect(id) and
onDirty() calls after the setNodes call.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b781b628-ca2b-4875-80e9-cc3581c0876f

📥 Commits

Reviewing files that changed from the base of the PR and between 536584d and 8a40e69.

📒 Files selected for processing (4)
  • packages/web/src/components/workflows/NodeInspector.tsx
  • packages/web/src/components/workflows/WorkflowBuilder.tsx
  • packages/web/src/components/workflows/WorkflowCanvas.tsx
  • packages/web/src/hooks/useBuilderKeyboard.ts

Comment thread packages/web/src/components/workflows/WorkflowCanvas.tsx Outdated
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) <noreply@anthropic.com>
@Wirasm Wirasm closed this Apr 20, 2026
@Wirasm Wirasm reopened this Apr 20, 2026
@Wirasm
Copy link
Copy Markdown
Collaborator

Wirasm commented Apr 21, 2026

PR Review Summary — #1113 (fix/971-workflow-builder-delete)

Multi-agent review: code-reviewer, docs-impact, comment-analyzer, pr-test-analyzer, code-simplifier.

Verdict: NEEDS FIXES (minor — no blockers to the fix itself)

The UX fix is correct and all three new delete paths (auto-select, context menu, header button) work. The issues below are about undo-stack correctness under tight timing, minor a11y, viewport clamping, and CLAUDE.md comment-style compliance.


Critical Issues

ID Agent Issue Location
C1 code-reviewer handleNodeDeleteById captures nodes/edges in its dep array, so pushSnapshot({nodes, edges}) can snapshot pre-drop state if the user presses Delete/Backspace in the same tick as auto-select after a drop. Undo stack can record a snapshot that does not include the just-dropped node. Fix: hold nodes/edges in refs and drop them from the dep array. WorkflowBuilder.tsx:236-245
C2 code-reviewer Context menu uses position: fixed with raw e.clientX/e.clientY; no clamping. A right-click near the viewport edge renders the menu partially off-screen. Fix: clamp with Math.min(x, innerWidth - MENU_W) / Math.min(y, innerHeight - MENU_H). WorkflowCanvas.tsx:389-409

Important Issues

ID Agent Issue Location
I1 code-reviewer role="menu" + role="menuitem" without autoFocus, tabIndex, or arrow-key handling violates the ARIA menu contract. For a single-item menu the cleanest fix is to drop the roles entirely and rely on the already-accessible <button>. WorkflowCanvas.tsx:390-408
I2 code-reviewer onPushSnapshot={() => pushSnapshot({ nodes, edges })} is an inline closure (not memoized). Causes onDrop to reconstruct every render and introduces a narrow stale-closure window. Fix: useCallback or the ref pattern from C1. WorkflowBuilder.tsx:491-493
I3 code-reviewer isInputTarget() covers INPUT/TEXTAREA/SELECT/contentEditable but not role="combobox" / role="textbox" / role="searchbox". A future shadcn/Radix widget with focus could cause Backspace to delete a node while the user is editing. Extend the guard to check ARIA roles. useBuilderKeyboard.ts:104-109
I4 comment-analyzer CLAUDE.md explicitly says not to reference issue numbers in comments ("they rot"). Seven 971-references were added across the four files. NodeInspector.tsx:701, WorkflowBuilder.tsx:235, WorkflowCanvas.tsx:178/295/308/388, useBuilderKeyboard.ts:107
I5 comment-analyzer The handleNodeDeleteById block comment describes WHAT + lists callers; CLAUDE.md says to avoid WHAT comments. Callers are IDE-findable. WorkflowBuilder.tsx:233
I6 pr-test-analyzer useBuilderKeyboard.ts is a pure event handler already testable with bun test — no RTL needed. The Backspace guard has high regression potential (silent data loss if a future edit breaks the isInputTarget check). Rating: 8/10. One focused test is ~30 min of work. useBuilderKeyboard.ts

Suggestions

ID Agent Suggestion Location
S1 code-simplifier Collapse handleNodeDeleteById + handleNodeDelete into one callback with an id param. Callers all have access to selectedNodeId. Removes one useCallback, one dep array, and the block comment explaining the pair. WorkflowBuilder.tsx:232-246
S2 code-simplifier closeContextMenu = useCallback(() => setContextMenu(null), []) wraps a stable setter — no value, and it bloats the useEffect dep array. Call setContextMenu(null) directly. WorkflowCanvas.tsx (new code)
S3 code-simplifier Rename onPointeronClickOutside. "Pointer" in web API terms means PointerEvent; the handler takes MouseEvent. WorkflowCanvas.tsx
S4 docs-impact packages/docs-web/src/content/docs/adapters/web.md:170-177 — the Workflow Builder section doesn't mention the new delete affordances. The "keyboard shortcuts" bullet (line 174) currently implies only undo/redo. Add a "Delete node" bullet covering Delete/Backspace, the header button, and the right-click menu. docs-web

Strengths

  • { capture: true } on the dismissal listeners correctly beats ReactFlow's own bubble-phase handlers and any stopPropagation() calls (verified by comment-analyzer).
  • isInputTarget() guard for the existing Delete case already covered the common cases; extending to Backspace is the right call.
  • The onPushSnapshot?.() calls added in onDrop and handleQuickAddNode are a real correctness win — undo before add was missing before this PR.
  • Moving Delete to the inspector header (visible for bash which has no Advanced tab) is the correct UX resolution for the original bug.
  • No type leaks, no any, no new dependencies, no backend changes — scope matches the PR description exactly.

Recommended Next Steps

  1. Fix C1 (stale-closure snapshot) and C2 (viewport clamping) before merge.
  2. Remove the 7 issue-number references in comments per CLAUDE.md (I4).
  3. Consider dropping role="menu"/role="menuitem" (I1) — the simplest conformant path.
  4. Add a short useBuilderKeyboard test for the Delete/Backspace + isInputTarget invariant (I6).
  5. One-line docs addition in web.md (S4).
  6. I2, I3, I5, and the simplifications (S1–S3) are nice-to-have and can land in a follow-up.

medevs and others added 2 commits April 21, 2026 18:46
- 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) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (5)
packages/web/src/components/workflows/WorkflowBuilder.tsx (2)

400-404: Redundant double-guard on selectedNodeId.

onDeleteSelected guards on selectedNodeId and then calls handleNodeDelete, which performs the same guard. You can simplify by calling handleNodeDelete directly (it already no-ops when nothing is selected) — this was also flagged as suggestion S1 in the review summary.

Proposed simplification
-      onDeleteSelected: (): void => {
-        if (selectedNodeId) {
-          handleNodeDelete();
-        }
-      },
+      onDeleteSelected: handleNodeDelete,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/components/workflows/WorkflowBuilder.tsx` around lines 400 -
404, The onDeleteSelected handler redundantly checks selectedNodeId before
calling handleNodeDelete which already no-ops when nothing is selected; remove
the outer guard so onDeleteSelected simply calls handleNodeDelete directly
(update the onDeleteSelected function to call handleNodeDelete unconditionally
and remove references to selectedNodeId inside that handler).

175-186: Ref-sync pattern: consider syncing during render for concurrent-safety.

Syncing refs in a useEffect is generally fine here because pushSnapshotLatest is invoked from user-driven event handlers that run after commit. However, under React 19 concurrent rendering, a render can be started and discarded before the effect runs, which means any callback that reads nodesRef.current between a committed state update and the effect could momentarily see a stale value.

For strictly latest-state reads in callbacks, a common pattern is to assign during render:

Alternative ref-sync pattern
-  const nodesRef = useRef(nodes);
-  const edgesRef = useRef(edges);
-  useEffect(() => {
-    nodesRef.current = nodes;
-    edgesRef.current = edges;
-  }, [nodes, edges]);
+  const nodesRef = useRef(nodes);
+  const edgesRef = useRef(edges);
+  // Sync during render so event handlers fired between render and effect
+  // still observe the latest committed state.
+  nodesRef.current = nodes;
+  edgesRef.current = edges;

Functionally equivalent for this PR's use case — raising as an optional refinement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/components/workflows/WorkflowBuilder.tsx` around lines 175 -
186, The current pattern updates nodesRef/edgesRef inside a useEffect which can
be stale under concurrent rendering; instead assign nodesRef.current = nodes and
edgesRef.current = edges during render (before defining pushSnapshotLatest) so
pushSnapshotLatest (which uses nodesRef/edgesRef) always reads the freshest
values; locate the refs named nodesRef and edgesRef and move the sync into
render (or at least perform the assignment immediately before the useCallback
for pushSnapshotLatest), and remove or keep the effect only if you still need it
for legacy timing.
packages/web/src/hooks/useBuilderKeyboard.test.ts (1)

86-136: LGTM — focused tests addressing review feedback I6.

The delete invariant coverage is comprehensive: Delete/Backspace on canvas fires onDeleteSelected; INPUT/TEXTAREA/contentEditable/ARIA combobox/textbox all suppress; enabled=false suppresses shortcuts. Tests are deterministic and don't rely on mock.module.

Optional: consider also asserting e.preventDefault was called on the Delete/Backspace canvas cases to pin down that invariant, since downstream ReactFlow behavior depends on it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/hooks/useBuilderKeyboard.test.ts` around lines 86 - 136, Add
assertions that the synthetic event's preventDefault was called in the canvas
Delete/Backspace tests to ensure the handler suppresses native behavior: in the
tests that call handleBuilderKeydown(makeEvent('Delete'..., actions) and
makeEvent('Backspace'..., actions) where tagName is 'DIV', assert that the event
object's preventDefault was invoked (alongside the existing expect on
actions.calls.onDeleteSelected) by checking the mock event created by makeEvent;
reference the handleBuilderKeydown and makeEvent helpers and the
actions.calls.onDeleteSelected expectation when adding these preventsDefault
assertions.
packages/web/src/components/workflows/WorkflowCanvas.tsx (2)

304-317: Viewport clamping addresses C2; consider also clamping the lower bound.

The right/bottom clamping with approximate menu dimensions correctly prevents overflow in the common case. One optional hardening: if the viewport is narrower than CONTEXT_MENU_WIDTH (rare, but possible on very small windows/iframes), innerWidth - CONTEXT_MENU_WIDTH is negative and the menu is pushed off-screen to the left. Clamping the lower bound to 0 avoids that:

Proposed refinement
-      const x = Math.min(e.clientX, window.innerWidth - CONTEXT_MENU_WIDTH);
-      const y = Math.min(e.clientY, window.innerHeight - CONTEXT_MENU_HEIGHT);
+      const x = Math.max(0, Math.min(e.clientX, window.innerWidth - CONTEXT_MENU_WIDTH));
+      const y = Math.max(0, Math.min(e.clientY, window.innerHeight - CONTEXT_MENU_HEIGHT));

Also, CONTEXT_MENU_WIDTH/CONTEXT_MENU_HEIGHT are static values — hoisting them to module scope (alongside the existing resolveNodeLabel) avoids re-binding on each render.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/components/workflows/WorkflowCanvas.tsx` around lines 304 -
317, The context menu position logic in handleNodeContextMenu can produce
negative x/y when the viewport is smaller than CONTEXT_MENU_WIDTH/HEIGHT; change
the clamping to Math.max(0, Math.min(...)) so x = Math.max(0,
Math.min(e.clientX, window.innerWidth - CONTEXT_MENU_WIDTH)) and similarly for y
before calling setContextMenu({ x, y, nodeId: node.id }). Also hoist
CONTEXT_MENU_WIDTH and CONTEXT_MENU_HEIGHT to module scope (next to
resolveNodeLabel) so they are not re-bound on each render.

385-402: Dropping role="menu"/role="menuitem" resolves I1.

For a single-action popover, relying on native <button> semantics is more correct than asserting menu roles without full arrow-key navigation and focus management. If/when this grows to multiple items, either implement the full ARIA menu pattern (roving tabindex, arrow keys, aria-activedescendant) or reach for a primitive like @radix-ui/react-dropdown-menu.

One small accessibility gap remains: the menu doesn't auto-focus when opened, so keyboard-only users who right-click via Shift+F10 can't tab directly to Delete without leaving the canvas. Non-blocking for this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/components/workflows/WorkflowCanvas.tsx` around lines 385 -
402, The context menu currently relies on a native button (good) but it doesn't
auto-focus when opened; add a ref (e.g., deleteButtonRef) to the Delete button
and, inside a useEffect that watches contextMenu, call
deleteButtonRef.current?.focus() when contextMenu is non-null so keyboard users
can immediately tab/activate the action; keep the existing contextMenuRef and
setContextMenu/onNodeDelete logic unchanged and do not introduce ARIA
menu/menuitem roles unless you implement full menu keyboard handling later.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/web/src/components/workflows/WorkflowBuilder.tsx`:
- Around line 400-404: The onDeleteSelected handler redundantly checks
selectedNodeId before calling handleNodeDelete which already no-ops when nothing
is selected; remove the outer guard so onDeleteSelected simply calls
handleNodeDelete directly (update the onDeleteSelected function to call
handleNodeDelete unconditionally and remove references to selectedNodeId inside
that handler).
- Around line 175-186: The current pattern updates nodesRef/edgesRef inside a
useEffect which can be stale under concurrent rendering; instead assign
nodesRef.current = nodes and edgesRef.current = edges during render (before
defining pushSnapshotLatest) so pushSnapshotLatest (which uses
nodesRef/edgesRef) always reads the freshest values; locate the refs named
nodesRef and edgesRef and move the sync into render (or at least perform the
assignment immediately before the useCallback for pushSnapshotLatest), and
remove or keep the effect only if you still need it for legacy timing.

In `@packages/web/src/components/workflows/WorkflowCanvas.tsx`:
- Around line 304-317: The context menu position logic in handleNodeContextMenu
can produce negative x/y when the viewport is smaller than
CONTEXT_MENU_WIDTH/HEIGHT; change the clamping to Math.max(0, Math.min(...)) so
x = Math.max(0, Math.min(e.clientX, window.innerWidth - CONTEXT_MENU_WIDTH)) and
similarly for y before calling setContextMenu({ x, y, nodeId: node.id }). Also
hoist CONTEXT_MENU_WIDTH and CONTEXT_MENU_HEIGHT to module scope (next to
resolveNodeLabel) so they are not re-bound on each render.
- Around line 385-402: The context menu currently relies on a native button
(good) but it doesn't auto-focus when opened; add a ref (e.g., deleteButtonRef)
to the Delete button and, inside a useEffect that watches contextMenu, call
deleteButtonRef.current?.focus() when contextMenu is non-null so keyboard users
can immediately tab/activate the action; keep the existing contextMenuRef and
setContextMenu/onNodeDelete logic unchanged and do not introduce ARIA
menu/menuitem roles unless you implement full menu keyboard handling later.

In `@packages/web/src/hooks/useBuilderKeyboard.test.ts`:
- Around line 86-136: Add assertions that the synthetic event's preventDefault
was called in the canvas Delete/Backspace tests to ensure the handler suppresses
native behavior: in the tests that call
handleBuilderKeydown(makeEvent('Delete'..., actions) and
makeEvent('Backspace'..., actions) where tagName is 'DIV', assert that the event
object's preventDefault was invoked (alongside the existing expect on
actions.calls.onDeleteSelected) by checking the mock event created by makeEvent;
reference the handleBuilderKeydown and makeEvent helpers and the
actions.calls.onDeleteSelected expectation when adding these preventsDefault
assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: efb2655b-869a-4c6e-8bb6-ecf53072717c

📥 Commits

Reviewing files that changed from the base of the PR and between b9c75dc and fc67740.

📒 Files selected for processing (7)
  • packages/docs-web/src/content/docs/adapters/web.md
  • packages/web/package.json
  • packages/web/src/components/workflows/NodeInspector.tsx
  • packages/web/src/components/workflows/WorkflowBuilder.tsx
  • packages/web/src/components/workflows/WorkflowCanvas.tsx
  • packages/web/src/hooks/useBuilderKeyboard.test.ts
  • packages/web/src/hooks/useBuilderKeyboard.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/web/package.json
  • packages/docs-web/src/content/docs/adapters/web.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/web/src/hooks/useBuilderKeyboard.ts

@Wirasm Wirasm merged commit d7f36b2 into coleam00:dev Apr 22, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(web): Workflow Builder nodes cannot be deleted after drag-and-drop (no auto-select, no context menu, missing delete button for bash nodes)

2 participants