beads-ui 0.4.4 → 0.5.0

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.
package/app/styles.css CHANGED
@@ -196,6 +196,28 @@ a {
196
196
  align-items: center;
197
197
  gap: var(--space-5);
198
198
  }
199
+ .header-loading {
200
+ display: inline-flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ width: 22px;
204
+ height: 22px;
205
+ color: var(--muted);
206
+ }
207
+ .header-loading__spinner {
208
+ width: 16px;
209
+ height: 16px;
210
+ border-radius: 999px;
211
+ border: 2px solid color-mix(in srgb, var(--muted) 55%, transparent);
212
+ border-top-color: var(--fg);
213
+ animation: header-spin 880ms linear infinite;
214
+ }
215
+
216
+ @keyframes header-spin {
217
+ to {
218
+ transform: rotate(360deg);
219
+ }
220
+ }
199
221
  .theme-toggle {
200
222
  display: inline-flex;
201
223
  align-items: center;
@@ -967,6 +989,23 @@ input.inline-edit:focus {
967
989
  background: inherit;
968
990
  backdrop-filter: saturate(140%) blur(4px);
969
991
  }
992
+ .board-column__title {
993
+ display: inline-flex;
994
+ align-items: center;
995
+ gap: var(--space-3);
996
+ }
997
+ .board-column__title-text {
998
+ font-weight: inherit;
999
+ }
1000
+ .board-column__count {
1001
+ background: color-mix(in srgb, var(--panel-bg) 88%, var(--muted));
1002
+ color: color-mix(in srgb, var(--muted) 82%, #000);
1003
+ border-color: color-mix(in srgb, var(--muted) 28%, transparent);
1004
+ min-width: 28px;
1005
+ text-align: center;
1006
+ letter-spacing: 0.02em;
1007
+ font-variant-numeric: tabular-nums;
1008
+ }
970
1009
  /* Small, unobtrusive select for closed filter */
971
1010
  .board-closed-filter {
972
1011
  margin: calc(var(--space-4) * -1) 0;
@@ -1440,6 +1479,102 @@ html[data-theme='dark'] {
1440
1479
  line-height: 1.2;
1441
1480
  }
1442
1481
 
1482
+ /* --- Fatal Error Dialog --- */
1483
+ #fatal-error-dialog {
1484
+ padding: 0;
1485
+ border: 1px solid var(--border);
1486
+ border-radius: 12px;
1487
+ background:
1488
+ linear-gradient(
1489
+ 180deg,
1490
+ color-mix(in srgb, var(--panel-bg) 82%, transparent),
1491
+ var(--panel-bg)
1492
+ ),
1493
+ radial-gradient(
1494
+ circle at 20% 0%,
1495
+ color-mix(in srgb, #fca5a5 48%, transparent) 0%,
1496
+ transparent 42%
1497
+ );
1498
+ color: var(--fg);
1499
+ width: min(640px, 94vw);
1500
+ max-height: 90vh;
1501
+ margin: 4vh auto;
1502
+ overflow: hidden;
1503
+ box-shadow: 0 18px 48px -16px rgba(17, 24, 39, 0.35);
1504
+ }
1505
+ #fatal-error-dialog::backdrop {
1506
+ background: color-mix(in srgb, #0f172a 55%, transparent);
1507
+ backdrop-filter: blur(1px);
1508
+ }
1509
+ .fatal-error {
1510
+ display: flex;
1511
+ align-items: flex-start;
1512
+ gap: 16px;
1513
+ padding: 18px 20px 20px;
1514
+ }
1515
+ .fatal-error__icon {
1516
+ width: 40px;
1517
+ height: 40px;
1518
+ border-radius: 12px;
1519
+ display: inline-flex;
1520
+ align-items: center;
1521
+ justify-content: center;
1522
+ font-weight: 800;
1523
+ font-size: 20px;
1524
+ color: #fff;
1525
+ background: linear-gradient(
1526
+ 135deg,
1527
+ color-mix(in srgb, var(--type-bug-base) 78%, #000),
1528
+ color-mix(in srgb, var(--type-bug-base) 58%, #111)
1529
+ );
1530
+ box-shadow: 0 10px 24px -12px rgba(159, 32, 17, 0.6);
1531
+ }
1532
+ .fatal-error__body {
1533
+ flex: 1;
1534
+ display: flex;
1535
+ flex-direction: column;
1536
+ gap: 6px;
1537
+ }
1538
+ .fatal-error__eyebrow {
1539
+ margin: 0;
1540
+ text-transform: uppercase;
1541
+ letter-spacing: 0.08em;
1542
+ font-size: 11px;
1543
+ color: color-mix(in srgb, var(--type-bug-base) 64%, #111);
1544
+ font-weight: 700;
1545
+ }
1546
+ .fatal-error__title {
1547
+ margin: 0;
1548
+ font-size: 20px;
1549
+ letter-spacing: 0.01em;
1550
+ }
1551
+ .fatal-error__message {
1552
+ margin: 0;
1553
+ color: color-mix(in srgb, var(--fg) 92%, #111);
1554
+ font-size: 14px;
1555
+ line-height: 1.5;
1556
+ }
1557
+ .fatal-error__detail {
1558
+ margin: 6px 0 0;
1559
+ background: var(--pre-bg);
1560
+ color: var(--pre-fg);
1561
+ border-radius: 8px;
1562
+ padding: 10px 12px;
1563
+ max-height: 220px;
1564
+ overflow: auto;
1565
+ font-size: 12px;
1566
+ border: 1px solid color-mix(in srgb, var(--pre-bg) 45%, #1f2937);
1567
+ white-space: pre-wrap;
1568
+ word-break: break-word;
1569
+ }
1570
+ .fatal-error__actions {
1571
+ display: flex;
1572
+ align-items: center;
1573
+ gap: 10px;
1574
+ margin-top: 10px;
1575
+ flex-wrap: wrap;
1576
+ }
1577
+
1443
1578
  /* Sidebar control aesthetics */
1444
1579
  #detail-root .props-card input[type='text'] {
1445
1580
  border: 1px solid var(--control-border);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beads-ui",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "Local UI for Beads — Collaborate on issues with your coding agent.",
5
5
  "keywords": [
6
6
  "agent",
@@ -18,15 +18,15 @@
18
18
  "node": ">=22"
19
19
  },
20
20
  "scripts": {
21
- "all": "npm run lint && npm run typecheck && npm test && npm run format:check",
21
+ "all": "npm run lint && npm run tsc && npm test && npm run prettier:check",
22
22
  "start": "node server/index.js --debug",
23
23
  "build": "node scripts/build-frontend.js",
24
24
  "test": "vitest run",
25
25
  "test:watch": "vitest",
26
- "typecheck": "tsc -p tsconfig.json --noEmit",
26
+ "tsc": "tsc -p tsconfig.json --noEmit",
27
27
  "lint": "eslint --ext .js .",
28
- "format": "prettier --write .",
29
- "format:check": "prettier --check .",
28
+ "prettier:write": "prettier --write .",
29
+ "prettier:check": "prettier --check .",
30
30
  "preversion": "npm run all",
31
31
  "version": "changes --commits --footer",
32
32
  "postversion": "git push --follow-tags && npm publish",
@@ -36,29 +36,29 @@
36
36
  "dependencies": {
37
37
  "debug": "^4.4.3",
38
38
  "dompurify": "^3.3.0",
39
- "express": "^5.1.0",
39
+ "express": "^5.2.1",
40
40
  "lit-html": "^3.3.1",
41
- "marked": "^16.4.1",
41
+ "marked": "^17.0.1",
42
42
  "ws": "^8.18.3"
43
43
  },
44
44
  "devDependencies": {
45
- "@eslint/js": "^9.38.0",
45
+ "@eslint/js": "^9.39.1",
46
46
  "@studio/changes": "^3.0.0",
47
- "@trivago/prettier-plugin-sort-imports": "^5.2.2",
47
+ "@trivago/prettier-plugin-sort-imports": "^6.0.0",
48
48
  "@types/debug": "^4.1.12",
49
- "@types/express": "^5.0.3",
50
- "@types/node": "^22.7.4",
49
+ "@types/express": "^5.0.6",
50
+ "@types/node": "^22.19.1",
51
51
  "@types/ws": "^8.18.1",
52
- "esbuild": "^0.25.11",
53
- "eslint": "^9.11.0",
52
+ "esbuild": "^0.27.1",
53
+ "eslint": "^9.39.1",
54
54
  "eslint-plugin-import": "^2.29.1",
55
- "eslint-plugin-jsdoc": "^61.1.9",
55
+ "eslint-plugin-jsdoc": "^61.4.1",
56
56
  "eslint-plugin-n": "^17.9.0",
57
- "globals": "^16.4.0",
58
- "jsdom": "^27.0.1",
59
- "prettier": "^3.3.3",
57
+ "globals": "^16.5.0",
58
+ "jsdom": "^27.2.0",
59
+ "prettier": "^3.7.4",
60
60
  "typescript": "^5.6.3",
61
- "vitest": "^4.0.6"
61
+ "vitest": "^4.0.15"
62
62
  },
63
63
  "files": [
64
64
  "app/index.html",
package/server/ws.js CHANGED
@@ -534,7 +534,8 @@ export async function handleMessage(ws, data) {
534
534
 
535
535
  // subscribe-list: payload { id: string, type: string, params?: object }
536
536
  if (req.type === 'subscribe-list') {
537
- log('subscribe-list %s', /** @type {any} */ (req.payload)?.id || '');
537
+ const payload_id = /** @type {any} */ (req.payload)?.id || '';
538
+ log('subscribe-list %s', payload_id);
538
539
  const validation = validateSubscribeListPayload(
539
540
  /** @type {any} */ (req.payload || {})
540
541
  );
@@ -546,31 +547,70 @@ export async function handleMessage(ws, data) {
546
547
  }
547
548
  const client_id = validation.id;
548
549
  const spec = validation.spec;
550
+ const key = keyOf(spec);
551
+
552
+ /**
553
+ * Reply with an error and avoid attaching the subscription when
554
+ * initialization fails.
555
+ *
556
+ * @param {string} code
557
+ * @param {string} message
558
+ * @param {Record<string, unknown>|undefined} details
559
+ */
560
+ const replyWithError = (code, message, details = undefined) => {
561
+ ws.send(JSON.stringify(makeError(req, code, message, details)));
562
+ };
563
+
564
+ /** @type {Awaited<ReturnType<typeof fetchListForSubscription>> | null} */
565
+ let initial = null;
566
+ try {
567
+ initial = await fetchListForSubscription(spec);
568
+ } catch (err) {
569
+ log('subscribe-list snapshot error for %s: %o', key, err);
570
+ const message =
571
+ (err && /** @type {any} */ (err).message) || 'Failed to load list';
572
+ replyWithError('bd_error', String(message), { key });
573
+ return;
574
+ }
575
+
576
+ if (!initial.ok) {
577
+ log(
578
+ 'initial snapshot failed for %s: %s %o',
579
+ key,
580
+ initial.error.message,
581
+ initial.error
582
+ );
583
+ const details = { ...(initial.error.details || {}), key };
584
+ replyWithError(initial.error.code, initial.error.message, details);
585
+ return;
586
+ }
587
+
549
588
  const s = ensureSubs(ws);
550
- // Attach to registry
551
- const { key } = registry.attach(spec, ws);
552
- s.list_subs?.set(client_id, { key, spec });
553
- // Send an initial snapshot for this client id only and store items
589
+ const { key: attached_key } = registry.attach(spec, ws);
590
+ s.list_subs?.set(client_id, { key: attached_key, spec });
591
+
554
592
  try {
555
- await registry.withKeyLock(key, async () => {
556
- const res = await fetchListForSubscription(spec);
557
- if (!res.ok) {
558
- log(
559
- 'initial snapshot failed for %s: %s %o',
560
- key,
561
- res.error.message,
562
- res.error
563
- );
564
- return;
565
- }
566
- const items = applyClosedIssuesFilter(spec, res.items);
567
- void registry.applyItems(key, items);
568
- emitSubscriptionSnapshot(ws, client_id, key, items);
593
+ await registry.withKeyLock(attached_key, async () => {
594
+ const items = applyClosedIssuesFilter(
595
+ spec,
596
+ initial ? initial.items : []
597
+ );
598
+ void registry.applyItems(attached_key, items);
599
+ emitSubscriptionSnapshot(ws, client_id, attached_key, items);
569
600
  });
570
601
  } catch (err) {
571
- log('subscribe-list snapshot error for %s: %o', key, err);
602
+ log('subscribe-list snapshot error for %s: %o', attached_key, err);
603
+ s.list_subs?.delete(client_id);
604
+ try {
605
+ registry.detach(spec, ws);
606
+ } catch {
607
+ // ignore detach errors
608
+ }
609
+ replyWithError('bd_error', 'Failed to publish snapshot', { key });
610
+ return;
572
611
  }
573
- ws.send(JSON.stringify(makeOk(req, { id: client_id, key })));
612
+
613
+ ws.send(JSON.stringify(makeOk(req, { id: client_id, key: attached_key })));
574
614
  return;
575
615
  }
576
616