beads-ui 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGES.md +14 -0
  2. package/README.md +4 -4
  3. package/app/data/list-selectors.js +103 -0
  4. package/app/data/providers.js +7 -138
  5. package/app/data/sort.js +47 -0
  6. package/app/data/subscription-issue-store.js +161 -0
  7. package/app/data/subscription-issue-stores.js +128 -0
  8. package/app/data/subscriptions-store.js +227 -0
  9. package/app/main.js +346 -66
  10. package/app/protocol.js +23 -17
  11. package/app/protocol.md +18 -15
  12. package/app/router.js +3 -0
  13. package/app/state.js +2 -0
  14. package/app/styles.css +222 -197
  15. package/app/utils/issue-id-renderer.js +2 -1
  16. package/app/utils/issue-id.js +1 -0
  17. package/app/utils/issue-type.js +2 -0
  18. package/app/utils/issue-url.js +1 -0
  19. package/app/utils/markdown.js +13 -198
  20. package/app/utils/priority-badge.js +1 -2
  21. package/app/utils/status-badge.js +1 -1
  22. package/app/utils/status.js +2 -0
  23. package/app/utils/toast.js +1 -1
  24. package/app/utils/type-badge.js +1 -3
  25. package/app/views/board.js +172 -148
  26. package/app/views/detail.js +79 -66
  27. package/app/views/epics.js +127 -74
  28. package/app/views/issue-dialog.js +9 -15
  29. package/app/views/issue-row.js +2 -3
  30. package/app/views/list.js +105 -104
  31. package/app/views/nav.js +1 -0
  32. package/app/views/new-issue-dialog.js +30 -34
  33. package/app/ws.js +10 -10
  34. package/bin/bdui.js +1 -1
  35. package/docs/adr/001-push-only-lists.md +134 -0
  36. package/docs/adr/002-per-subscription-stores-and-full-issue-push.md +200 -0
  37. package/docs/architecture.md +34 -84
  38. package/docs/data-exchange-subscription-plan.md +198 -0
  39. package/docs/db-watching.md +2 -1
  40. package/docs/migration-v2.md +54 -0
  41. package/docs/protocol/issues-push-v2.md +179 -0
  42. package/docs/subscription-issue-store.md +112 -0
  43. package/package.json +5 -4
  44. package/server/app.js +2 -0
  45. package/server/bd.js +4 -2
  46. package/server/cli/commands.js +5 -2
  47. package/server/cli/daemon.js +19 -5
  48. package/server/cli/index.js +2 -2
  49. package/server/cli/open.js +3 -0
  50. package/server/cli/usage.js +2 -1
  51. package/server/config.js +13 -6
  52. package/server/db.js +3 -1
  53. package/server/index.js +9 -5
  54. package/server/list-adapters.js +224 -0
  55. package/server/subscriptions.js +289 -0
  56. package/server/validators.js +113 -0
  57. package/server/watcher.js +8 -8
  58. package/server/ws.js +457 -229
@@ -0,0 +1,134 @@
1
+ # ADR 001 — Push‑Only Lists (v2)
2
+
3
+ ```
4
+ Date: 2025-10-26
5
+ Status: Accepted (data‑flow details superseded by ADR 002)
6
+ Owner: agent
7
+ ```
8
+
9
+ ## Context
10
+
11
+ The UI currently mixes push updates with read RPCs like `list-issues` and
12
+ `epic-status`. This ADR establishes the push‑only direction for list data and
13
+ removing read RPCs in list views. It predated ADR 002 which later simplified the
14
+ data flow further (per‑subscription stores + full‑issue payloads).
15
+
16
+ - Push streams provide everything lists need to render. See
17
+ `docs/protocol/issues-push-v2.md` and ADR 002. Earlier iterations used a
18
+ central `issues` entity cache plus `list-delta` membership; this has been
19
+ replaced by per‑subscription stores receiving full issue payloads.
20
+
21
+ We want every list‑shaped view (Issues, Board, Epics → children) to render
22
+ exclusively from local push data. Reads remain only for mutations that return a
23
+ single updated entity (e.g. detail view refresh).
24
+
25
+ Related docs:
26
+
27
+ - Protocol: `docs/protocol/issues-push-v2.md`
28
+ - Server plan: `docs/data-exchange-subscription-plan.md`
29
+
30
+ ## Decision
31
+
32
+ - One active subscription per visible list. Examples (client ids):
33
+ - Issues tab: `tab:issues` with spec from filters via `computeIssuesSpec()`
34
+ - Board: `tab:board:ready|in-progress|closed|blocked`
35
+ - Epics list: `tab:epics` (for epic entities); children subscribe on expand as
36
+ `detail:{id}` with `{ type: 'issue-detail', params: { id } }`
37
+ - Rendering reads from two local stores only:
38
+ - `per‑subscription stores`: one store per active client subscription id.
39
+ Stores receive versioned `snapshot`/`upsert`/`delete` push envelopes with
40
+ full issue payloads and expose deterministic, sorted snapshots for the
41
+ owning view.
42
+ - `subscriptions`: manages subscription lifecycle and keys. Rendering reads
43
+ from per‑subscription stores, not from membership ids.
44
+ - Introduce a small selectors utility to apply view‑specific sort rules on store
45
+ snapshots (no composition from a central cache).
46
+ - Remove read RPCs used for lists: `list-issues`, `epic-status`. Keep mutation
47
+ RPCs and `show-issue` until detail view also reads from push cache.
48
+ - Tests drive views with push envelopes and `list-delta`; no RPC stubs for
49
+ reads.
50
+
51
+ ## API Shape (Client)
52
+
53
+ Subscriptions store (already implemented):
54
+
55
+ ```js
56
+ // app/data/subscriptions-store.js
57
+ createSubscriptionStore(send) -> {
58
+ wireEvents(on), subscribeList(client_id, spec) -> unsubscribe,
59
+ selectors: { getIds(client_id), has(client_id), count(client_id) }
60
+ }
61
+ ```
62
+
63
+ Selectors utility (implemented):
64
+
65
+ ```js
66
+ // app/data/list-selectors.js
67
+ /** Compose from per‑subscription store snapshots and apply stable sort. */
68
+ export function createListSelectors(issueStores) {
69
+ return {
70
+ selectIssuesFor(client_id) {},
71
+ selectBoardColumn(client_id, mode) {},
72
+ selectEpicChildren(epic_id) {},
73
+ subscribe(fn) {}
74
+ };
75
+ }
76
+ ```
77
+
78
+ Sorting rules:
79
+
80
+ - Issues list: priority asc (0..4), then `created_at` desc, then id asc.
81
+ - Board columns: preserve existing view rules (ready → priority asc, then
82
+ `updated_at` desc; in‑progress → `updated_at` desc; closed → `closed_at`
83
+ desc).
84
+ - Epics children: same as Issues list unless view specifies otherwise.
85
+
86
+ ## Consequences
87
+
88
+ Pros:
89
+
90
+ - Consistent, snappy UI with minimal fetch logic; views are pure derives.
91
+ - Server can batch and coalesce; client renders at most once per envelope.
92
+ - Clear separation: mutations via RPC, reads via push caches.
93
+
94
+ Cons / Risks:
95
+
96
+ - Initial implementation work in views and tests.
97
+ - Need disciplined subscription lifecycle on route/tab changes.
98
+ - Requires follow‑up to migrate detail view fully to the push cache.
99
+
100
+ ## Migration Checklist
101
+
102
+ Views
103
+
104
+ - [x] Issues view renders from per‑subscription stores; no `list-issues`.
105
+ - [x] Board renders from per‑subscription stores; no `get*` list reads.
106
+ - [x] Epics list/children render from per‑subscription stores; children use
107
+ `issue-detail` for the epic id; children come from `dependents`.
108
+
109
+ Client Data Layer
110
+
111
+ - [x] Add `app/data/list-selectors.js` with helpers listed above (UI-156).
112
+ - [x] Remove list read functions from `app/data/providers.js` (UI-159).
113
+ - [ ] Keep `getIssue` and all mutation helpers until detail view push migration
114
+ happens (follow‑up).
115
+
116
+ Tests
117
+
118
+ - [x] Update list/board/epics tests to use per‑subscription push envelopes
119
+ (UI-158).
120
+ - [x] Remove RPC read stubs from tests.
121
+
122
+ Docs
123
+
124
+ - [x] This ADR committed (UI-152).
125
+ - [x] Update protocol and architecture docs for push‑only model (UI-160).
126
+
127
+ ## Notes
128
+
129
+ - Client ids used in this repo today:
130
+ - `tab:issues` for the Issues view
131
+ - `tab:board:ready|in-progress|closed|blocked` for Board columns
132
+ - `tab:epics` for the Epics tab; `epic:${id}` for expanded children
133
+ - See `app/main.js` for current subscription wiring, filter → spec mapping, and
134
+ per‑subscription push routing.
@@ -0,0 +1,200 @@
1
+ # ADR 002 — Per‑Subscription Stores and Full‑Issue Push (Breaking)
2
+
3
+ ```
4
+ Date: 2025-10-26
5
+ Status: Proposed (ready for owner approval)
6
+ Owner: agent
7
+ ```
8
+
9
+ ## Context
10
+
11
+ The UI currently maintains a central `issues` cache and a separate list
12
+ membership model. Push events update the central cache and lists fan out from
13
+ it. This split increases cognitive load (two caches, two sets of selectors),
14
+ creates subtle ordering/dedup bugs, and complicates tests and routing.
15
+
16
+ We want a simpler, local model per visible list: one subscription → one store →
17
+ one push update stream → one rendered list. Push events must contain complete
18
+ issue objects for correctness and to avoid fan‑out to a central cache.
19
+
20
+ ## Decision
21
+
22
+ - Adopt a per‑subscription issue store (`SubscriptionIssueStore`) keyed by the
23
+ client’s subscription id.
24
+ - Server sends per‑subscription full‑issue payloads only; no id‑only deltas.
25
+ Messages are serialized per subscription and revisioned.
26
+ - Lists render exclusively from their own store snapshots; the central issue
27
+ cache is removed from the list render path.
28
+ - Breaking change: remove legacy id‑only list deltas and any compatibility
29
+ paths/flags. No phased rollout and no telemetry collection.
30
+
31
+ ## Protocol (Server → Client)
32
+
33
+ Message shapes are defined in `types/subscriptions.ts` and documented in
34
+ `docs/data-exchange-subscription-plan.md`.
35
+
36
+ All envelopes include a version tag and a per‑subscription, strictly monotonic
37
+ `revision` used for ordering and replay protection (see UI‑144).
38
+
39
+ - `subscribed` `{ id: string }`
40
+ - `snapshot` `{ id: string, revision: number, issues: Issue[] }`
41
+ - `upsert` `{ id: string, revision: number, issue: Issue }`
42
+ - `delete` `{ id: string, revision: number, issue_id: string }`
43
+ - `error` `{ id?: string, code: string, message: string, details?: object }`
44
+
45
+ Notes
46
+
47
+ - Per‑subscription ordering is guaranteed by the server and signaled via
48
+ `revision`. Clients MUST apply envelopes in `revision` order and ignore any
49
+ envelope whose `revision` is ≤ the last applied.
50
+ - Clients MUST treat updates as idempotent and MAY additionally guard on an
51
+ `issue.updated_at` timestamp to ignore stale `upsert`s that race with local
52
+ state. Timestamps are advisory; `revision` is canonical for ordering.
53
+ - Initial state arrives as a `snapshot` with a complete list of issues for the
54
+ subscription key.
55
+
56
+ ## Client Store API
57
+
58
+ The UI manages one store per active subscription. Minimal API surface:
59
+
60
+ ```ts
61
+ // types only — see types/subscription-issue-store.ts
62
+ export interface SubscriptionIssueStore {
63
+ /** Client subscription id this store belongs to. */
64
+ readonly id: string;
65
+
66
+ /** Attach a listener that is called after each applied message. */
67
+ subscribe(listener: () => void): () => void;
68
+
69
+ /** Apply a push message: snapshot, upsert, or delete. */
70
+ applyPush(msg: SnapshotMsg | UpsertMsg | DeleteMsg): void;
71
+
72
+ /** Read-only, stable snapshot for rendering (deterministic sort). */
73
+ snapshot(): readonly Issue[];
74
+
75
+ /** Lookup helpers used by views/tests. */
76
+ size(): number;
77
+ getById(id: string): Issue | undefined;
78
+
79
+ /** Release references and listeners when the view unmounts. */
80
+ dispose(): void;
81
+ }
82
+
83
+ export type SnapshotMsg = {
84
+ type: 'snapshot';
85
+ id: string;
86
+ revision: number;
87
+ issues: Issue[];
88
+ };
89
+
90
+ export type UpsertMsg = {
91
+ type: 'upsert';
92
+ id: string;
93
+ revision: number;
94
+ issue: Issue;
95
+ };
96
+
97
+ export type DeleteMsg = {
98
+ type: 'delete';
99
+ id: string;
100
+ revision: number;
101
+ issue_id: string;
102
+ };
103
+ ```
104
+
105
+ ### Sorting and identity
106
+
107
+ - Stores maintain stable item identity across updates (same object ref for the
108
+ same `id` when only fields change) and expose a deterministic sort order
109
+ suitable for the owning view (e.g., Issues: priority asc, then `created_at`
110
+ desc, then id asc).
111
+
112
+ ### Error handling and reconnect
113
+
114
+ - On disconnect/reconnect, the client creates a fresh store and re‑subscribes.
115
+ The server sends a fresh snapshot; no attempt is made to diff across sessions.
116
+
117
+ ### Reconcile algorithm (pseudo‑code)
118
+
119
+ ```
120
+ state: Map<string, Issue> = new Map()
121
+ lastRevision: number = 0
122
+
123
+ function applyPush(msg) {
124
+ if (msg.revision <= lastRevision) return // stale or duplicate
125
+ lastRevision = msg.revision
126
+
127
+ switch (msg.type) {
128
+ case 'snapshot':
129
+ state.clear()
130
+ for (const it of deterministicallySort(msg.issues)) {
131
+ state.set(it.id, it)
132
+ }
133
+ break
134
+ case 'upsert':
135
+ const existing = state.get(msg.issue.id)
136
+ if (!existing || existing.updated_at <= msg.issue.updated_at) {
137
+ state.set(msg.issue.id, msg.issue)
138
+ }
139
+ break
140
+ case 'delete':
141
+ state.delete(msg.issue_id)
142
+ break
143
+ }
144
+ notifyListeners()
145
+ }
146
+ ```
147
+
148
+ The sort function must be deterministic and view‑specific (e.g., priority asc,
149
+ then `created_at` desc, then `id` asc). Stores keep object identity stable for
150
+ the same `id` whenever fields change.
151
+
152
+ ## Migration
153
+
154
+ - Delete list render paths that read via the central issues cache.
155
+ - Introduce a factory `createSubscriptionIssueStore(id)` at view mount; wire the
156
+ push client to route `snapshot`/`upsert`/`delete` messages by `id` to the
157
+ corresponding store via `applyPush`.
158
+ - Update list components to render from `store.snapshot()` and subscribe to
159
+ re‑render on changes.
160
+ - Remove legacy central‑store fan‑out and dead selectors.
161
+
162
+ ## Consequences
163
+
164
+ Pros
165
+
166
+ - One‑to‑one mapping of subscription → store → view simplifies reasoning and
167
+ testing.
168
+ - No cache fan‑out; updates apply once per subscription and render once.
169
+ - Clearer ownership boundaries; easier disposal on route/tab changes.
170
+
171
+ Cons / Risks
172
+
173
+ - Larger `updated` payloads vs id‑only membership deltas. Mitigated by
174
+ per‑subscription scoping and batching.
175
+ - Requires coordinated server/client cutover due to the breaking change.
176
+
177
+ ## Alternatives Considered
178
+
179
+ - Keep the central cache and fan‑out membership to lists. Rejected: duplicates
180
+ ownership, increases complexity and test surface, caused known ordering bugs.
181
+ - Maintain id‑only list deltas with separate issue fetches. Rejected: adds
182
+ round‑trips and cross‑store coordination; does not meet simplicity goal.
183
+ - Dual protocol/feature flag with gradual cutover. Rejected per epic scope: no
184
+ flags, no compatibility layer, and no telemetry collection.
185
+
186
+ ## Related
187
+
188
+ - ADR 001 — Push‑Only Lists (v2): establishes push‑only direction and server
189
+ batching; this ADR replaces the central‑store + list‑membership split with a
190
+ per‑subscription store model.
191
+ - `docs/protocol/issues-push-v2.md` and
192
+ `docs/data-exchange-subscription-plan.md` for normative protocol and server
193
+ behavior.
194
+
195
+ ## Status & Follow‑ups
196
+
197
+ - This ADR is a breaking change with no flags/compat/telemetry. It becomes
198
+ Accepted once UI‑166 is approved by frontend and backend owners and the
199
+ server/client work (UI‑167, UI‑168, UI‑169) lands.
200
+ - Cleanup (UI-174) removes the central store and delta fan‑out code.
@@ -1,7 +1,13 @@
1
- # beads-ui Architecture and Protocol (v1)
1
+ # beads-ui Architecture (v2)
2
2
 
3
- This document describes the high‑level architecture of beads‑ui and the v1
4
- WebSocket protocol used between the browser SPA and the local Node.js server.
3
+ Note (2025-10-26)
4
+
5
+ - beads-ui has migrated to a push‑only protocol for lists and details. The
6
+ server no longer implements legacy read RPCs `list-issues` and `epic-status`.
7
+ For the normative protocol reference, see `docs/protocol/issues-push-v2.md`.
8
+
9
+ This document describes the high‑level architecture of beads‑ui and the v2
10
+ push‑only data flow used between the browser SPA and the local Node.js server.
5
11
 
6
12
  ## Overview
7
13
 
@@ -45,7 +51,7 @@ WebSocket protocol used between the browser SPA and the local Node.js server.
45
51
  - bd bridge: `server/bd.js` (spawn `bd`, inject `--db` consistently, JSON
46
52
  helpers)
47
53
  - DB resolution/watch: `server/db.js` (resolve active DB path),
48
- `server/watcher.js` (emit `issues-changed`)
54
+ `server/watcher.js` (schedule list refresh)
49
55
  - Config: `server/config.js` (bind to `127.0.0.1`, default port 3000)
50
56
 
51
57
  ## Data Flow
@@ -55,95 +61,44 @@ WebSocket protocol used between the browser SPA and the local Node.js server.
55
61
  2. Server validates and maps the request to a `bd` command (no shell; args array
56
62
  only).
