beads-ui 0.4.3 → 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/CHANGES.md +39 -0
- package/app/index.html +11 -0
- package/app/main.bundle.js +257 -236
- package/app/main.bundle.js.map +4 -4
- package/app/styles.css +146 -20
- package/package.json +18 -18
- package/server/ws.js +61 -21
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;
|
|
@@ -497,6 +519,9 @@ button:disabled {
|
|
|
497
519
|
vertical-align: middle;
|
|
498
520
|
user-select: none;
|
|
499
521
|
border: 1px solid color-mix(in srgb, currentColor 35%, transparent);
|
|
522
|
+
text-wrap: nowrap;
|
|
523
|
+
overflow: hidden;
|
|
524
|
+
text-overflow: ellipsis;
|
|
500
525
|
}
|
|
501
526
|
.badge + .badge,
|
|
502
527
|
.status-badge + .status-badge,
|
|
@@ -964,6 +989,23 @@ input.inline-edit:focus {
|
|
|
964
989
|
background: inherit;
|
|
965
990
|
backdrop-filter: saturate(140%) blur(4px);
|
|
966
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
|
+
}
|
|
967
1009
|
/* Small, unobtrusive select for closed filter */
|
|
968
1010
|
.board-closed-filter {
|
|
969
1011
|
margin: calc(var(--space-4) * -1) 0;
|
|
@@ -1324,20 +1366,20 @@ html[data-theme='dark'] {
|
|
|
1324
1366
|
gap: 10px;
|
|
1325
1367
|
padding: 8px 0;
|
|
1326
1368
|
border-top: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1369
|
+
&:first-of-type {
|
|
1370
|
+
border-top: none;
|
|
1371
|
+
}
|
|
1372
|
+
& .label {
|
|
1373
|
+
color: var(--muted);
|
|
1374
|
+
font-size: 12px;
|
|
1375
|
+
min-width: 80px;
|
|
1376
|
+
}
|
|
1377
|
+
& .value {
|
|
1378
|
+
font-size: 13px;
|
|
1379
|
+
display: flex;
|
|
1380
|
+
align-items: center;
|
|
1381
|
+
gap: 8px;
|
|
1382
|
+
}
|
|
1341
1383
|
}
|
|
1342
1384
|
|
|
1343
1385
|
/* --- Issue Details Dialog (UI-104) --- */
|
|
@@ -1437,16 +1479,100 @@ html[data-theme='dark'] {
|
|
|
1437
1479
|
line-height: 1.2;
|
|
1438
1480
|
}
|
|
1439
1481
|
|
|
1440
|
-
|
|
1441
|
-
|
|
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);
|
|
1442
1508
|
}
|
|
1443
|
-
.
|
|
1509
|
+
.fatal-error {
|
|
1444
1510
|
display: flex;
|
|
1445
|
-
|
|
1446
|
-
gap:
|
|
1511
|
+
align-items: flex-start;
|
|
1512
|
+
gap: 16px;
|
|
1513
|
+
padding: 18px 20px 20px;
|
|
1447
1514
|
}
|
|
1448
|
-
.
|
|
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 {
|
|
1449
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;
|
|
1450
1576
|
}
|
|
1451
1577
|
|
|
1452
1578
|
/* Sidebar control aesthetics */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beads-ui",
|
|
3
|
-
"version": "0.
|
|
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
|
|
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
|
-
"
|
|
26
|
+
"tsc": "tsc -p tsconfig.json --noEmit",
|
|
27
27
|
"lint": "eslint --ext .js .",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
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
|
|
39
|
+
"express": "^5.2.1",
|
|
40
40
|
"lit-html": "^3.3.1",
|
|
41
|
-
"marked": "^
|
|
41
|
+
"marked": "^17.0.1",
|
|
42
42
|
"ws": "^8.18.3"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@eslint/js": "^9.
|
|
45
|
+
"@eslint/js": "^9.39.1",
|
|
46
46
|
"@studio/changes": "^3.0.0",
|
|
47
|
-
"@trivago/prettier-plugin-sort-imports": "^
|
|
47
|
+
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
|
|
48
48
|
"@types/debug": "^4.1.12",
|
|
49
|
-
"@types/express": "^5.0.
|
|
50
|
-
"@types/node": "^22.
|
|
49
|
+
"@types/express": "^5.0.6",
|
|
50
|
+
"@types/node": "^22.19.1",
|
|
51
51
|
"@types/ws": "^8.18.1",
|
|
52
|
-
"esbuild": "^0.
|
|
53
|
-
"eslint": "^9.
|
|
52
|
+
"esbuild": "^0.27.1",
|
|
53
|
+
"eslint": "^9.39.1",
|
|
54
54
|
"eslint-plugin-import": "^2.29.1",
|
|
55
|
-
"eslint-plugin-jsdoc": "^61.1
|
|
55
|
+
"eslint-plugin-jsdoc": "^61.4.1",
|
|
56
56
|
"eslint-plugin-n": "^17.9.0",
|
|
57
|
-
"globals": "^16.
|
|
58
|
-
"jsdom": "^27.0
|
|
59
|
-
"prettier": "^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.
|
|
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
|
-
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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(
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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',
|
|
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
|
-
|
|
612
|
+
|
|
613
|
+
ws.send(JSON.stringify(makeOk(req, { id: client_id, key: attached_key })));
|
|
574
614
|
return;
|
|
575
615
|
}
|
|
576
616
|
|