Skip to content

feat(ui): build node palette sidebar with drag-and-drop onto workflow canvas#1112

Open
geoffjay wants to merge 2 commits intoissue-1100from
issue-1099
Open

feat(ui): build node palette sidebar with drag-and-drop onto workflow canvas#1112
geoffjay wants to merge 2 commits intoissue-1100from
issue-1099

Conversation

@geoffjay
Copy link
Copy Markdown
Owner

Adds collapsible NodePalette sidebar with all 13 trigger types grouped by category, agent list, search filtering, and HTML5 drag-and-drop with encoded transfer data.

Closes #1099

@geoffjay geoffjay added the review-agent Used to invoke a review by an agent tracking this label label Apr 14, 2026
Copy link
Copy Markdown
Owner Author

@geoffjay geoffjay left a comment

Choose a reason for hiding this comment

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

Review: feat(ui): build node palette sidebar with drag-and-drop onto workflow canvas

LGTM — cannot self-approve, posting as comment.

Stack note

This branch (issue-1099) is stacked on PR #1111 (issue-1100: "feat(ui): implement graph-to-API serialization for workflow canvas save/load"), which also carries review-agent. #1111 should be reviewed first. The chain above this PR — #1113 (needs-rework) and #1114 (merge-queue) — is blocked until #1113's edit-mode bug is fixed, but that doesn't affect this PR's code quality.

What was verified

  • Drag transfer data: encodeDragData / decodeDragData roundtrip via JSON.stringify / JSON.parse; PALETTE_DRAG_KEY = "application/agentd-palette" is a namespaced MIME type — correct for cross-origin drag safety. ✓
  • ALL_TRIGGER_TYPES: 13 entries, matching the TriggerConfig discriminated union in orchestrator.ts (github_issues, github_pull_requests, linear_issues, webhook, cron, delay, agent_lifecycle, agent_idle, dispatch_result, ask_response, manual, queue, composite). ✓
  • TRIGGER_ICONS completeness: one icon per type, with ?? Hand fallback in the render. ✓
  • Disabled state: draggable={!disabled} + onDragStart={!disabled ? handleDragStart : undefined} — both attributes gated, non-running agents are non-draggable. ✓
  • groupedTriggers memo: dep is [searchLower], correctly recomputes on search change. ✓
  • filteredAgents memo: dep is [agents, searchLower]. ✓
  • Collapse: controlled path: collapsed = collapsedProp ?? internalCollapsed — controlled prop takes precedence; uncontrolled falls through to internal state. ✓
  • Exports: NodePalette, NodePaletteProps, PaletteDragData, PALETTE_DRAG_KEY, encodeDragData, decodeDragData all re-exported from canvas/index.ts. ✓
  • Test coverage: 22 tests across rendering, collapse/expand, search filtering (4 cases including case-insensitive), drag data correctness (trigger + agent), disabled agent, empty agents. ✓
  • Drag tests use fireEvent: appropriate — userEvent does not support HTML5 drag in jsdom. ✓

Non-blocking suggestions

1. Doc comment says "14 types" — should be 13

The file-level JSDoc says "all 14 types" and orchestrator.ts also has "covers all 14 backend trigger variants". Both are off by one — the actual union has 13 members and the test asserts exactly 13. Updating both comments to "13" would eliminate confusion if a 14th type is added later and someone wonders whether the palette is missing it.

2. Controlled/uncontrolled state drift in toggleCollapsed

function toggleCollapsed() {
    const next = !collapsed;
    setInternalCollapsed(next);   // ← always runs, even in controlled mode
    onCollapsedChange?.(next);
}

When collapsedProp is provided, internalCollapsed still advances, though it has no visible effect (the controlled prop shadows it). If the parent later stops providing collapsedProp, the palette would snap to the stale internal value. The standard fix is to only update internal state when uncontrolled:

function toggleCollapsed() {
    const next = !collapsed;
    if (collapsedProp === undefined) setInternalCollapsed(next);
    onCollapsedChange?.(next);
}

Non-blocking — the current behaviour is correct for all documented use cases.

3. decodeDragData has no structural validation

JSON.parse(raw) as PaletteDragData | null is a TypeScript-only cast; the parsed object is not validated at runtime. If the drop handler receives malformed data (e.g., type: "agent" with no agentId), downstream code in WorkflowBuilder.handleDrop would receive undefined where a string is expected. Since this is a local UI and drag data is always set by encodeDragData, the risk is low — but a simple guard (if (!decoded || !('type' in decoded)) return null) would make the function defensive. Non-blocking.

@geoffjay geoffjay added merge-queue Approved by reviewer, queued for merge by conductor and removed review-agent Used to invoke a review by an agent tracking this label labels Apr 17, 2026
…canvas

- Categorised trigger list (External, Schedules, Events, Internal) with icons
- Agents section from live API data — running agents draggable, others grayed out
- HTML5 drag-and-drop: sets PALETTE_DRAG_KEY transfer data for canvas onDrop
- Search/filter input (case-insensitive, matches triggers and agents)
- Collapse/expand toggle (controlled + uncontrolled modes)
- PaletteDragData type + encode/decode helpers exported for canvas integration
- 21 tests: rendering, collapse, search, drag data, disabled states
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-queue Approved by reviewer, queued for merge by conductor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant