xarmian 5f7b7af50f feat(web): surface required/default/suffix/relation field controls (TASK-596) (#135)
* feat(web): surface required/default/suffix/relation field controls

Closes TASK-596 in PLAN-593.

Expose field capabilities that already round-tripped through
EditableField but had no UI. Controls live in a collapsible Advanced
section on each field card.

FieldEditor
- New exported CollectionOption type for the relation picker input.
- Advanced section (collapsed by default, auto-expanded when any of
  required/default/suffix/collection is already set) containing:
  * Required checkbox (all types)
  * Default value — type-appropriate input:
      text/url   -> text input
      number     -> number input (+ Suffix row below it)
      date       -> date picker
      checkbox   -> "Checked by default" toggle
      select     -> dropdown restricted to the field's options
      multi_select / relation -> deliberately skipped
  * Relates to dropdown (relation type only), populated from the
    workspace collections list passed in via props. Shows a helpful
    empty-state when no other collections exist.
- Computed fields render a muted "computed" badge and the advanced
  inputs are disabled (changing defaults / suffix / required on a
  computed field is nonsensical). Label / type / remove remain
  editable to preserve current behavior.
- Typed input handlers coerce field.default into the right shape
  (string / number / boolean) so the polymorphic value stays clean.

CreateCollectionModal + EditCollectionModal
- Both fetch api.collections.list(ws) lazily on open and pass the
  result down to every FieldEditor as `collections`.
- Both new-field save paths now emit required / computed / suffix /
  collection / default onto the serialized FieldDef. Existing-field
  save path in EditCollectionModal already handled these; this brings
  the new-field path to parity and adds equivalent handling in the
  Create modal.

Behavior notes
- Values round-trip: set in Advanced -> save -> reopen -> still there.
- Emit-when-set keeps payloads compact and compatible with existing
  schemas that don't carry these fields.
- Known visual quirk: the taller card may exacerbate the type-select
  alignment already tracked on TASK-598; deferred to the visual pass.

* fix(web): gate advanced field properties by current field type

Codex P2 (PR #135): the save paths emitted `suffix`, `collection`,
and `default` for every new field regardless of f.type, so a user
could set a number default/suffix, switch the field to `relation` or
`multi_select`, and still persist the hidden value — producing schema
defaults that don't match the final type and are then auto-applied
to new items by ValidateFields.

Fix: gate type-specific advanced-value emission by the current type
at save time. This keeps the user's in-memory state intact (no
surprise clears on type toggle) but prevents stale values from
leaking into the saved schema.

- suffix: only when type === 'number'
- collection (relation target): only when type === 'relation'
- default: only when typeSupportsDefault(type) returns true

Apply the gating in all three save paths:
- CreateCollectionModal.handleCreate (new fields)
- EditCollectionModal.handleSave addedFields (new fields)
- EditCollectionModal.handleSave updatedExisting (existing fields) —
  same pre-existing risk if the user changes an existing field's
  type and hits save

Extract the default-support check into typeSupportsDefault() in
field-editor-types.ts so FieldEditor (which gates the rendered
default input) and the save paths share one predicate. Adjust
FieldEditor's local `supportsDefault` derived to call through it.

* fix(web): coerce and normalize default values at save time

Two related Codex findings on PR #135:

P1: Coerce default values to active field type before save
  Type-switch drift — user sets a boolean default on a checkbox, then
  switches the type to `text`, the stale boolean was previously
  serialized as the text default. ValidateFields later auto-applies
  it to new items without re-validating the value type.

P2: Trim select defaults to match normalized option values
  Option text is trimmed on save ("open " -> "open"), but the select
  default handler stored raw option text, producing schemas with
  `options:["open"]` + `default:"open "` — defaults that aren't in
  the allowed set and get auto-injected as invalid values.

Fix: add coerceDefault(raw, type, options?) to field-editor-types.ts.
Returns undefined when the raw value can't be represented in the
target type (caller drops it). Handles:

- text/url    -> must be a non-empty string
- number      -> number, or parseable non-empty numeric string
- date        -> non-empty string (server validates format)
- checkbox    -> must be boolean
- select      -> trimmed string that exists in normalized options

Wire through all three save paths:
- CreateCollectionModal.handleCreate (new fields)
- EditCollectionModal.handleSave addedFields (new fields)
- EditCollectionModal.handleSave updatedExisting (existing fields,
  where the same type-switch risk applies)

The select-options branch passes the already-normalized `def.options`
into coerceDefault so whitespace drift is caught in the same step as
type coercion.

* fix(web): tighten date coercion, preserve opaque defaults, stable keys

Three Codex findings on PR #135:

P1: Validate date defaults before persisting them
  coerceDefault was accepting any non-empty string for the date type,
  so switching a field from text/select to date could serialize stale
  garbage like "soon" as the date default even though the date input
  renders blank. Tighten the date branch to require ISO 8601 format
  (YYYY-MM-DD, optionally followed by a T-prefixed datetime tail).
  Server still performs stricter parsing; this guard blocks obvious
  invalid strings from leaking through.

P1: Preserve unsupported field defaults during edit saves
  The existing-fields save path dropped `default` whenever
  typeSupportsDefault(f.type) returned false. Opening and saving a
  collection that contained a multi_select or relation default (e.g.
  from an API import) would silently strip those defaults as a side
  effect of unrelated edits — schema-mutating regression.

  Fix: in the existing-fields branch, if the active type isn't UI-
  editable for defaults, pass field.default through verbatim instead
  of dropping it. Types that *are* UI-editable still run through
  coerceDefault. New-field paths are unchanged because new fields
  never carry a pre-existing opaque default.

P2: Use stable unique keys for select default options
  The default-value dropdown for select fields keyed its <option>s by
  text, but duplicate option labels aren't prevented anywhere in the
  editor or save path. A collection with duplicate options would hit
  Svelte's keyed-each duplicate-key behavior and break the control.
  Switch to keying by index for display stability.

* fix(web): clear stale relation options before async reload

Codex P2 (PR #135): loadCollectionOptions() awaited the fetch before
replacing collectionOptions, so a reopened modal — especially after
a workspace switch — briefly showed the previous workspace's
relation targets. A fast user could pick one and persist a slug that
doesn't exist in the current workspace.

Fix: clear collectionOptions = [] synchronously at the start of
loadCollectionOptions(), before awaiting the request. If the fetch
fails the picker falls back to its empty-state hint. Applied in both
CreateCollectionModal and EditCollectionModal.

* fix(web): token-guard collection fetch + checkbox default clear

Two Codex findings on PR #135:

P2: Ignore stale collection-list responses before setting options
  The previous fix cleared collectionOptions at fetch start but still
  unconditionally applied whichever response resolved last. Rapid
  reopens or slow networks could let an older response land after a
  newer one and overwrite it, letting a user persist a relation slug
  from the wrong workspace.

  Fix: add a monotonic collectionsRequestToken in both modals. Bump it
  on each fetch, capture the current value, and drop the response if
  the token has moved on when it resolves. Applied in both success
  and error paths.

P2: Allow clearing checkbox defaults instead of forcing false
  The checkbox default was tri-state at the schema level (no default
  / default false / default true) but the UI only toggled between
  true and false. Unchecking stored `false`, and there was no way to
  get back to `undefined` — so ValidateFields would auto-inject
  `false` into new items even when the user meant "no default".

  Fix: add an explicit "Clear" affordance next to the checkbox that
  shows only when field.default is set. Clears to undefined, leaving
  schema with no default for that field. Preserves the intentional
  `false` case (user wants new items to default to unchecked).

* fix(web): calendar-validate date defaults instead of regex shape only

Codex P2 (PR #135): the date branch of coerceDefault accepted any
string matching the YYYY-MM-DD shape, so impossible dates like
"2026-99-99" or "2026-01-32" could be persisted when users switched
a field from text/select to date. ValidateFields later auto-applies
these as defaults on new items without re-checking, propagating
invalid dates silently.

Replace the shape-only regex with real calendar validation:

- Plain date branch (YYYY-MM-DD): parse month/day, then round-trip
  through Date.UTC and verify the resulting year/month/day match
  the input. Rejects out-of-range components (month > 12) and
  overflow cases (day 32 rolling to next month).

- RFC3339 datetime branch: keep the shape check (stricter than a
  loose `T.+` suffix — rejects "2026-01-01Tnot-a-time"), then confirm
  Date.parse yields a finite timestamp.

Both branches return undefined on rejection so the caller drops the
default rather than persisting garbage.

* fix(web): strict datetime coercion + drop select defaults w/ empty opts

Two follow-up Codex findings on PR #135:

P1: Reject non-RFC3339 datetime defaults in coercion
  Previous fix did shape + Date.parse, but `new Date(...)` silently
  rolls calendar-invalid dates (e.g. "2026-02-31T10:00:00Z" becomes
  March 3) so impossible timestamps still passed. Switch the datetime
  branch to the same component-parse + round-trip technique as the
  YYYY-MM-DD branch:

  - Extract Y/M/D + h/m[/s] from the regex capture groups
  - Range-check each component (month 1–12, day 1–31, h ≤ 23, m/s ≤ 59)
  - Construct a UTC Date from Y/M/D and verify the resulting
    components match the input to catch day overflow

  Date.parse is no longer trusted alone. Out-of-range days,
  impossible calendar dates, and non-RFC3339 strings are all dropped.

P2: Drop select defaults when normalized options are empty
  The save paths passed `def.options` into coerceDefault, but
  `def.options` is omitted when the normalized list is empty, so a
  select field with no options would skip the membership check and
  keep a stale string default. ValidateFields would then auto-apply
  a default that doesn't exist in any allowed set.

  Fix: in all three save paths, pass the already-normalized opts
  array (including []) to coerceDefault when the type is select.
  Non-select types continue to pass undefined since they don't
  consult the options parameter.

  - CreateCollectionModal: use the local `opts` variable
  - EditCollectionModal addedFields: use the local `opts` variable
  - EditCollectionModal updatedExisting: extract a
    `normalizedOpts` local (options were previously inlined) and
    reuse it for both def.options and the coerceDefault call

* fix(web): drop stale default on type switch to multi_select/relation

Two Codex findings on PR #135:

P1: Drop stale default when existing field switches to relation/multi_select
  The existing-fields save path preserved f.default verbatim for every
  UI-unsupported type. That's correct when the field was loaded with a
  pre-existing opaque default (API/import). But it misfires when the
  user sets a default while the field is text/number/select and then
  switches the type to relation or multi_select — the default UI
  hides, but the stale value persists and gets saved.

  Fix: track the load-time type as `originalType` on EditableField and
  only fall through to the verbatim-preserve branch when the active
  type still matches the original. In-session type switches to a
  UI-unsupported type now drop the default instead. New-field paths
  don't need this because new fields never carry pre-existing
  opaque defaults.

P2: Enforce strict RFC3339 datetime shape in default coercion
  The previous datetime regex accepted optional timezone and
  offsets without the colon, so "2026-01-01T10:00" and
  "2026-01-01T10:00+0100" round-tripped as defaults even though the
  backend's time.RFC3339 parser requires seconds + a colon in the
  offset. That lets defaults survive here that the server rejects.

  Fix: require seconds, require timezone, require colon in offset.
  Matches strict RFC3339 / Go time.RFC3339.

* fix(web): validate RFC3339 timezone offsets in date coercion

Codex P2 (PR #135): the datetime regex enforced the `±hh:mm` shape
but never validated the numeric ranges of the offset, so values like
"2026-01-01T10:00:00+99:99" were treated as valid and serialized.
Go's time.RFC3339 (backend parser) rejects those, and defaults are
auto-applied to new items without re-validation, so an invalid
offset would silently propagate.

Add explicit offset bounds: hours 0–23, minutes 0–59 (matching Go's
time.RFC3339 acceptance of ±23:59). `Z` skips the check. Applied
after the regex match in the datetime branch.

* fix(web): raw string number default + defaults-equal type switch check

Two Codex findings on PR #135:

P2: Preserve raw number input until commit
  The number-default input called Number(v) on every oninput and
  wrote the coerced value back to field.default. Because the input
  was controlled by `value={defaultAsString}`, partial typing states
  like "1." collapsed to "1" on each keystroke (Number("1.") === 1),
  making it impossible to type decimals. Negative signs had the same
  problem.

  Fix: keep the raw string in field.default while editing.
  coerceDefault already handles string→number conversion at save
  time and drops garbage strings, so no save-path change is needed.

P2: Track any type switch before preserving hidden defaults
  The existing-fields unsupported-type fallback preserved f.default
  whenever the active type matched originalType. That missed the
  round-trip case: relation → text → relation with a new default
  injected in the middle. Type matches at save but the default is
  stale and un-editable through the UI.

  Fix: snapshot originalDefault at load alongside originalType, and
  only preserve the default when BOTH are unchanged. Otherwise drop.
  Add defaultsEqual() helper to field-editor-types.ts for
  polymorphic comparison (JSON-stringify-based — fine for schema
  defaults, which are always JSON primitives/arrays).

* fix(web): truncate datetime defaults to YYYY-MM-DD for date input binding

Codex P2 (PR #135): <input type="date"> only accepts a YYYY-MM-DD
value. An RFC3339 datetime default like "2026-01-01T10:00:00Z" was
bound directly via defaultAsString and rendered blank, leading users
to believe the field had no default — while field.default remained
populated and was preserved on save through coerceDefault. Result:
hidden datetime defaults that silently survived unrelated edits.

Fix: derive a display-only dateDefaultDisplay string that truncates
anything after the YYYY-MM-DD prefix, and bind the date input to
that. field.default itself stays untouched until the user actually
picks a new date, at which point onDefaultDateInput writes the pure
YYYY-MM-DD value. This keeps API-loaded datetime defaults round-
tripping untouched (when the user doesn't edit them) while making
them visible for manual correction.
2026-04-17 15:36:24 -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

Pad

Project management for developers and AI agents.

CI Release License


One binary. Local-first. No accounts. Pad gives you a CLI, a web UI, and an AI agent skill — all backed by SQLite, all running on your machine. Your project data never leaves your laptop.

Quick Start

brew install xarmian/tap/pad
cd your-project
pad auth configure
pad workspace init
pad server open

For a local install, choose Local in pad auth configure. Pad will remember that this client manages a local server, auto-start it when needed, and open the web UI at localhost:7777.

Why Pad?

Tools like Linear, Jira, and Notion are built for teams on the cloud. Pad is built for developers on their machine — and for the AI agents working alongside them.

Pad Linear / Jira Notion
Setup pad auth configure + pad workspace init Create account, invite team, configure Create account, pick template
AI agents Native /pad skill for 7+ tools Third-party integrations Third-party integrations
Data Local SQLite, you own it Their cloud Their cloud
Offline Full functionality Read-only cache at best Limited
CLI First-class Afterthought None
Price Free, open source Per-seat pricing Per-seat pricing

Features

For Developers

CLI that doesn't get in your way. Create tasks, search items, check status — without leaving the terminal.

pad item create task "Fix OAuth redirect" --priority high
pad item create idea "Real-time collaboration" --category infrastructure
pad item list tasks --status in-progress
pad item search "authentication"
pad project dashboard                   # Project dashboard
pad project next                        # What should I work on?
pad server info                         # How this client is connected to Pad

Web UI that stays out of your way. A clean, dark-themed interface at localhost:7777 with:

  • Board, list, and table views — drag-and-drop between status columns
  • Keyboard navigationj/k to move, Enter to open, Esc to go back, Cmd+K to search
  • Rich text editor — Tiptap-based with markdown, formatting toolbar, and auto-save
  • Wiki-links — type [[Title]] to link between items
  • Real-time updates — agent creates a task in the terminal, it appears in the browser instantly (via SSE)
  • Dashboard — collection overview, active work, plan tracking, activity feed

For AI Agents

Your agent becomes a project partner. Install the /pad skill once, and your AI coding tool can read, create, and update project items through natural language.

pad agent install        # Auto-detects your tools and installs the skill

Works with Claude Code, Cursor, Windsurf, Codex, GitHub Copilot, Amazon Q, and JetBrains Junie.

Then just talk to your project:

> /pad what should I work on next?
> /pad I finished the OAuth fix
> /pad create a task to add rate limiting
> /pad let's brainstorm about the API redesign

Conventions and playbooks teach agents how your project works:

  • Conventions — trigger-based rules like "run tests before marking a task done" or "use conventional commits"
  • Playbooks — multi-step workflows like "when implementing a feature: read the spec, create a branch, write tests first, then implement"
pad item create convention "Run tests before completing tasks" \
  --field trigger=on-task-complete \
  --field scope=all \
  --field priority=must

Agents load relevant conventions automatically. All agent actions are attributed in the activity feed, so you always know what the AI changed.

Onboard agents to a new codebase:

pad workspace onboard    # Analyzes project structure, saves workspace context, and suggests conventions

Collections & Custom Fields

Pad organizes work into collections — typed containers with structured fields.

Built-in collections:

Collection Purpose
Tasks Work items with status, priority, assignee, effort, due date
Ideas Feature ideas with impact and category
Plans Project milestones with progress tracking
Docs Documentation, decisions, reference material
Conventions Project rules that guide agent behavior
Playbooks Multi-step workflows for agents to follow

Create your own with typed fields — select, text, date, number, url, relation, checkbox:

pad collection create "Bug Reports" \
  --fields "severity:select:low,medium,high,critical; browser:text; reproducible:checkbox"

Items get reference numbers automatically (TASK-5, BUG-12) and can be moved between collections with field migration.

Installation

Homebrew (macOS and Linux)

brew install xarmian/tap/pad

Build from Source

git clone https://github.com/xarmian/pad
cd pad
make build
cp pad ~/.local/bin/   # or /usr/local/bin/

Requires Go 1.25+ and Node.js 22+.

The go install github.com/xarmian/pad/cmd/pad@latest path is not supported for the full Pad binary, because the web UI must be built and embedded during the source build.

Docker

docker run -p 127.0.0.1:7777:7777 -v pad-data:/data ghcr.io/xarmian/pad

This publishes Pad to localhost:7777 on the host machine, which is the recommended default for local use.

To expose Pad beyond localhost intentionally, publish the port more broadly:

docker run -p 7777:7777 -v pad-data:/data ghcr.io/xarmian/pad

Use broader publishing only when you intend to make Pad reachable from other machines or interfaces.

Binary Download

Pre-built binaries for macOS, Linux, and Windows are available on the releases page.

Getting Started

1. Configure this Pad client

pad auth configure

For most local installs, choose Local. If you're connecting to another Pad server, choose Remote or Docker and enter its base URL.

2. Initialize a workspace

cd ~/projects/myapp
pad workspace init "My App"

This creates a .pad.toml file linking your project directory to a Pad workspace with default collections. Choose a template to start with pre-configured collections:

pad workspace init "My App" --template scrum     # Scrum-style with sprints
pad workspace init "My App" --template product   # Product management focused

3. Install the AI skill

pad agent install            # Auto-detect and install for all found tools
pad agent install claude     # Or install for a specific tool
pad agent install cursor
pad agent install copilot

4. Start working

# From the CLI
pad item create task "Set up CI pipeline" --priority high
pad item create idea "Add WebSocket support" --category infrastructure
pad project dashboard

# From the web UI
pad server open              # Opens localhost:7777 in your browser

# From your AI agent
# Just use /pad in Claude Code, Cursor, etc.

5. Teach your agents the rules

pad workspace onboard        # Auto-analyze project, save workspace context, and suggest conventions
# Or browse the convention library
pad library list --type conventions  # Pre-built conventions you can adopt
pad library list --type playbooks    # Pre-built multi-step workflows

CLI Reference

pad auth configure                    Configure how this client connects to Pad
pad auth setup                        Initialize the first admin account
pad auth login                        Sign in
pad auth whoami                       Show current user

pad server start                      Start the Pad API server
pad server stop                       Stop the Pad server
pad server info                       Show client, connection, and local server status
pad server open                       Open web UI in browser

pad workspace init [name]             Initialize workspace in current directory
pad workspace link <workspace>        Link current directory to an existing workspace
pad workspace list                    List all workspaces
pad workspace switch <workspace>      Switch active workspace
pad workspace context                 Show structured workspace context
pad workspace context set --file X    Update structured workspace context from JSON
pad workspace onboard                 Analyze project, save workspace context, and suggest conventions
pad workspace members                 List workspace members
pad workspace invite <email>          Invite a workspace member
pad workspace join <code>             Accept an invitation
pad workspace export                  Export workspace data
pad workspace import <file>           Import workspace data

pad project dashboard                 Project dashboard
pad project next                      Recommended next task
pad project ready                     Query actionable next items
pad project stale                     Query stalled or attention-worthy items
pad project standup [--days N]        Daily standup report
pad project changelog [--days N]      Release notes from completed items
pad project watch                     Real-time activity stream
pad project reconcile                 Reconcile item and PR state

pad item create <coll> "title"        Create item (task, idea, plan, doc, ...)
pad item list [collection]            List items (filters: --status, --priority, --all)
pad item show <ref>                   Show item detail
pad item update <ref>                 Update item fields
pad item delete <ref>                 Delete item
pad item move <ref> <collection>      Move item between collections
pad item edit <ref>                   Open item in $EDITOR
pad item search "query"               Full-text search across all items
pad item comment <ref> "text"         Add comment to an item
pad item comments <ref>               View item comments
pad item note <ref> "summary"         Append an implementation note to an item
pad item decide <ref> "decision"      Append a decision log entry to an item
pad item block <src> <target>         Create dependency
pad item blocked-by <item> <blk>      Mark item as blocked
pad item deps <ref>                   Show dependencies
pad item unblock <src> <target>       Remove dependency
pad item related <ref>                Show direct relationships for an item
pad item implemented-by <ref>         Show incoming implementers for an item
pad item bulk-update --status X       Batch update multiple items

pad collection list                   List collections with item counts
pad collection create <name>          Create a custom collection

pad library list                      Browse convention and playbook library
pad library activate <title>          Activate a convention or playbook

pad agent install [tool]              Install /pad skill for AI coding tools
pad agent status                      Show supported tools and installation status
pad agent update                      Update installed tool integrations

pad github link [item-ref]   Link current branch's PR to item
pad github status [item-ref] Show PR status for linked items
pad github unlink <item-ref> Remove PR link from item

pad webhook list             List workspace webhooks
pad webhook create <url>     Create webhook

All commands accept --format json for machine-readable output and --workspace to target a specific workspace.

Authentication

Pad runs without authentication by default for frictionless local use. On a fresh instance, run pad auth setup on the server host to create the first admin account:

pad auth setup         # Initialize the first admin account
pad auth login         # Sign in
pad auth whoami        # Show current user
pad auth logout        # Sign out

Once a user exists, all API requests and web UI access require authentication. Credentials are stored in ~/.pad/credentials.json. Multiple users can be invited to workspaces with role-based access control (owner, editor, viewer).

pad workspace members               # List workspace members
pad workspace invite user@example.com
pad workspace join <code>

Architecture

┌──────────────────────────────────────────────┐
│              pad (single binary)              │
│                                               │
│  ┌──────────┐  ┌──────────┐  ┌────────────┐  │
│  │   CLI    │  │  REST    │  │  Embedded  │  │
│  │ (Cobra)  │  │  API     │  │  Web UI    │  │
│  └────┬─────┘  └────┬─────┘  │ (SvelteKit)│  │
│       │    HTTP      │        └────────────┘  │
│       └──────────────┤                        │
│                ┌─────▼─────┐                  │
│                │  SQLite   │                  │
│                │  + FTS5   │                  │
│                └───────────┘                  │
└───────────────────────────────────────────────┘
  • Go backend — chi router, SQLite via modernc.org/sqlite (pure Go, no CGO), FTS5 full-text search, SSE for real-time updates
  • SvelteKit frontend — Svelte 5, Tiptap editor, drag-and-drop, adapter-static, embedded via go:embed
  • Single binary — serves the API and web UI, runs on macOS, Linux, and Windows
  • Workspace-per-project — each project gets its own workspace linked by a .pad.toml file

All data lives in ~/.pad/pad.db. Your data. Your machine. No telemetry, no cloud, no accounts.

Contributing

See CONTRIBUTING.md for the development guide.

make build      # Build web UI + Go binary
make test       # Run Go tests
make dev-web    # SvelteKit dev server with hot reload
make install    # Build, install to ~/.local/bin, restart server

Security

See SECURITY.md for reporting vulnerabilities.

License

Apache License 2.0

Languages
Go 65.1%
TypeScript 21.3%
Svelte 13%
Shell 0.3%
CSS 0.1%