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,198 @@
1
+ # Data Exchange Model — Subscription‑Based Updates (Full‑Issue)
2
+
3
+ ```
4
+ Date: 2025-10-25
5
+ Status: Implemented
6
+ Owner: agent
7
+ ```
8
+
9
+ ## Goals
10
+
11
+ - Replace ad-hoc list fetching with subscription-based incremental updates.
12
+ - Minimize complexity; send full‑issue payloads in envelopes targeted to a
13
+ specific subscription key.
14
+ - Ensure consistent, race-free updates around user-triggered mutations.
15
+ - Keep UI models per-subscription to simplify rendering and memory usage.
16
+
17
+ ## Scope
18
+
19
+ - Server and client for `beads-ui`.
20
+ - Uses `bd` CLI for data access; no DB schema changes.
21
+
22
+ ## Subscription Types
23
+
24
+ - `all-issues`
25
+ - `epics` // Removed: `issues-for-epic` (use `issue-detail` for the epic and
26
+ render its `dependents`)
27
+ - `blocked-issues`
28
+ - `ready-issues`
29
+ - `in-progress-issues`
30
+ - `closed-issues` (special filtering noted below)
31
+
32
+ ## Server Architecture
33
+
34
+ ### Subscription Registry (Issue List Subscriptions)
35
+
36
+ - Keyed by `subscriptionKey = type + JSON.stringify(params)`.
37
+ - Value:
38
+ `{ itemsById: Map<string, { updated_at: string, closed_at: string|null }>, subscribers: Set<SubscriberId>, lastRunAt?: number }`.
39
+ - Each subscribe request either attaches to an existing registry entry or
40
+ creates a new one.
41
+ - No TTL: subscriptions are evicted only on WebSocket disconnect. Unsubscribe
42
+ removes a subscriber from the set but keeps the registry entry until the
43
+ connection closes.
44
+
45
+ ### Mapping to `bd` Commands
46
+
47
+ - `all-issues` → `bd list` (default/open)
48
+ - `epics` → `bd list --type epic` (or equivalent)
49
+ - `detail:{id}` → `bd show <id> --json` (use `dependents` from the epic detail
50
+ for children)
51
+ - `blocked-issues` → `bd list --blocked`
52
+ - `ready-issues` → `bd ready --limit 1000`
53
+ - `in-progress-issues` → `bd list --status in_progress`
54
+ - `closed-issues` → `bd list --status closed` (then filter first; see Special
55
+ Cases)
56
+
57
+ Notes:
58
+
59
+ - Exact flags depend on `bd`; create adapters that encapsulate CLI details and
60
+ normalize results.
61
+
62
+ ### Refresh Algorithm (per run)
63
+
64
+ 1. Execute mapped `bd` command to get the full list of `issues` for the spec.
65
+ 2. If subscription is `closed-issues` with a filter, apply it before step 3.
66
+ 3. Compare with the registry’s last known items for this subscription key.
67
+ 4. For new or changed items, emit `upsert` envelopes with the full issue payload
68
+ to all subscribers of the key on the current connection.
69
+ 5. For removed items, emit `delete` envelopes with `issue_id`.
70
+ 6. Update the registry’s state for the key.
71
+
72
+ ### Special Case: Closed Issues Filtering
73
+
74
+ - Apply `since` filter (epoch milliseconds) before diffing to avoid spurious
75
+ updates when reloading older closed items. Only items with
76
+ `closed_at >= since` are included. Invalid or non-positive `since` values are
77
+ ignored.
78
+ - Filters are part of subscription params to keep deterministic diffing.
79
+
80
+ ### Migration
81
+
82
+ This change replaces request/response list reads and id‑only deltas with
83
+ subscription‑based, full‑issue push envelopes.
84
+
85
+ Client migration steps:
86
+
87
+ - Replace list fetch calls with `subscribe-list`/`unsubscribe-list` messages.
88
+ - Maintain a per‑subscription local store keyed by the client `id`.
89
+ - Apply `snapshot`/`upsert`/`delete` envelopes in revision order; render from
90
+ `store.snapshot()`.
91
+ - Remove any legacy polling timers; updates now arrive via server push.
92
+ - For closed issue feeds, pass a `params.since` value (epoch ms) that reflects
93
+ the UI’s filter horizon if needed server‑side.
94
+
95
+ ### Watcher Integration (DB Updates)
96
+
97
+ - A file/DB watcher signals any data change.
98
+ - On signal, for each active subscription: re-run its mapped `bd` command → diff
99
+ → push deltas to all subscribers.
100
+ - Backpressure: coalesce multiple watcher events into a single run per
101
+ subscription (leading-edge, with trailing-edge within 50–100ms).
102
+
103
+ ### User Mutations (Race Control)
104
+
105
+ When client requests a change (e.g., update status):
106
+
107
+ 1. Execute the explicit protocol mutation (mapped to a concrete `bd` command
108
+ under the hood; no arbitrary commands allowed).
109
+ 2. In parallel, attach a once-listener to the watcher that resolves on the next
110
+ change event (no debounce) or a 500ms timeout, whichever occurs first.
111
+ 3. After the promise resolves, for each affected subscription, run the standard
112
+ refresh/diff/push routine exactly once.
113
+ 4. During the pending mutation window, suppress watcher-triggered refreshes for
114
+ affected subscriptions to avoid duplicate pushes.
115
+
116
+ ### Error Handling
117
+
118
+ - Validate subscription params; return structured errors.
119
+ - For `bd` failures, include stderr and exit code; do not crash subscriptions.
120
+ - If a subscriber disconnects mid-push, drop silently and clean up.
121
+
122
+ ## Client Architecture
123
+
124
+ ### Local Store per Subscription
125
+
126
+ - Keyed by `subscriptionKey`.
127
+ - Value: `{ itemsById: Map<string, Issue>, lastAppliedAt: number }`.
128
+ - On `{ added, updated, removed }`, update `itemsById` accordingly and request
129
+ view re-render.
130
+ - Tabs and epic expansion toggle subscribe/unsubscribe appropriately.
131
+
132
+ ### UI Flow
133
+
134
+ - Tab switch: unsubscribe previous, subscribe new.
135
+ - Epic toggle: subscribe/unsubscribe `detail:{id}` with
136
+ `{ type: 'issue-detail', params: { id } }`.
137
+ - Components derive view state from the local store snapshot.
138
+
139
+ ## Wire Protocol (vNext)
140
+
141
+ ### Messages: Client → Server
142
+
143
+ - `subscribe-list` `{ id: string, type: string, params?: object }`
144
+ - `unsubscribe-list` `{ id: string }`
145
+ - Explicit mutation messages (enumerated in the protocol; no generic command
146
+ pipe). The set mirrors the main protocol (update-status, edit-text,
147
+ update-priority, update-assignee, create-issue, dep-add/remove,
148
+ label-add/remove).
149
+
150
+ ### Messages: Server → Client (Per‑Subscription)
151
+
152
+ All envelopes include a per‑subscription `revision` (monotonic, starting at 1),
153
+ and the client subscription `id`.
154
+
155
+ - `snapshot` `{ id, schema, revision, issues: Issue[] }`
156
+ - `upsert` `{ id, schema, revision, issue: Issue }`
157
+ - `delete` `{ id, schema, revision, issue_id: string }`
158
+
159
+ Notes
160
+
161
+ - Initial subscribe triggers a single `snapshot` for the requesting `id` only.
162
+ - Subsequent refresh runs emit `upsert`/`delete` events to all subscribers of
163
+ the same subscription key on that connection.
164
+ - Clients MUST apply envelopes in `revision` order and ignore stale revisions.
165
+
166
+ ## Concurrency & Ordering Guarantees
167
+
168
+ - Per-subscription ordering: server serializes diff runs per key.
169
+ - Deltas are applied in order on the client; no interleaving for a given `id`.
170
+ - Mutations provide “eventually up-to-date” guarantee via the once-listener +
171
+ timeout.
172
+
173
+ ## Observability
174
+
175
+ - Basic development logging only; no telemetry collection for message rates.
176
+
177
+ ## Security
178
+
179
+ - Only explicit mutation operations are implemented by the protocol; no
180
+ arbitrary commands from clients.
181
+ - Reject unknown subscription types; enforce param schemas.
182
+
183
+ ## Testing Strategy
184
+
185
+ - Unit: diffing, registry, adapter mapping, filter logic.
186
+ - Integration: watcher → refresh → push flow; mutation window once-only
187
+ behavior.
188
+ - E2E: tab switching, epic expansion, status changes while updates stream.
189
+
190
+ ## Release Notes
191
+
192
+ - Breaking change: Clients must adopt `snapshot`/`upsert`/`delete` envelopes and
193
+ per‑subscription stores. Previous polling and id‑only list deltas are removed.
194
+
195
+ ## Open Questions
196
+
197
+ - Exact `bd` flags for each list type; confirm and codify.
198
+ - Closed-issue filter semantics (date range vs. other criteria).
@@ -1,7 +1,8 @@
1
1
  # DB Watching and Resolution