57
63
  3. Server replies with `{ id, ok, type, payload }` or `{ id, ok:false, error }`.
58
- 4. Independent of requests, the DB watcher sends `issues-changed` events to all
59
- clients.
64
+ 4. Independent of requests, the DB watcher schedules a refresh for active list
65
+ subscriptions; clients receive `snapshot`/`upsert`/`delete` envelopes.
60
66
 
61
- ## Protocol (v1.0.0)
67
+ ## Protocol (v2.0.0)
62
68
 
63
- Envelope shapes (see `app/protocol.js` for the source of truth):
69
+ Envelope shapes (see `app/protocol.md` and `docs/protocol/issues-push-v2.md`):
64
70
 
65
- - Request: `{ id: string, type: MessageType, payload?: any }`
71
+ - Request: `{ id: string, type: string, payload?: any }`
66
72
  - Reply:
67
- `{ id: string, ok: boolean, type: MessageType, payload?: any, error?: { code, message, details? } }`
73
+ `{ id: string, ok: boolean, type: string, payload?: any, error?: { code: string, message: string, details?: any } }`
74
+
75
+ Push‑only subscriptions for lists and details:
76
+
77
+ - Client subscribes via `subscribe-list` with `{ id, type, params? }`.
78
+ - Server acks the subscription and immediately publishes a `snapshot` envelope
79
+ with the full list for that subscription id followed by `upsert`/`delete`
80
+ envelopes as data changes. Clients render from local per‑subscription stores.
68
81
 
