Skip to content

feat(desktop): workflow builder UI with form editor, CRUD, and channel picker#231

Merged
wesbillman merged 22 commits intomainfrom
workflow-improvements
Apr 4, 2026
Merged

feat(desktop): workflow builder UI with form editor, CRUD, and channel picker#231
wesbillman merged 22 commits intomainfrom
workflow-improvements

Conversation

@wesbillman
Copy link
Copy Markdown
Collaborator

Summary

  • Form-based workflow builder replacing the raw YAML textarea — structured UI for trigger config, step editing with per-action fields, and YAML toggle for power users
  • Full CRUD support — create, edit, duplicate, and delete workflows with confirmation dialogs
  • Searchable channel picker (ChannelCombobox) with keyboard navigation, filtering, and proper a11y
  • Dialog layout with fixed header/footer and scrollable content area
  • autoCapitalize/autoCorrect off on all ID, snake_case, pubkey, URL, cron, and emoji fields
  • Human-readable step IDs (step_1, step_2) instead of UUIDs
  • Playwright E2E tests covering workflow CRUD operations
  • 17 files changed, +1,773 / -199 lines across 19 commits

New files

  • ChannelCombobox.tsx — searchable channel picker with Radix Popover + keyboard nav
  • WorkflowFormBuilder.tsx — form-based workflow editor with YAML toggle
  • WorkflowStepCard.tsx — per-step editor with action-specific config fields
  • WorkflowDialog.tsx — shared create/edit/duplicate dialog
  • WorkflowDeleteDialog.tsx — delete confirmation
  • workflowFormTypes.ts — types, YAML serialization, step ID generation
  • workflowFormPrimitives.tsx — shared form components (FieldLabel, FormSelect)
  • workflows.spec.ts — 6 Playwright E2E tests
  • e2eBridge.ts additions — mock workflow CRUD handlers

Test plan

  • Open the workflow builder from a channel header — verify form-based UI loads
  • Create a workflow with trigger + steps — verify YAML serialization is correct
  • Edit an existing workflow — verify form pre-populates from YAML
  • Duplicate a workflow — verify "(copy)" suffix and new creation
  • Delete a workflow — verify confirmation dialog and removal
  • Test channel picker search/filter with keyboard navigation (arrow keys + enter)
  • Verify dialog scrolls correctly with 10+ steps (header/footer pinned)
  • Run pnpm test:e2e for Playwright tests

🤖 Generated with Claude Code

wesbillman and others added 19 commits April 3, 2026 13:45
Allow users to create workflows directly from a channel view via a Zap
button in the channel header bar. The dialog pre-selects the current
channel and hides the channel selector since context is already known.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove dead branches in CreateWorkflowDialog (channels.length === 0
checks inside a channels.length > 1 guard can never be true). Biome
auto-sorted imports in ChannelMembersBar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DM channels don't support workflows, so disable the Zap button when
channelType is "dm" — matching the existing guard on the Add Agent button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…description

Reuse canAddAgents guard for the Zap button (no DMs, no archived, respects
canManageMembers for private channels). Adapt dialog description text when
opened from single-channel context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 2 — adds a proper form UI for creating workflows:
- WorkflowFormBuilder with name field, trigger type selector, dynamic
  trigger config fields, and step builder with add/remove
- Supports all trigger types (message_posted, reaction_added, webhook,
  schedule) and all action types (delay, send_message, send_dm,
  call_webhook, request_approval, add_reaction, set_channel_topic)
- "Edit as YAML" toggle for power users with bidirectional form↔YAML sync
- Split into three files to stay under the 500-line limit:
  workflowFormTypes.ts, WorkflowStepCard.tsx, WorkflowFormBuilder.tsx
- Added yaml npm package for serialization/parsing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nents

Extract duplicated FormSelect and FieldLabel components from
WorkflowFormBuilder and WorkflowStepCard into a shared
workflowFormPrimitives module.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CreateWorkflowDialog now uses shared FormSelect/FieldLabel from
  workflowFormPrimitives instead of an inline select with duplicated styles
- Fix step ID collision: use timestamp+counter instead of steps.length
  which produces duplicates after deletions
- Extract stable resetMutation ref to avoid unnecessary reset callback
  re-creation on every render

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use crypto.randomUUID() for step IDs to prevent React key collisions
- Add https:// URL validation warning on call_webhook steps
- Set explicit method: "POST" default when creating call_webhook steps
  and in serialization, so display always matches YAML output

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add Array.isArray guard in yamlToFormState to handle malformed steps
  field gracefully instead of crashing on .map()