2
2
 
3
3
  The server watches the active beads SQLite database file for changes and
4
- broadcasts an `issues-changed` event to connected clients.
4
+ schedules a refresh of active list subscriptions. Clients receive
5
+ `snapshot`/`upsert`/`delete` envelopes for their active subscriptions.
5
6
 
6
7
  ## Resolution Order
7
8
 
@@ -0,0 +1,54 @@
1
+ # Migration: Push‑Only Per‑Subscription Stores (Breaking)
2
+
3
+ ```
4
+ Date: 2025-10-26
5
+ Status: Final
6
+ Owner: agent
7
+ ```
8
+
9
+ This release replaces legacy list reads and id‑only list deltas with
10
+ per‑subscription push envelopes that carry full issue payloads. There is no
11
+ compatibility mode and no feature flags.
12
+
13
+ ## Required Versions
14
+
15
+ - beads‑ui: 0.2.0 or later (includes the server)
16
+ - Node.js: >= 22 (see `package.json` engines)
17
+
18
+ Upgrade:
19
+
20
+ ```sh
21
+ npm i -g beads-ui@latest
22
+ ```
23
+
24
+ ## What Changed
25
+
26
+ - New protocol: `snapshot` / `upsert` / `delete` envelopes and a
27
+ per‑subscription `revision`.
28
+ - One store per list: views render from a `SubscriptionIssueStore` created for
29
+ each active subscription id.
30
+ - Removed: central issues store and delta fan‑out.
31
+ - Removed: legacy read RPCs `list-issues` and `epic-status`.
32
+
33
+ ## Migration Checklist
34
+
35
+ - Replace list reads with `subscribe-list`/`unsubscribe-list`.
36
+ - Create a `SubscriptionIssueStore` at view mount and wire the WS client to call
37
+ `store.applyPush(payload)` for `snapshot`/`upsert`/`delete`.
38
+ - Render from `store.snapshot()`; remove code paths that read from a central
39
+ issue cache.
40
+ - Delete dead selectors/helpers that depended on the central cache.
41
+ - Verify reconnect flows: a fresh `snapshot` (rev 1) replaces state cleanly.
42
+
43
+ ## Notes
44
+
45
+ - No telemetry or phased rollout was implemented; ensure the UI and server are
46
+ updated together. Older clients will not function with the new server.
47
+ - For closed‑issues feeds, prefer passing a `since` param where applicable to
48
+ keep snapshots small.
49
+
50
+ ## References
51
+
52
+ - `docs/protocol/issues-push-v2.md`
53
+ - `docs/subscription-issue-store.md`
54
+ - ADR 002 — Per‑Subscription Stores and Full‑Issue Push
@@ -0,0 +1,179 @@
1
+ # Subscription Push Protocol — per‑subscription full‑issue envelopes (Breaking)
2
+
3
+ ```
4
+ Date: 2025-10-26
5
+ Status: Implemented
6
+ Owner: agent
7
+ ```
8
+
9
+ This document specifies the push‑only protocol used by beads‑ui to deliver list
10
+ updates from the local server to the client. It replaces the legacy
11
+ notify‑then‑fetch model. There is no version negotiation or fallback.
12
+
13
+ ## Overview
14
+
15
+ - Transport: single WebSocket connection per client
16
+ - Encoding: JSON text frames
17
+ - Subscriptions: one client‑chosen `id` per active list subscription
18
+ - Delivery: per‑subscription envelopes with full issue payloads
19
+ - Messages: `snapshot` | `upsert` | `delete`
20
+ - Ordering: strictly increasing `revision` per subscription key and connection
21
+
22
+ ## Envelopes
23
+
24
+ ```ts
25
+ export type SnapshotEnvelope = {
26
+ type: 'snapshot';
27
+ id: string; // client subscription id
28
+ revision: number; // starts at 1 and increments per envelope
29
+ issues: Issue[]; // full list for this subscription
30
+ };
31
+
32
+ export type UpsertEnvelope = {
33
+ type: 'upsert';
34
+ id: string;
35
+ revision: number;
36
+ issue: Issue; // full issue payload
37
+ };
38
+
39
+ export type DeleteEnvelope = {
40
+ type: 'delete';
41
+ id: string;
42
+ revision: number;
43
+ issue_id: string; // id only
44
+ };
45
+ ```
46
+
47
+ Notes
48
+
49
+ - Server serializes refresh runs per subscription key and emits envelopes in
50
+ `revision` order. Clients MUST ignore any envelope with `revision <=` the last
51
+ applied for the same `id`.
52
+ - Clients SHOULD treat `upsert` as idempotent and MAY additionally guard on an
53
+ `issue.updated_at` timestamp to ignore stale updates racing with local state.
54
+
55
+ ## Handshake (subscribe‑list)
56
+
57
+ Client subscribes to a list with a chosen `id`, a `type`, and optional `params`.
58
+
59
+ Client → Server
60
+
61
+ ```json
62
+ {
63
+ "id": "req-1",
64
+ "type": "subscribe-list",
65
+ "payload": { "id": "ready", "type": "ready-issues" }
66
+ }
67
+ ```
68
+
69
+ Server → Client
70
+
71
+ ```json
72
+ {
73
+ "id": "req-1",
74
+ "ok": true,
75
+ "type": "subscribe-list",
76
+ "payload": { "id": "ready", "key": "ready-issues:{}" }
77
+ }
78
+ ```
79
+
80
+ Immediately after the ack, the server sends a `snapshot` envelope containing the
81
+ full list for that subscription `id` with `revision: 1`.
82
+
83
+ ```json
84
+ {
85
+ "id": "evt-1730000000000",
86
+ "ok": true,
87
+ "type": "snapshot",
88
+ "payload": {
89
+ "type": "snapshot",
90
+ "id": "ready",
91
+ "revision": 1,
92
+ "issues": [{ "id": "UI-1", "title": "..." }]
93
+ }
94
+ }
95
+ ```
96
+
97
+ ## Updates
98
+
99
+ Subsequent refreshes emit `upsert` and `delete` envelopes as the list changes.
100
+
101
+ ```json
102
+ {
103
+ "id": "evt-1730000000100",
104
+ "ok": true,
105
+ "type": "upsert",
106
+ "payload": {
107
+ "type": "upsert",
108
+ "id": "ready",
109
+ "revision": 2,
110
+ "issue": { "id": "UI-2", "status": "in_progress" }
111
+ }
112
+ }
113
+ ```
114
+
115
+ ```json
116
+ {
117
+ "id": "evt-1730000000200",
118
+ "ok": true,
119
+ "type": "delete",
120
+ "payload": {
121
+ "type": "delete",
122
+ "id": "ready",
123
+ "revision": 3,
124
+ "issue_id": "UI-9"
125
+ }
126
+ }
127
+ ```
128
+
129
+ ## Reconnect Behavior
130
+
131
+ - On reconnect, clients resubscribe using the same `id` values as needed. The
132
+ server treats this as a new connection and sends a fresh `snapshot` with
133
+ `revision: 1` for each active subscription.
134
+
135
+ ## Diagrams
136
+
137
+ ```mermaid
138
+ sequenceDiagram
139
+ participant C as Client
140
+ participant S as Server
141
+ C->>S: subscribe-list { id, type, params }
142
+ S-->>C: ack { id, key }
143
+ S-->>C: snapshot { id, schema, revision:1, issues:[...] }
144
+ S-->>C: upsert/delete { id, schema, revision:n, ... }
145
+ ```
146
+
147
+ ```mermaid
148
+ stateDiagram-v2
149
+ [*] --> Idle
150
+ Idle --> Subscribed: subscribe-list(id)
151
+ Subscribed --> Subscribed: snapshot(rev=1)
152
+ Subscribed --> Subscribed: upsert/delete(rev++)
153
+ Subscribed --> Idle: unsubscribe-list(id) / disconnect
154
+ ```
155
+
156
+ ## Client Responsibilities
157
+
158
+ - Maintain one `SubscriptionIssueStore` per active subscription `id`.
159
+ - Apply envelopes strictly in `revision` order; ignore stale revisions.
160
+ - Render list components from `store.snapshot()` (deterministic order).
161
+ - Dispose stores on route/tab changes.
162
+
163
+ Detail view
164
+
165
+ - Detail pages use the same mechanism with a single‑item subscription, e.g.
166
+ `{ type: 'issue-detail', params: { id: 'UI-1' } }` under a client id like
167
+ `detail:UI-1`. The server returns a one‑element list for `snapshot` and
168
+ `upsert` events.
169
+
170
+ ## Rollout and Compatibility
171
+
172
+ - Breaking change: no flags and no compatibility layer with the legacy
173
+ notify‑then‑fetch flow. Update both client and server together.
174
+
175
+ ## See Also
176
+
177
+ - ADR 002 — Per‑Subscription Stores and Full‑Issue Push
178
+ - `docs/data-exchange-subscription-plan.md` (server refresh and publish model)
179
+ - `docs/subscription-issue-store.md` (store API and usage examples)
@@ -0,0 +1,112 @@
1
+ # SubscriptionIssueStore — API and Usage Examples
2
+
3
+ ```
4
+ Date: 2025-10-26
5
+ Status: Implemented
6
+ Owner: agent
7
+ ```
8
+
9
+ The `SubscriptionIssueStore` is a per‑subscription in‑memory store that owns the
10
+ issues for a single list subscription. It applies server push envelopes
11
+ (`snapshot`/`upsert`/`delete`) in revision order and exposes a deterministic,
12
+ read‑only snapshot for rendering.
13
+
14
+ See also: `docs/protocol/issues-push-v2.md` for the wire protocol.
15
+
16
+ ## API
17
+
18
+ Factory: `app/data/subscription-issue-store.js`
19
+
20
+ ```js
21
+ import { createSubscriptionIssueStore } from '../app/data/subscription-issue-store.js';
22
+
23
+ // Create at view mount (id is client-chosen)
24
+ const store = createSubscriptionIssueStore('ready');
25
+
26
+ // Listen for changes
27
+ const unsubscribe = store.subscribe(() => {
28
+ render(store.snapshot());
29
+ });
30
+
31
+ // Apply push envelopes from the WebSocket client
32
+ ws.on('message', (evt) => {
33
+ const msg = JSON.parse(evt.data);
34
+ if (msg && msg.ok === true && msg.payload && msg.payload.id === 'ready') {
35
+ // payload has { type, id, schema, revision, ... }
36
+ store.applyPush(msg.payload);
37
+ }
38
+ });
39
+
40
+ // Read helpers
41
+ store.size(); // number of issues
42
+ store.getById('UI-1'); // lookup by id
43
+
44
+ // Dispose on unmount
45
+ unsubscribe();
46
+ store.dispose();
47
+ ```
48
+
49
+ Options: deterministic sort can be customized per list via the optional
50
+ `{ sort(a,b) }` parameter when constructing the store.
51
+
52
+ ## Subscribing to a List
53
+
54
+ Pair store creation with the subscribe‑list handshake. The server will send a
55
+ `snapshot` immediately after the ack, followed by `upsert`/`delete`.
56
+
57
+ ```js
58
+ // Request a subscription
59
+ socket.send(
60
+ JSON.stringify({
61
+ id: 'req-1',
62
+ type: 'subscribe-list',
63
+ payload: { id: 'ready', type: 'ready-issues' }
64
+ })
65
+ );
66
+
67
+ socket.addEventListener('message', (ev) => {
68
+ const frame = JSON.parse(ev.data);
69
+ if (frame.ok && frame.type === 'snapshot' && frame.payload.id === 'ready') {
70
+ store.applyPush(frame.payload);
71
+ }
72
+ if (frame.ok && frame.type === 'upsert' && frame.payload.id === 'ready') {
73
+ store.applyPush(frame.payload);
74
+ }
75
+ if (frame.ok && frame.type === 'delete' && frame.payload.id === 'ready') {
76
+ store.applyPush(frame.payload);
77
+ }
78
+ });
79
+ ```
80
+
81
+ ## Rendering Pattern (List component)
82
+
83
+ ```js
84
+ /** @param {{ store: ReturnType<typeof createSubscriptionIssueStore> }} props */
85
+ export function ListView({ store }) {
86
+ let items = store.snapshot();
87
+
88
+ const un = store.subscribe(() => {
89
+ items = store.snapshot();
90
+ requestRender();
91
+ });
92
+
93
+ // framework-specific teardown
94
+ onUnmount(() => un());
95
+
96
+ return html`<ul>
97
+ ${items.map((it) => html`<li data-id=${it.id}>${it.title}</li>`)}
98
+ </ul>`;
99
+ }
100
+ ```
101
+
102
+ ## Ordering and Identity
103
+
104
+ - Default sort: priority asc, then `created_at` desc, then id asc.
105
+ - When upserting, the store preserves object identity for existing ids by
106
+ mutating fields in place. This reduces unnecessary re‑renders.
107
+
108
+ ## Reconnects
109
+
110
+ - On reconnect, repeat the subscribe‑list call. The server sends a fresh
111
+ `snapshot` with `revision: 1`. The store ignores stale envelopes using the
112
+ `revision` guard.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beads-ui",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Local‑first UI for Beads — a fast issue tracker for your coding agent.",
5
5
  "homepage": "https://github.com/mantoni/beads-ui",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "format": "prettier --write .",
21
21
  "format:check": "prettier --check .",
22
22
  "preversion": "npm run all",
23
- "version": "changes",
23
+ "version": "changes --commits --footer",
24
24
  "postversion": "git push --follow-tags && npm publish"
25
25
  },
26
26
  "devDependencies": {
@@ -32,9 +32,8 @@
32
32
  "@types/ws": "^8.18.1",
33
33
  "eslint": "^9.11.0",
34
34
  "eslint-plugin-import": "^2.29.1",
35
- "eslint-plugin-jsdoc": "^48.10.2",
35
+ "eslint-plugin-jsdoc": "^61.1.9",
36
36
  "eslint-plugin-n": "^17.9.0",
37
- "eslint-plugin-promise": "^6.1.1",
38
37
  "globals": "^16.4.0",
39
38
  "jsdom": "^27.0.1",
40
39
  "prettier": "^3.3.3",
@@ -42,9 +41,11 @@
42
41
  "vitest": "^2.1.3"
43
42
  },
44
43
  "dependencies": {
44
+ "dompurify": "^3.3.0",
45
45
  "esbuild": "^0.25.11",
46
46
  "express": "^5.1.0",
47
47
  "lit-html": "^3.3.1",
48
+ "marked": "^16.4.1",
48
49
  "ws": "^8.18.3"
49
50
  },
