Files
pad/web
xarmian cd5c5702d4 feat(web): give ItemPicker a source model; keep the Relationships tab on server FTS (TASK-2862)
Lead ruling on PR #1241, and the right call. The extraction had silently moved
the add-relationship search onto the warm local path, and `localIndex` strips
`content` by design — so a user who links an item by a phrase they remember
from its BODY lost that, with no signal anything had changed. Consistency with
the other pickers does not buy back a capability under CONVE-139.

`source` is now an explicit MODEL choice, not a performance one:

  'index' (default) — `localSearch` over title / ref / tags / parent / field
  values, no network call, server only as a cold fallback. Right for a RELATION
  field, where you are choosing a row from a known collection and know what it
  is called. U2 onward take this.

  'server' — always `/search`, whose FTS also indexes body content. Right for
  the Relationships tab, where you are finding an item you remember rather than
  one you can name, and what it did before this component existed. ItemDetail
  passes it.

An empty-query LISTING stays on the index for both: it is not a search,
`/search` cannot answer one (it requires a `q`), and the rows are local either
way. Only QUERIES follow `source`.

Two supporting changes fall out of it rather than being bolted on:

**`rawResults` + a derived `results`.** The exclusion filter is now part of the
derivation, so a late `excludeIds` — `ItemDetail` loads `itemLinks`
asynchronously — re-filters on its own. Without that, honouring a late
exclusion on the server-backed caller would have meant re-issuing the request,
which is the rate-limiter pressure the debounce exists to avoid. The refresh
effect no longer needs `excludeIds` as a dependency at all, and server-sourced
QUERIES are explicitly not re-run on an index delta.

**The highlight is an ID, not an index.** `activeId` is state; `activeIndex`
derives from it. Identity survives the list changing underneath — a delta, a
late exclusion — where an index silently moves the highlight onto whatever slid
into that position. This deletes the hand-rolled preserve/restore that lived in
the effect, so no future site that changes the list has to remember to do it.

Pins, per the ruling: the server caller queries `/search` with a hydrated index
and never touches `localSearch`; the control leg asserts the default source on
the same warm index never reaches the network; and a source-level test asserts
ItemDetail's call site still carries `source="server"` — a regression invisible
from the component's own tests, which is why it is asserted at the call site.

Verified in a real browser against a locally built binary with a marker string
present ONLY in an item's body and never in its title, so the local index
cannot answer it: the Relationships picker finds it, arrows to it, and creates
the link.

Mutation matrix 20 of 20 killed, including the three new ones — ignore `source`
(3 failed), re-query on a delta (1 failed), drop `source="server"` at the call
site (1 failed).

Gates: `npm run check` 1092 files 0 errors, 6 pre-existing warnings; full web
suite 121 files / 2041 tests green.
2026-09-03 16:16:11 +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
    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