- Wire up id/htmlFor on all step card inputs for proper label association

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add id attributes to all step card form inputs and connect them to
FieldLabel via htmlFor, using a wf-step-{index} prefix pattern for
unique DOM IDs. Clicking a label now focuses the associated input.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Create WorkflowDialog component supporting create/edit/duplicate modes
- Refactor CreateWorkflowDialog as thin wrapper for backwards compatibility
- Add Edit and Duplicate actions to WorkflowCard dropdown menu
- Add Edit button to WorkflowDetailPanel header
- Wire up dialog state management in WorkflowsView with discriminated union
- Edit mode pre-populates form from existing definition, calls updateWorkflow
- Duplicate mode pre-fills form with "(copy)" suffix, creates new workflow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract stable .mutate references from triggerMutation and deleteMutation
so useCallback deps don't change on every render. Same pattern used in
WorkflowDialog for resetCreate/resetUpdate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- yamlToFormState now rejects unknown trigger/action types with a clear
  error message instead of silently defaulting to "delay"/"message_posted"
- Add WorkflowDeleteDialog (AlertDialog pattern from PersonaDeleteDialog)
  showing workflow name in confirmation message
- Wire delete confirmation into WorkflowsView — delete now opens dialog
  first instead of immediately mutating

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add mock workflow handlers to e2eBridge.ts (in-memory CRUD for
  create, update, delete, trigger, get workflows/runs/approvals)
- Create workflows.spec.ts with 6 tests covering:
  - Empty state navigation
  - Create workflow via form builder
  - Edit existing workflow (name change)
  - Duplicate workflow
  - Delete with confirmation dialog
  - Trigger from detail panel
- Add workflows.spec.ts to smoke project in playwright.config.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When WorkflowFormBuilder mounts with a YAML string that fails to parse
(e.g. editing a workflow with unknown action types), it now starts in
YAML mode with the parse error visible instead of silently showing an
empty form.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…channel picker

- Fixed header/footer in workflow dialog with scrollable content area
- Added autoCapitalize="off" to all ID, snake_case, pubkey, UUID, URL,
  cron, interval, and emoji input fields
- Generate human-readable step IDs (step_1, step_2) instead of UUIDs
- Replaced native <select> with searchable channel combobox (popover +
  filtered list) for better UX with many channels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add min-h-0 to scrollable content area (required for flex overflow)
- Add autoCorrect="off" to workflow name field per spec
- Fix step ID generation to find highest existing step_N and increment
  from there, rather than starting from steps.length

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Parse YAML once on mount via useRef instead of three separate calls
- Extract nextStepId() helper from inline addStep callback
- Extract formatChannelLabel() to deduplicate channel display string
- Consolidate two identical channel info branches in WorkflowDialog
- Replace autoFocus with ref callback to satisfy biome a11y rule
- Inline highlight reset into onChange to avoid unnecessary useEffect

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-locates the step ID generator with other workflow form helpers
(formStateToYaml, yamlToFormState) where it logically belongs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@wesbillman
Copy link
Copy Markdown
Collaborator Author

@codex please review

wesbillman and others added 3 commits April 3, 2026 18:27
The "Create" button selector was matching both the stream creation
submit button and the new "Create workflow" button. Scope the selector
to the create-stream-form to avoid the collision.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Scopes all `getByRole("button", { name: "Create" })` selectors in
integration.spec.ts to the `create-stream-form` test ID to avoid
collision with the "Create workflow" button's aria-label.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Scope ambiguous getByRole("button", { name: "Create" }) selectors in
channels.spec.ts, stream.spec.ts, smoke.spec.ts to create-stream-form,
and workflows.spec.ts to the dialog element.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 89a751ff88

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

...actionFieldsForStep(step),
}));

return yamlStringify({ name: state.name, trigger, steps });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve top-level workflow flags in form serialization

When a workflow is edited in form mode, formStateToYaml rewrites the definition with only name, trigger, and steps, so top-level keys like enabled/description are dropped. In particular, workflows saved with enabled: false will be re-saved without that flag, and backend parsing defaults enabled to true, which can unintentionally reactivate workflows after a simple metadata edit. Please either round-trip unknown top-level fields or force YAML mode when fields outside the supported form schema are present.

Useful? React with 👍 / 👎.

Comment on lines +193 to +196
(step: Record<string, unknown>, index: number) => ({
id: (step.id as string) ?? `step_${index + 1}`,
action: (step.action as ActionType) ?? ACTION_TYPES[0],
duration: step.duration as string | undefined,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid stripping step guards when converting YAML to form

The YAML parser maps each step into StepFormState using only action-specific fields, so supported step metadata such as if, timeout_secs, and name is silently discarded. If a user edits any parsed workflow in form mode (for example, changing just the workflow name), those dropped fields are not re-emitted and execution semantics change (e.g., guarded steps can become unconditional). This should preserve unsupported step keys or reject form mode for definitions that include them.

Useful? React with 👍 / 👎.

@wesbillman wesbillman merged commit 5923a7b into main Apr 4, 2026
8 checks passed
@wesbillman wesbillman deleted the workflow-improvements branch April 4, 2026 01:41
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.

1 participant