69
- Message types implemented by the server today:
82
+ Common message types (mutations only; list reads removed):
70
83
 
71
- - `list-issues` payload: `{ filters?: { status?: string, priority?: number } }`
72
- - `show-issue` payload: `{ id: string }`
73
84
  - `update-status` payload:
74
85
  `{ id: string, status: 'open'|'in_progress'|'closed' }`
75
86
  - `edit-text` payload:
76
- `{ id: string, field: 'title'|'description'|'acceptance', value: string }`
87
+ `{ id: string, field: 'title'|'description'|'acceptance'|'notes'|'design', value: string }`
77
88
  - `update-priority` payload: `{ id: string, priority: 0|1|2|3|4 }`
78
89
  - `dep-add` payload: `{ a: string, b: string, view_id?: string }`
79
90
  - `dep-remove` payload: `{ a: string, b: string, view_id?: string }`
80
- - `issues-changed` (server push) payload:
81
- `{ ts: number, hint?: { ids?: string[] } }`
82
-
83
- Defined in the spec but not yet handled on the server:
84
-
85
- - `create-issue`, `list-ready`, `subscribe-updates` (client sends on connect;
86
- ignored safely)
87
-
88
- ### Examples
91
+ - `create-issue` payload:
92
+ `{ title: string, type?: 'bug'|'feature'|'task'|'epic'|'chore', priority?: 0|1|2|3|4, description?: string }`
89
93
 
