Files
pad/web
xarmian 1d3b1a5355 feat(web): first-class playbook editor — slug validation, args builder, test invocation (TASK-1384) (#524)
* feat(web): first-class playbook editor — slug validation, args builder, test invocation (TASK-1384)

Builds the first-class playbook editing experience for PLAN-1377's
invocation model. New surface area:

- arguments.ts — shared parser/generator for the playbook body's
  ## Arguments section, plus the canonical invocation_slug regex,
  the skeleton-template body inserted on new, and a buildTestInvocation
  helper that produces the three command renderings (Claude Code, CLI,
  pad_playbook MCP JSON) from a slug + sample inputs. The structured
  arguments JSON field is canonical; the markdown section round-trips.

- PlaybookFormFields.svelte — reusable Svelte 5 component with the
  structured slug input (kebab-case validation + workspace-scoped
  uniqueness check, debounced 300ms), trigger selector with
  Other-(custom) escape hatch, scope + status selectors driven by the
  collection schema, an arguments builder (add/remove/edit each
  PlaybookArgument card, with type-specific options for enums), and
  the test-invocation helper. Args ↔ body section is two-way bound via
  signature-key tracking to avoid reactive loops.

- playbooks/[slug]/+page.svelte — dedicated edit page that loads an
  existing playbook, hosts the title input + Save/Cancel actions, and
  splits the layout (form fields | body textarea). Builds the canonical
  fields object on save: arguments stored as a JSON value, empty
  invocation_slug omitted entirely so the optional column stays clean.

- playbooks/+page.svelte (list) — pre-fills the new-form textarea with
  PLAYBOOK_SKELETON_BODY when opened, embeds PlaybookFormFields beside
  the body textarea so every new playbook gets the same affordances,
  and emits the canonical fields shape on create.

Acceptance:
- Creating from "+ New" shows the skeleton template
- Non-kebab-case slug → inline error
- Duplicate slug → debounced inline error
- Arguments builder mutations update the body's ## Arguments section
- Editing the markdown section reflects back in the structured form
- Test invocation shows /pad ship PLAN-609 stop-after-each merge-strategy=rebase

Parent: TASK-1384 / PLAN-1377.

* fix(web): collection guard + preserve custom triggers + carry duplicate args per Codex review (round 1)

Codex round 1 findings:

1. [P2] Edit page used cross-collection api.items.get; /playbooks/TASK-1 could
   load a task and Save would rewrite its fields as a playbook. Added a
   collection_slug guard — if the loaded item isn't a playbook, show a toast
   ('Not a playbook — TASK-1 lives in tasks') and refuse to render the editor.

2. [P2] Snap effects in the list page (newTrigger/newScope) were forcing
   the form's current value into the schema list. When a user typed a
   custom trigger via PlaybookFormFields' 'Other…' mode, the snap silently
   replaced it with the first schema option. Gated both snaps on
   !showNewForm so they only fire while the form is closed (initial
   schema-vs-default reconciliation), leaving user edits untouched.

3. [P3] duplicatePlaybook dropped the arguments contract. A copy of an
   argumented playbook silently lost its arg spec while the body still
   described them. Carry forward fields.arguments on duplicate.
   invocation_slug is intentionally still dropped — a duplicate would
   clash on the unique index — but arguments are non-unique and safe.

Parent: TASK-1384 / PLAN-1377.

* fix(web): hide create-form status selector overridden by submit buttons per Codex review (round 2)

Codex round 2 finding:

[P2] PlaybookFormFields.status was wired into the new-form but the
'Create as Draft' / 'Create as Active' submit buttons pass their status
literal directly to createPlaybook(status), silently overriding any
status the user selected (deprecated, especially).

Added a hideStatus prop to PlaybookFormFields, defaulted to false (edit
page keeps the selector). Pass hideStatus={true} from the create form
where the buttons already own status. Edit-page UX unchanged.

Parent: TASK-1384 / PLAN-1377.

* fix(web): preserve unknown fields on playbook save per Codex review (round 3)

Codex round 3 finding:

[P2] save() rebuilt the fields object from scratch (status/trigger/scope/
arguments/invocation_slug), so api.items.update — which replaces the
whole fields JSON blob — would silently drop any custom workspace
fields or future metadata the form doesn't render. Fixed by seeding
the saved fieldsObj from parseFields(item) so unknown keys survive
the round-trip. Empty invocation_slug now explicitly deletes the
key rather than persisting an empty string that would still hit the
unique index.

Parent: TASK-1384 / PLAN-1377.

* fix(web): clear stale item on load + coerce typed default values per Codex review (round 4)

Codex round 4 findings:

[P2] loadItem catch path left the previously-loaded playbook editable
when a re-fetch under a new slug failed. Cleared item = null at the
start of every load and on the error path so a 404 renders 'Playbook
not found' instead of letting the user edit the stale item.

[P2] PlaybookFormFields' Default input always stored the value as a
string; flag/number defaults like 'true' or '5' were serialized as the
strings '"true"' / '"5"' into fields.arguments. The server passes
defaults opaquely, so agents got the wrong types when binding. Added
coerceDefaultForType in arguments.ts (mirrors the markdown parser's
coerceDefaultValue rules) and applied it from argumentsToJSON before
serialization.

Parent: TASK-1384 / PLAN-1377.
2026-05-12 21:01:39 -04:00
..
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00

Pad Web UI

SvelteKit 2 + Svelte 5 frontend for Pad, compiled to static files and embedded into the Go binary.

Development

npm install
npm run dev          # Dev server at localhost:5173 (proxies API to localhost:7777)
npm run build        # Production build to build/
npm run check        # Type checking with svelte-check

When developing, run the Go backend separately with make dev from the project root.

Building for Production

Do not build in isolation. Always use make build from the project root — this builds the web frontend, then compiles the Go binary with the build output embedded via //go:embed.

Stack

  • Svelte 5 with runes ($state, $derived, $effect)
  • SvelteKit 2 with adapter-static (SPA mode)
  • Tiptap block editor with markdown round-trip
  • svelte-dnd-action for drag-and-drop in board/list views
  • SSE for real-time updates
  • TypeScript throughout

Structure

src/
  routes/                    SvelteKit pages
    +layout.svelte           App shell (sidebar + main)
    +page.svelte             Landing/redirect
    [workspace]/
      +page.svelte           Dashboard (collections, phases, activity)
      +layout.svelte         SSE connection per workspace
      [collection]/
        +page.svelte         Collection view (board/list)
      [collection]/[item]/
        +page.svelte         Item detail + editor
      conventions/            Purpose-built conventions page
      playbooks/              Purpose-built playbooks page
      settings/               Workspace settings
  lib/
    api/client.ts            HTTP API client
    components/
      layout/                Sidebar, navigation
      editor/                Tiptap editor, raw markdown editor
      fields/                FieldEditor, relation picker
      items/                 ItemCard, ItemDetail
      collections/           BoardView, ListView
      common/                StatusBadge, badges, modals
      search/                CommandPalette
      activity/              ActivityFeed
    stores/                  Svelte 5 reactive stores
      workspace.svelte.ts    Workspace state
      collections.svelte.ts  Collection + item state
      ui.svelte.ts           Sidebar, mobile state
    types/index.ts           TypeScript types and constants
  app.css                    Global styles and design tokens