50
51
  "files": [
package/server/app.js CHANGED
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
 
7
7
  /**
8
8
  * Create and configure the Express application.
9
+ *
9
10
  * @param {{ host: string, port: number, env: string, app_dir: string, root_dir: string }} config - Server configuration.
10
11
  * @returns {Express} Configured Express app instance.
11
12
  */
@@ -28,6 +29,7 @@ export function createApp(config) {
28
29
  /**
29
30
  * On-demand bundle for the browser using esbuild.
30
31
  * Note: esbuild is loaded lazily so tests don't require it to be installed.
32
+ *
31
33
  * @param {Request} _req
32
34
  * @param {Response} res
33
35
  */
package/server/bd.js CHANGED
@@ -3,6 +3,7 @@ import { resolveDbPath } from './db.js';
3
3
 
4
4
  /**
5
5
  * Resolve the bd executable path.
6
+ *
6
7
  * @returns {string}
7
8
  */
8
9
  export function getBdBin() {
@@ -16,6 +17,7 @@ export function getBdBin() {
16
17
  /**
17
18
  * Run the `bd` CLI with provided arguments.
18
19
  * Shell is not used to avoid injection; args must be pre-split.
20
+ *
19
21
  * @param {string[]} args - Arguments to pass (e.g., ["list", "--json"]).
20
22
  * @param {{ cwd?: string, env?: Record<string, string | undefined>, timeout_ms?: number }} [options]
21
23
  * @returns {Promise<{ code: number, stdout: string, stderr: string }>}
@@ -42,14 +44,12 @@ export function runBd(args, options = {}) {
42
44
 
43
45
  if (child.stdout) {
44
46
  child.stdout.setEncoding('utf8');
45
- /** @param {string} chunk */
46
47
  child.stdout.on('data', (chunk) => {
47
48
  out_chunks.push(String(chunk));
48
49
  });
49
50
  }
50
51
  if (child.stderr) {
51
52
  child.stderr.setEncoding('utf8');
52
- /** @param {string} chunk */
53
53
  child.stderr.on('data', (chunk) => {
54
54
  err_chunks.push(String(chunk));
55
55
  });
@@ -90,6 +90,7 @@ export function runBd(args, options = {}) {
90
90
 
91
91
  /**
92
92
  * Run `bd` and parse JSON from stdout if exit code is 0.
93
+ *
93
94
  * @param {string[]} args - Must include flags that cause JSON to be printed (e.g., `--json`).
94
95
  * @param {{ cwd?: string, env?: Record<string, string | undefined>, timeout_ms?: number }} [options]
95
96
  * @returns {Promise<{ code: number, stdoutJson?: unknown, stderr?: string }>}
@@ -111,6 +112,7 @@ export async function runBdJson(args, options = {}) {
111
112
 
112
113
  /**
113
114
  * Add a resolved "--db <path>" pair to args when none present.
115
+ *
114
116
  * @param {string[]} args
115
117
  * @param {string} cwd
116
118
  * @param {Record<string, string | undefined>} env
@@ -13,6 +13,7 @@ import { openUrl, waitForServer } from './open.js';
13
13
  * Handle `start` command. Idempotent when already running.
14
14
  * - Spawns a detached server process, writes PID file, returns 0.
15
15
  * - If already running (PID file present and process alive), prints URL and returns 0.
16
+ *
16
17
  * @returns {Promise<number>} Exit code (0 on success)
17
18
  */
18
19
  /**
@@ -36,8 +37,7 @@ export async function handleStart(options) {
36
37
  printServerUrl();
37
38
  // Auto-open the browser once for a fresh daemon start
38
39
  if (!no_open) {
39
- const cfg = getConfig();
40
- const url = 'http://' + cfg.host + ':' + String(cfg.port);
40
+ const { url } = getConfig();
41
41
  // Wait briefly for the server to accept connections (single retry window)
42
42
  await waitForServer(url, 600);
43
43
  // Best-effort open; ignore result
@@ -53,6 +53,7 @@ export async function handleStart(options) {
53
53
  * Handle `stop` command.
54
54
  * - Sends SIGTERM and waits for exit (with SIGKILL fallback), removes PID file.
55
55
  * - Returns 2 if not running.
56
+ *
56
57
  * @returns {Promise<number>} Exit code
57
58
  */
58
59
  export async function handleStop() {
@@ -79,12 +80,14 @@ export async function handleStop() {
79
80
 
80
81
  /**
81
82
  * Handle `restart` command: stop (ignore not-running) then start.
83
+ *
82
84
  * @returns {Promise<number>} Exit code (0 on success)
83
85
  */
84
86
  /**
85
87
  * Handle `restart` command: stop (ignore not-running) then start.
86
88
  * Accepts the same options as `handleStart` and passes them through,
87
89
  * so restart only opens a browser when `no_open` is explicitly false.
90
+ *
88
91
  * @param {{ no_open?: boolean }} [options]
89
92
  * @returns {Promise<number>}
90
93
  */