90
- List issues
94
+ Removed in v2:
91
95
 
92
- ```json
93
- {
94
- "id": "r1",
95
- "type": "list-issues",
96
- "payload": { "filters": { "status": "open" } }
97
- }
98
- ```
99
-
100
- Reply
101
-
102
- ```json
103
- {
104
- "id": "r1",
105
- "ok": true,
106
- "type": "list-issues",
107
- "payload": [{ "id": "UI-1", "title": "..." }]
108
- }
109
- ```
110
-
111
- Update status
112
-
113
- ```json
114
- {
115
- "id": "r2",
116
- "type": "update-status",
117
- "payload": { "id": "UI-1", "status": "in_progress" }
118
- }
119
- ```
120
-
121
- Server push (watcher)
122
-
123
- ```json
124
- {
125
- "id": "evt-1732212345000",
126
- "ok": true,
127
- "type": "issues-changed",
128
- "payload": { "ts": 1732212345000 }
129
- }
130
- ```
131
-
132
- Error reply
133
-
134
- ```json
135
- {
136
- "id": "r3",
137
- "ok": false,
138
- "type": "show-issue",
139
- "error": { "code": "not_found", "message": "Issue UI-99" }
140
- }
141
- ```
96
+ - `list-issues`, `epic-status`, `subscribe-updates` and the legacy
97
+ `issues-changed` event.
142
98
 
143
99
  ## UI → bd Command Mapping
144
100
 
145
- - List: `bd list --json [--status <s>] [--priority <n>]`
146
- - Show: `bd show <id> --json`
101
+ - Lists and details: push‑only via `subscribe-list` (no list reads)
147
102
  - Update status: `bd update <id> --status <open|in_progress|closed>`
148
103
  - Update priority: `bd update <id> --priority <0..4>`
149
104
  - Edit title: `bd update <id> --title <text>`
@@ -151,9 +106,7 @@ Error reply
151
106
  - Edit acceptance: `bd update <id> --acceptance-criteria <text>`
152
107
  - Link dependency: `bd dep add <a> <b>` (a depends on b)
153
108
  - Unlink dependency: `bd dep remove <a> <b>`
154
- - Planned (UI not wired yet): Create:
155
- `bd create "title" -t <type> -p <prio> -d "desc"`; Ready list:
156
- `bd ready --json`
109
+ - Create issue: `bd create "title" -t <type> -p <prio> -d "desc"`
157
110
 
158
111
  Rationale
159
112
 
@@ -199,9 +152,6 @@ Notes
199
152
  - Error object: `{ code: string, message: string, details?: any }`
200
153
  - Common codes: `bad_request`, `not_found`, `bd_error`, `unknown_type`,
201
154
  `bad_json`
202
- - Versioning: `PROTOCOL_VERSION` in `app/protocol.js` (currently `1.0.0`).
203
- Breaking changes increment this value; additive message types are backwards
204
- compatible.
205
155
 
206
156
  ## Security and Local Boundaries
207
157
 
@@ -214,8 +164,8 @@ Notes
214
164
 
215
165
  - The server resolves the active beads SQLite DB path (see
216
166
  `docs/db-watching.md`).
217
- - File watcher emits `issues-changed` events with a timestamp; UI refreshes
218
- list/detail as needed.
167
+ - File watcher schedules list refresh; the server publishes subscription
168
+ envelopes. UI re-renders from local per-subscription stores.
219
169
 
220
170
  ## Risks & Open Questions
221
171