@worca/ui 0.31.0 → 0.32.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/README.md +10 -0
- package/app/main.bundle.js +1446 -1444
- package/app/main.bundle.js.map +4 -4
- package/app/styles.css +55 -5
- package/package.json +2 -1
- package/server/beads-reader.js +12 -4
- package/server/ws-beads-watcher.js +131 -21
- package/server/ws-modular.js +4 -2
package/app/styles.css
CHANGED
|
@@ -2425,6 +2425,13 @@ sl-details.agent-prompt-section::part(content) {
|
|
|
2425
2425
|
margin-bottom: 12px;
|
|
2426
2426
|
}
|
|
2427
2427
|
|
|
2428
|
+
/* Divider + gap before the user message header */
|
|
2429
|
+
.agent-prompt-block + .agent-prompt-block {
|
|
2430
|
+
margin-top: 18px;
|
|
2431
|
+
padding-top: 16px;
|
|
2432
|
+
border-top: 1px solid var(--border-subtle);
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2428
2435
|
.agent-prompt-label-row {
|
|
2429
2436
|
display: flex;
|
|
2430
2437
|
align-items: center;
|
|
@@ -2455,12 +2462,14 @@ sl-details.agent-prompt-section::part(content) {
|
|
|
2455
2462
|
flex-shrink: 0;
|
|
2456
2463
|
}
|
|
2457
2464
|
|
|
2465
|
+
/* Accent the section headers (not the message bodies) so each block reads as
|
|
2466
|
+
a labelled section; the bodies stay neutral. */
|
|
2458
2467
|
.agent-prompt-label {
|
|
2459
|
-
font-size:
|
|
2460
|
-
font-weight:
|
|
2461
|
-
color: var(--
|
|
2468
|
+
font-size: 12px;
|
|
2469
|
+
font-weight: 700;
|
|
2470
|
+
color: var(--accent);
|
|
2462
2471
|
text-transform: uppercase;
|
|
2463
|
-
letter-spacing: 0.
|
|
2472
|
+
letter-spacing: 0.6px;
|
|
2464
2473
|
margin-bottom: 4px;
|
|
2465
2474
|
}
|
|
2466
2475
|
|
|
@@ -4833,7 +4842,7 @@ sl-tooltip.bead-tooltip::part(body) {
|
|
|
4833
4842
|
|
|
4834
4843
|
.bead-tooltip-excerpt {
|
|
4835
4844
|
font-size: 12px;
|
|
4836
|
-
white-space:
|
|
4845
|
+
white-space: normal;
|
|
4837
4846
|
margin-bottom: 2px;
|
|
4838
4847
|
}
|
|
4839
4848
|
|
|
@@ -6248,6 +6257,47 @@ sl-dialog.markdown-dialog::part(body) {
|
|
|
6248
6257
|
text-align: left;
|
|
6249
6258
|
}
|
|
6250
6259
|
|
|
6260
|
+
/* --- Markdown context overrides --- */
|
|
6261
|
+
|
|
6262
|
+
.markdown-inline.markdown-body {
|
|
6263
|
+
display: inline;
|
|
6264
|
+
font-size: inherit;
|
|
6265
|
+
line-height: inherit;
|
|
6266
|
+
}
|
|
6267
|
+
.markdown-inline.markdown-body p {
|
|
6268
|
+
display: inline;
|
|
6269
|
+
margin: 0;
|
|
6270
|
+
}
|
|
6271
|
+
.markdown-inline.markdown-body code {
|
|
6272
|
+
font-size: 0.85em;
|
|
6273
|
+
}
|
|
6274
|
+
|
|
6275
|
+
.agent-prompt-content .markdown-body {
|
|
6276
|
+
white-space: normal;
|
|
6277
|
+
font-size: 12px;
|
|
6278
|
+
line-height: 1.5;
|
|
6279
|
+
}
|
|
6280
|
+
.agent-prompt-content .markdown-body h1,
|
|
6281
|
+
.agent-prompt-content .markdown-body h2,
|
|
6282
|
+
.agent-prompt-content .markdown-body h3 {
|
|
6283
|
+
margin: 0.8em 0 0.3em;
|
|
6284
|
+
}
|
|
6285
|
+
|
|
6286
|
+
.bead-tooltip-excerpt.markdown-body {
|
|
6287
|
+
white-space: normal;
|
|
6288
|
+
font-size: 12px;
|
|
6289
|
+
max-height: 200px;
|
|
6290
|
+
overflow-y: auto;
|
|
6291
|
+
}
|
|
6292
|
+
.bead-tooltip-excerpt.markdown-body p {
|
|
6293
|
+
margin: 0.3em 0;
|
|
6294
|
+
}
|
|
6295
|
+
.bead-tooltip-excerpt.markdown-body pre {
|
|
6296
|
+
font-size: 11px;
|
|
6297
|
+
max-height: 120px;
|
|
6298
|
+
overflow: auto;
|
|
6299
|
+
}
|
|
6300
|
+
|
|
6251
6301
|
.workspace-run-card .workspace-card-root,
|
|
6252
6302
|
.workspace-run-card .workspace-card-name {
|
|
6253
6303
|
font-family: var(--font-mono);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@worca/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Pipeline monitoring UI for worca-cc",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sinisha Djukic",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"@xterm/addon-fit": "^0.11.0",
|
|
66
66
|
"@xterm/addon-search": "^0.16.0",
|
|
67
67
|
"@xterm/xterm": "^6.0.0",
|
|
68
|
+
"dompurify": "^3.4.5",
|
|
68
69
|
"express": "^5.2.1",
|
|
69
70
|
"lit-html": "^3.3.1",
|
|
70
71
|
"lucide": "^0.577.0",
|
package/server/beads-reader.js
CHANGED
|
@@ -40,7 +40,7 @@ function transformIssue(issue, deps) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
async function
|
|
43
|
+
export async function enrichIssuesWithDeps(issues, dbPath) {
|
|
44
44
|
if (issues.length === 0) return [];
|
|
45
45
|
const detailed = await runBd(['show', ...issues.map((i) => i.id)], dbPath);
|
|
46
46
|
const detailMap = new Map(detailed.map((d) => [d.id, d]));
|
|
@@ -54,13 +54,21 @@ export function dbExists(beadsDb) {
|
|
|
54
54
|
return existsSync(beadsDb);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export async function listIssuesShallow(beadsDb) {
|
|
58
|
+
try {
|
|
59
|
+
return await runBd(['list', '--limit', '0'], beadsDb);
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
57
65
|
export async function listIssues(beadsDb) {
|
|
58
66
|
try {
|
|
59
67
|
const issues = await runBd(['list', '--limit', '0'], beadsDb);
|
|
60
|
-
// Must await here — without it, an
|
|
68
|
+
// Must await here — without it, an enrichIssuesWithDeps rejection (e.g. bd show
|
|
61
69
|
// SIGTERM under daemon contention) escapes the try/catch and propagates
|
|
62
70
|
// to the WS handler as an unhandled rejection, crashing Node.
|
|
63
|
-
return await
|
|
71
|
+
return await enrichIssuesWithDeps(issues, beadsDb);
|
|
64
72
|
} catch {
|
|
65
73
|
return [];
|
|
66
74
|
}
|
|
@@ -72,7 +80,7 @@ export async function listIssuesByLabel(beadsDb, label) {
|
|
|
72
80
|
['list', '--label-any', label, '--all', '--limit', '0'],
|
|
73
81
|
beadsDb,
|
|
74
82
|
);
|
|
75
|
-
return await
|
|
83
|
+
return await enrichIssuesWithDeps(issues, beadsDb);
|
|
76
84
|
};
|
|
77
85
|
try {
|
|
78
86
|
return await attempt();
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
import { existsSync, statSync, unwatchFile, watch, watchFile } from 'node:fs';
|
|
8
8
|
import { join, resolve } from 'node:path';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
countIssuesByRunLabel,
|
|
11
|
+
enrichIssuesWithDeps,
|
|
12
|
+
listIssuesShallow,
|
|
13
|
+
} from './beads-reader.js';
|
|
10
14
|
|
|
11
15
|
const BEADS_DEBOUNCE_MS = 500;
|
|
12
16
|
const BEADS_POLL_MS = 2000;
|
|
@@ -34,19 +38,73 @@ export function createBeadsWatcher({ worcaDir, broadcaster, projectId }) {
|
|
|
34
38
|
let lastPayloadJson = null;
|
|
35
39
|
let lastSelfReadWalStat = null;
|
|
36
40
|
let latestCounts = {};
|
|
41
|
+
// In-flight guard + trailing coalesce. The refresh body spawns several `bd`
|
|
42
|
+
// subprocesses (listIssuesShallow, then enrichIssuesWithDeps +
|
|
43
|
+
// countIssuesByRunLabel when the fingerprint changes) that, on a large beads
|
|
44
|
+
// db, take seconds. The debounce only collapses scheduling — once the async body
|
|
45
|
+
// is awaiting, a fresh db/WAL event would otherwise start an overlapping
|
|
46
|
+
// refresh and pile up bd processes unbounded. Allow at most one refresh in
|
|
47
|
+
// flight; events arriving mid-refresh collapse into a single trailing pass.
|
|
48
|
+
let refreshing = false;
|
|
49
|
+
let refreshPending = false;
|
|
50
|
+
let lastListFingerprint = null;
|
|
51
|
+
|
|
52
|
+
function computeFingerprint(issues) {
|
|
53
|
+
const sorted = [...issues].sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
54
|
+
return JSON.stringify(
|
|
55
|
+
sorted.map((i) => ({
|
|
56
|
+
id: i.id,
|
|
57
|
+
status: i.status,
|
|
58
|
+
priority: i.priority,
|
|
59
|
+
title: i.title,
|
|
60
|
+
updated_at: i.updated_at,
|
|
61
|
+
// dependency_count/dependent_count come free in `bd list` (no `bd show`).
|
|
62
|
+
// They catch dependency-edge changes (`bd dep add/remove`) that don't
|
|
63
|
+
// bump an issue's own updated_at — without them the fingerprint would
|
|
64
|
+
// bail and leave depends_on/blocked_by (and blocked badges) stale.
|
|
65
|
+
dependency_count: i.dependency_count,
|
|
66
|
+
dependent_count: i.dependent_count,
|
|
67
|
+
})),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function recordWalStat() {
|
|
72
|
+
try {
|
|
73
|
+
const s = statSync(beadsWalPath);
|
|
74
|
+
lastSelfReadWalStat = { mtimeMs: s.mtimeMs, size: s.size };
|
|
75
|
+
} catch {
|
|
76
|
+
lastSelfReadWalStat = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
37
79
|
|
|
38
80
|
function scheduleBeadsRefresh() {
|
|
39
81
|
if (BEADS_REFRESH_TIMER) clearTimeout(BEADS_REFRESH_TIMER);
|
|
40
|
-
BEADS_REFRESH_TIMER = setTimeout(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
82
|
+
BEADS_REFRESH_TIMER = setTimeout(runBeadsRefresh, BEADS_DEBOUNCE_MS);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function runBeadsRefresh() {
|
|
86
|
+
BEADS_REFRESH_TIMER = null;
|
|
87
|
+
if (refreshing) {
|
|
88
|
+
refreshPending = true;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
refreshing = true;
|
|
92
|
+
try {
|
|
93
|
+
const shallowIssues = await listIssuesShallow(beadsDbPath);
|
|
94
|
+
const fingerprint = computeFingerprint(shallowIssues);
|
|
95
|
+
if (fingerprint === lastListFingerprint) {
|
|
96
|
+
recordWalStat();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
lastListFingerprint = fingerprint;
|
|
100
|
+
|
|
101
|
+
const [issues, counts] = await Promise.all([
|
|
102
|
+
enrichIssuesWithDeps(shallowIssues, beadsDbPath),
|
|
103
|
+
countIssuesByRunLabel(beadsDbPath).catch(() => ({})),
|
|
104
|
+
]);
|
|
105
|
+
latestCounts = counts;
|
|
106
|
+
const payloadJson = JSON.stringify({ issues, counts });
|
|
107
|
+
if (payloadJson !== lastPayloadJson) {
|
|
50
108
|
lastPayloadJson = payloadJson;
|
|
51
109
|
broadcaster.broadcast(
|
|
52
110
|
'beads-update',
|
|
@@ -58,16 +116,17 @@ export function createBeadsWatcher({ worcaDir, broadcaster, projectId }) {
|
|
|
58
116
|
},
|
|
59
117
|
projectId,
|
|
60
118
|
);
|
|
61
|
-
try {
|
|
62
|
-
const s = statSync(beadsWalPath);
|
|
63
|
-
lastSelfReadWalStat = { mtimeMs: s.mtimeMs, size: s.size };
|
|
64
|
-
} catch {
|
|
65
|
-
lastSelfReadWalStat = null;
|
|
66
|
-
}
|
|
67
|
-
} catch {
|
|
68
|
-
/* ignore */
|
|
69
119
|
}
|
|
70
|
-
|
|
120
|
+
recordWalStat();
|
|
121
|
+
} catch {
|
|
122
|
+
/* ignore */
|
|
123
|
+
} finally {
|
|
124
|
+
refreshing = false;
|
|
125
|
+
if (refreshPending) {
|
|
126
|
+
refreshPending = false;
|
|
127
|
+
scheduleBeadsRefresh();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
71
130
|
}
|
|
72
131
|
|
|
73
132
|
if (existsSync(beadsDir)) {
|
|
@@ -116,7 +175,11 @@ export function createBeadsWatcher({ worcaDir, broadcaster, projectId }) {
|
|
|
116
175
|
return { getBeadsDbPath, getLatestCounts, destroy };
|
|
117
176
|
}
|
|
118
177
|
|
|
119
|
-
|
|
178
|
+
// Throttle window for the cold-path bead-count read (used by REST /runs and chat
|
|
179
|
+
// when no live watcher is warm). The read costs seconds on a large db, so a short
|
|
180
|
+
// TTL lets repeated /runs across many projects re-spawn `bd` too often. Counts are
|
|
181
|
+
// advisory and the live watcher keeps a viewed project fresh, so 30s is ample.
|
|
182
|
+
const FALLBACK_TTL_MS = 30000;
|
|
120
183
|
/** @type {Map<string, { ts: number, counts: object }>} */
|
|
121
184
|
const fallbackCache = new Map();
|
|
122
185
|
/** @type {Map<string, Promise<object>>} */
|
|
@@ -163,3 +226,50 @@ export async function resolveBeadsCounts(wset) {
|
|
|
163
226
|
fallbackInflight.set(key, promise);
|
|
164
227
|
return promise;
|
|
165
228
|
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Non-blocking variant of {@link resolveBeadsCounts}: returns whatever counts
|
|
232
|
+
* are already available (the live watcher cache, or a fresh TTL-cached fallback)
|
|
233
|
+
* WITHOUT ever awaiting the cold `bd` query. When nothing is cached it kicks off
|
|
234
|
+
* a background refresh (reusing the same TTL cache + in-flight dedup) so a later
|
|
235
|
+
* caller gets real data, and returns {} immediately.
|
|
236
|
+
*
|
|
237
|
+
* Used by the REST `/runs` endpoint: a cold `bd show` on a large beads db
|
|
238
|
+
* (worca-cc has hundreds of issues — the cold read takes ~10s) would otherwise
|
|
239
|
+
* block the run list and, fanned out across every project, stall the whole UI.
|
|
240
|
+
* Bead counts on run cards are advisory and arrive via the `beads-update`
|
|
241
|
+
* broadcast once warm; the web run-detail does not use them at all.
|
|
242
|
+
*
|
|
243
|
+
* @param {{ projectId?: string, worcaDir?: string, beadsWatcher?: { getLatestCounts: () => object } | null }} [wset]
|
|
244
|
+
* @returns {Record<string, { total: number, done: number }>}
|
|
245
|
+
*/
|
|
246
|
+
export function peekBeadsCounts(wset) {
|
|
247
|
+
if (!wset || !wset.worcaDir) return {};
|
|
248
|
+
|
|
249
|
+
const live = wset.beadsWatcher?.getLatestCounts();
|
|
250
|
+
if (live && Object.keys(live).length > 0) return live;
|
|
251
|
+
|
|
252
|
+
const key = wset.projectId ?? wset.worcaDir;
|
|
253
|
+
const cached = fallbackCache.get(key);
|
|
254
|
+
if (cached && Date.now() - cached.ts < FALLBACK_TTL_MS) return cached.counts;
|
|
255
|
+
|
|
256
|
+
// Cold cache: warm it in the background (dedup'd), but never block the caller.
|
|
257
|
+
if (!fallbackInflight.has(key)) {
|
|
258
|
+
const dbPath = beadsDbPathFor(wset.worcaDir);
|
|
259
|
+
if (existsSync(dbPath)) {
|
|
260
|
+
const promise = (async () => {
|
|
261
|
+
try {
|
|
262
|
+
const counts = await countIssuesByRunLabel(dbPath);
|
|
263
|
+
fallbackCache.set(key, { ts: Date.now(), counts });
|
|
264
|
+
return counts;
|
|
265
|
+
} catch {
|
|
266
|
+
return {};
|
|
267
|
+
} finally {
|
|
268
|
+
fallbackInflight.delete(key);
|
|
269
|
+
}
|
|
270
|
+
})();
|
|
271
|
+
fallbackInflight.set(key, promise);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return {};
|
|
275
|
+
}
|
package/server/ws-modular.js
CHANGED
|
@@ -13,7 +13,7 @@ import { fleetRunsDir, workspaceRunsDir } from './paths.js';
|
|
|
13
13
|
import { readProjects, synthesizeDefaultProject } from './project-registry.js';
|
|
14
14
|
import { TIER_FULL, TIER_POLLING, WatcherSet } from './watcher-set.js';
|
|
15
15
|
import { readProjectWorcaVersion } from './worca-setup.js';
|
|
16
|
-
import {
|
|
16
|
+
import { peekBeadsCounts } from './ws-beads-watcher.js';
|
|
17
17
|
import { createBroadcaster } from './ws-broadcaster.js';
|
|
18
18
|
import { createClientManager } from './ws-client-manager.js';
|
|
19
19
|
import { createFleetManifestWatcher } from './ws-fleet-manifest-watcher.js';
|
|
@@ -331,8 +331,10 @@ export function attachWsServer(httpServer, config) {
|
|
|
331
331
|
return null;
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
// Non-blocking: returns cached/live counts and warms cold caches in the
|
|
335
|
+
// background. The REST /runs endpoint must never block on a slow `bd` read.
|
|
334
336
|
function getBeadsCounts(projectId) {
|
|
335
|
-
return
|
|
337
|
+
return peekBeadsCounts(watcherSets.get(projectId));
|
|
336
338
|
}
|
|
337
339
|
|
|
338
340
|
return {
|