mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 21:39:01 +00:00
1d3b1a5355
* 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.
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