@worca/ui 0.41.0 → 0.43.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/main.bundle.js +2625 -1971
- package/app/main.bundle.js.map +4 -4
- package/app/protocol.js +2 -0
- package/app/styles.css +1356 -6
- package/package.json +2 -2
- package/server/app.js +90 -0
- package/server/dispatch-defaults.js +5 -5
- package/server/dispatch-events-aggregator.js +27 -13
- package/server/dispatch-migration.js +35 -1
- package/server/events-jsonl-reader.js +93 -0
- package/server/file-access-aggregator.js +553 -0
- package/server/graph-query-aggregator.js +165 -0
- package/server/integrations/renderers.js +11 -0
- package/server/process-manager.js +86 -0
- package/server/project-routes.js +16 -3
- package/server/schemas/keys.json +5 -0
- package/server/template-prompts.js +136 -0
- package/server/templates-routes.js +303 -49
- package/server/watcher.js +122 -40
- package/server/ws-broadcaster.js +5 -4
- package/server/ws-message-router.js +23 -2
- package/server/ws-status-watcher.js +5 -1
package/server/watcher.js
CHANGED
|
@@ -2,25 +2,35 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { readdir, readFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from './dispatch-events-aggregator.js';
|
|
5
|
+
import { assignEventsToIterations } from './dispatch-events-aggregator.js';
|
|
6
|
+
import { readEventsForEnrichment } from './events-jsonl-reader.js';
|
|
7
|
+
import { assignGraphQueryCountsToIterations } from './graph-query-aggregator.js';
|
|
9
8
|
import { readPipelineOverlay } from './run-dir-resolver.js';
|
|
10
9
|
import { safeWatch } from './safe-watch.js';
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
|
-
* Enrich a status object
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* Enrich a status object from events.jsonl in the same run directory:
|
|
13
|
+
* - dispatch events → `dispatch_events` per iteration (skills/subagents badges)
|
|
14
|
+
* - graph-query events → live graphify_invocations / crg_invocations /
|
|
15
|
+
* crg_tool_counts for the still-running iteration (graphify/CRG badges),
|
|
16
|
+
* without clobbering the runner's authoritative completion-time counts.
|
|
17
|
+
* No-op when events.jsonl is missing (e.g. a run started before the emit was
|
|
18
|
+
* wired, or one with no dispatches / graph queries).
|
|
17
19
|
*/
|
|
18
20
|
function enrichWithDispatchEvents(status, runDir) {
|
|
19
21
|
if (!status?.stages) return status;
|
|
20
22
|
const eventsPath = join(runDir, 'events.jsonl');
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
// Single read for both event kinds (issue #296) — was two full passes.
|
|
24
|
+
const { dispatchEvents, graphEvents } = readEventsForEnrichment(eventsPath);
|
|
25
|
+
if (dispatchEvents.length > 0) {
|
|
26
|
+
status.stages = assignEventsToIterations(dispatchEvents, status.stages);
|
|
27
|
+
}
|
|
28
|
+
if (graphEvents.length > 0) {
|
|
29
|
+
status.stages = assignGraphQueryCountsToIterations(
|
|
30
|
+
graphEvents,
|
|
31
|
+
status.stages,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
24
34
|
return status;
|
|
25
35
|
}
|
|
26
36
|
|
|
@@ -47,7 +57,7 @@ function isTerminal(status) {
|
|
|
47
57
|
);
|
|
48
58
|
}
|
|
49
59
|
|
|
50
|
-
const _discoverRunsCache = new Map(); // worcaDir → { ts, runs }
|
|
60
|
+
const _discoverRunsCache = new Map(); // `${worcaDir}|${enrich}` → { ts, runs }
|
|
51
61
|
// TTL defaults to 0 under vitest (NODE_ENV=test) so the cache is a no-op in
|
|
52
62
|
// tests — they build fixture dirs from Date.now() and a shared path could
|
|
53
63
|
// otherwise serve a stale cached scan across tests. Production uses 1500ms;
|
|
@@ -67,14 +77,21 @@ export function _setDiscoverRunsTtlForTest(ms) {
|
|
|
67
77
|
* single scan. Per-run handlers use findRun() instead. Live status changes
|
|
68
78
|
* still reach clients via the statusWatcher broadcast, so TTL-window staleness
|
|
69
79
|
* here is invisible in the UI.
|
|
80
|
+
*
|
|
81
|
+
* `enrich` (default false) controls events.jsonl enrichment. List/sidebar
|
|
82
|
+
* callers never render dispatch_events / graph-query counts (only the detailed
|
|
83
|
+
* run view does, fed by findRun), so they pass enrich:false and skip reading
|
|
84
|
+
* every project's events.jsonl entirely — the hot-path fix for issue #296. The
|
|
85
|
+
* cache is keyed on (worcaDir, enrich) since the two shapes differ.
|
|
70
86
|
*/
|
|
71
|
-
export function discoverRuns(worcaDir) {
|
|
72
|
-
const
|
|
87
|
+
export function discoverRuns(worcaDir, { enrich = false } = {}) {
|
|
88
|
+
const cacheKey = `${worcaDir}|${enrich ? 1 : 0}`;
|
|
89
|
+
const cached = _discoverRunsCache.get(cacheKey);
|
|
73
90
|
if (cached && Date.now() - cached.ts < _discoverRunsTtlMs) {
|
|
74
91
|
return cached.runs;
|
|
75
92
|
}
|
|
76
|
-
const runs = _discoverRunsUncached(worcaDir);
|
|
77
|
-
_discoverRunsCache.set(
|
|
93
|
+
const runs = _discoverRunsUncached(worcaDir, { enrich });
|
|
94
|
+
_discoverRunsCache.set(cacheKey, { ts: Date.now(), runs });
|
|
78
95
|
return runs;
|
|
79
96
|
}
|
|
80
97
|
|
|
@@ -139,8 +156,12 @@ export function findRun(worcaDir, runId) {
|
|
|
139
156
|
|
|
140
157
|
// Fallback for legacy layouts where the on-disk name != the computed id
|
|
141
158
|
// (flat .worca/status.json, hashed legacy ids). Rare — pay one (TTL-cached)
|
|
142
|
-
// full scan rather than regress correctness vs discoverRuns().find().
|
|
143
|
-
|
|
159
|
+
// full scan rather than regress correctness vs discoverRuns().find(). Pass
|
|
160
|
+
// enrich:true so findRun stays fully enriched in every branch (its contract),
|
|
161
|
+
// even though discoverRuns now defaults enrichment off for the list path.
|
|
162
|
+
return (
|
|
163
|
+
discoverRuns(worcaDir, { enrich: true }).find((r) => r.id === runId) || null
|
|
164
|
+
);
|
|
144
165
|
}
|
|
145
166
|
|
|
146
167
|
function _shapeRunFromFile(
|
|
@@ -158,7 +179,13 @@ function _shapeRunFromFile(
|
|
|
158
179
|
if (enrich && runDir) status = enrichWithDispatchEvents(status, runDir);
|
|
159
180
|
const id = createRunId(status);
|
|
160
181
|
const active = !isTerminal(status) && status.pipeline_status === 'running';
|
|
161
|
-
const base = {
|
|
182
|
+
const base = {
|
|
183
|
+
id,
|
|
184
|
+
active,
|
|
185
|
+
...status,
|
|
186
|
+
source_type: status.source_type ?? null,
|
|
187
|
+
source_ref: status.source_ref ?? null,
|
|
188
|
+
};
|
|
162
189
|
if (worktreeReg) {
|
|
163
190
|
return {
|
|
164
191
|
...base,
|
|
@@ -177,7 +204,7 @@ function _shapeRunFromFile(
|
|
|
177
204
|
}
|
|
178
205
|
}
|
|
179
206
|
|
|
180
|
-
function _discoverRunsUncached(worcaDir) {
|
|
207
|
+
function _discoverRunsUncached(worcaDir, { enrich = false } = {}) {
|
|
181
208
|
const runs = [];
|
|
182
209
|
const seenIds = new Set();
|
|
183
210
|
|
|
@@ -190,13 +217,19 @@ function _discoverRunsUncached(worcaDir) {
|
|
|
190
217
|
if (!existsSync(statusPath)) continue;
|
|
191
218
|
try {
|
|
192
219
|
let status = JSON.parse(readFileSync(statusPath, 'utf8'));
|
|
193
|
-
status = enrichWithDispatchEvents(status, runDir);
|
|
220
|
+
if (enrich) status = enrichWithDispatchEvents(status, runDir);
|
|
194
221
|
const id = createRunId(status);
|
|
195
222
|
if (seenIds.has(id)) continue;
|
|
196
223
|
seenIds.add(id);
|
|
197
224
|
const active =
|
|
198
225
|
!isTerminal(status) && status.pipeline_status === 'running';
|
|
199
|
-
runs.push({
|
|
226
|
+
runs.push({
|
|
227
|
+
id,
|
|
228
|
+
active,
|
|
229
|
+
...status,
|
|
230
|
+
source_type: status.source_type ?? null,
|
|
231
|
+
source_ref: status.source_ref ?? null,
|
|
232
|
+
});
|
|
200
233
|
} catch {
|
|
201
234
|
/* ignore */
|
|
202
235
|
}
|
|
@@ -212,7 +245,13 @@ function _discoverRunsUncached(worcaDir) {
|
|
|
212
245
|
if (!seenIds.has(id)) {
|
|
213
246
|
const active =
|
|
214
247
|
!isTerminal(status) && status.pipeline_status === 'running';
|
|
215
|
-
runs.push({
|
|
248
|
+
runs.push({
|
|
249
|
+
id,
|
|
250
|
+
active,
|
|
251
|
+
...status,
|
|
252
|
+
source_type: status.source_type ?? null,
|
|
253
|
+
source_ref: status.source_ref ?? null,
|
|
254
|
+
});
|
|
216
255
|
seenIds.add(id);
|
|
217
256
|
}
|
|
218
257
|
} catch {
|
|
@@ -236,7 +275,13 @@ function _discoverRunsUncached(worcaDir) {
|
|
|
236
275
|
seenIds.add(id);
|
|
237
276
|
const active =
|
|
238
277
|
!isTerminal(data) && data.pipeline_status === 'running';
|
|
239
|
-
runs.push({
|
|
278
|
+
runs.push({
|
|
279
|
+
id,
|
|
280
|
+
active,
|
|
281
|
+
...data,
|
|
282
|
+
source_type: data.source_type ?? null,
|
|
283
|
+
source_ref: data.source_ref ?? null,
|
|
284
|
+
});
|
|
240
285
|
}
|
|
241
286
|
}
|
|
242
287
|
} else if (entry.isDirectory()) {
|
|
@@ -249,7 +294,13 @@ function _discoverRunsUncached(worcaDir) {
|
|
|
249
294
|
seenIds.add(id);
|
|
250
295
|
const active =
|
|
251
296
|
!isTerminal(data) && data.pipeline_status === 'running';
|
|
252
|
-
runs.push({
|
|
297
|
+
runs.push({
|
|
298
|
+
id,
|
|
299
|
+
active,
|
|
300
|
+
...data,
|
|
301
|
+
source_type: data.source_type ?? null,
|
|
302
|
+
source_ref: data.source_ref ?? null,
|
|
303
|
+
});
|
|
253
304
|
}
|
|
254
305
|
}
|
|
255
306
|
}
|
|
@@ -274,10 +325,12 @@ function _discoverRunsUncached(worcaDir) {
|
|
|
274
325
|
if (!existsSync(sp)) continue;
|
|
275
326
|
try {
|
|
276
327
|
let status = JSON.parse(readFileSync(sp, 'utf8'));
|
|
277
|
-
|
|
278
|
-
status
|
|
279
|
-
|
|
280
|
-
|
|
328
|
+
if (enrich) {
|
|
329
|
+
status = enrichWithDispatchEvents(
|
|
330
|
+
status,
|
|
331
|
+
join(wtRunsDir, runEntry),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
281
334
|
const id = createRunId(status);
|
|
282
335
|
if (seenIds.has(id)) continue;
|
|
283
336
|
seenIds.add(id);
|
|
@@ -287,6 +340,8 @@ function _discoverRunsUncached(worcaDir) {
|
|
|
287
340
|
id,
|
|
288
341
|
active,
|
|
289
342
|
...status,
|
|
343
|
+
source_type: status.source_type ?? null,
|
|
344
|
+
source_ref: status.source_ref ?? null,
|
|
290
345
|
worktree_worca_dir: join(reg.worktree_path, '.worca'),
|
|
291
346
|
is_worktree_run: true,
|
|
292
347
|
head_branch: reg.branch || null,
|
|
@@ -309,10 +364,13 @@ function _discoverRunsUncached(worcaDir) {
|
|
|
309
364
|
}
|
|
310
365
|
|
|
311
366
|
/**
|
|
312
|
-
* Async version of discoverRuns — avoids blocking the event loop.
|
|
313
|
-
*
|
|
367
|
+
* Async version of discoverRuns — avoids blocking the event loop. Used by the
|
|
368
|
+
* status watcher's debounced refresh (enrich:true, since it broadcasts
|
|
369
|
+
* run-snapshot to the detailed view's live update) and by the list-path REST /
|
|
370
|
+
* WS handlers (enrich:false — issue #296, keeps the all-projects scan off the
|
|
371
|
+
* event loop without reading every events.jsonl).
|
|
314
372
|
*/
|
|
315
|
-
export async function discoverRunsAsync(worcaDir) {
|
|
373
|
+
export async function discoverRunsAsync(worcaDir, { enrich = false } = {}) {
|
|
316
374
|
const runs = [];
|
|
317
375
|
const seenIds = new Set();
|
|
318
376
|
|
|
@@ -332,13 +390,21 @@ export async function discoverRunsAsync(worcaDir) {
|
|
|
332
390
|
});
|
|
333
391
|
for (const result of await Promise.all(readPromises)) {
|
|
334
392
|
if (!result) continue;
|
|
335
|
-
const status =
|
|
393
|
+
const status = enrich
|
|
394
|
+
? enrichWithDispatchEvents(result.status, result.runDir)
|
|
395
|
+
: result.status;
|
|
336
396
|
const id = createRunId(status);
|
|
337
397
|
if (seenIds.has(id)) continue;
|
|
338
398
|
seenIds.add(id);
|
|
339
399
|
const active =
|
|
340
400
|
!isTerminal(status) && status.pipeline_status === 'running';
|
|
341
|
-
runs.push({
|
|
401
|
+
runs.push({
|
|
402
|
+
id,
|
|
403
|
+
active,
|
|
404
|
+
...status,
|
|
405
|
+
source_type: status.source_type ?? null,
|
|
406
|
+
source_ref: status.source_ref ?? null,
|
|
407
|
+
});
|
|
342
408
|
}
|
|
343
409
|
} catch {
|
|
344
410
|
/* ignore */
|
|
@@ -353,7 +419,13 @@ export async function discoverRunsAsync(worcaDir) {
|
|
|
353
419
|
if (!seenIds.has(id)) {
|
|
354
420
|
const active =
|
|
355
421
|
!isTerminal(status) && status.pipeline_status === 'running';
|
|
356
|
-
runs.push({
|
|
422
|
+
runs.push({
|
|
423
|
+
id,
|
|
424
|
+
active,
|
|
425
|
+
...status,
|
|
426
|
+
source_type: status.source_type ?? null,
|
|
427
|
+
source_ref: status.source_ref ?? null,
|
|
428
|
+
});
|
|
357
429
|
seenIds.add(id);
|
|
358
430
|
}
|
|
359
431
|
} catch {
|
|
@@ -386,7 +458,13 @@ export async function discoverRunsAsync(worcaDir) {
|
|
|
386
458
|
if (!seenIds.has(id)) {
|
|
387
459
|
seenIds.add(id);
|
|
388
460
|
const active = !isTerminal(data) && data.pipeline_status === 'running';
|
|
389
|
-
runs.push({
|
|
461
|
+
runs.push({
|
|
462
|
+
id,
|
|
463
|
+
active,
|
|
464
|
+
...data,
|
|
465
|
+
source_type: data.source_type ?? null,
|
|
466
|
+
source_ref: data.source_ref ?? null,
|
|
467
|
+
});
|
|
390
468
|
}
|
|
391
469
|
}
|
|
392
470
|
} catch {
|
|
@@ -417,10 +495,12 @@ export async function discoverRunsAsync(worcaDir) {
|
|
|
417
495
|
try {
|
|
418
496
|
const sp = join(wtRunsDir, runEntry, 'status.json');
|
|
419
497
|
let status = JSON.parse(await readFile(sp, 'utf8'));
|
|
420
|
-
|
|
421
|
-
status
|
|
422
|
-
|
|
423
|
-
|
|
498
|
+
if (enrich) {
|
|
499
|
+
status = enrichWithDispatchEvents(
|
|
500
|
+
status,
|
|
501
|
+
join(wtRunsDir, runEntry),
|
|
502
|
+
);
|
|
503
|
+
}
|
|
424
504
|
results.push({ status, reg });
|
|
425
505
|
} catch {
|
|
426
506
|
/* ignore */
|
|
@@ -442,6 +522,8 @@ export async function discoverRunsAsync(worcaDir) {
|
|
|
442
522
|
id,
|
|
443
523
|
active,
|
|
444
524
|
...status,
|
|
525
|
+
source_type: status.source_type ?? null,
|
|
526
|
+
source_ref: status.source_ref ?? null,
|
|
445
527
|
worktree_worca_dir: join(reg.worktree_path, '.worca'),
|
|
446
528
|
is_worktree_run: true,
|
|
447
529
|
head_branch: reg.branch || null,
|
package/server/ws-broadcaster.js
CHANGED
|
@@ -14,10 +14,11 @@ export function createBroadcaster({ wss, getSubs }) {
|
|
|
14
14
|
* Build a message envelope. For protocol 2 clients with a projectId,
|
|
15
15
|
* a `project` field is added to the top-level message.
|
|
16
16
|
*/
|
|
17
|
-
function sendToClient(ws, baseMsg) {
|
|
17
|
+
function sendToClient(ws, baseMsg, sourceProjectId) {
|
|
18
18
|
const s = getSubs(ws);
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const project = sourceProjectId || s?.projectId || null;
|
|
20
|
+
if (s && s.protocolVersion >= 2 && project) {
|
|
21
|
+
ws.send(JSON.stringify({ ...baseMsg, project }));
|
|
21
22
|
} else {
|
|
22
23
|
ws.send(JSON.stringify(baseMsg));
|
|
23
24
|
}
|
|
@@ -44,7 +45,7 @@ export function createBroadcaster({ wss, getSubs }) {
|
|
|
44
45
|
)
|
|
45
46
|
continue;
|
|
46
47
|
}
|
|
47
|
-
sendToClient(ws, base);
|
|
48
|
+
sendToClient(ws, base, projectId);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
listIssuesByLabel,
|
|
18
18
|
listUnlinkedIssues,
|
|
19
19
|
} from './beads-reader.js';
|
|
20
|
+
import { buildFileAccessModel } from './file-access-aggregator.js';
|
|
20
21
|
import {
|
|
21
22
|
listIterationFiles,
|
|
22
23
|
listLogFiles,
|
|
@@ -33,7 +34,7 @@ import {
|
|
|
33
34
|
} from './process-manager.js';
|
|
34
35
|
import { resolveRunDir } from './run-dir-resolver.js';
|
|
35
36
|
import { readSettings } from './settings-reader.js';
|
|
36
|
-
import {
|
|
37
|
+
import { discoverRunsAsync, findRun } from './watcher.js';
|
|
37
38
|
import { resolveBeadsCounts } from './ws-beads-watcher.js';
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -142,7 +143,8 @@ export function createMessageRouter({
|
|
|
142
143
|
// list-runs
|
|
143
144
|
if (req.type === 'list-runs') {
|
|
144
145
|
const proj = resolveProject(ws, req.payload);
|
|
145
|
-
|
|
146
|
+
// List/sidebar path: async scan, no events.jsonl enrichment (issue #296).
|
|
147
|
+
const runs = await discoverRunsAsync(proj.worcaDir, { enrich: false });
|
|
146
148
|
const settings = readSettings(proj.settingsPath);
|
|
147
149
|
ws.send(JSON.stringify(makeOk(req, { runs, settings })));
|
|
148
150
|
return;
|
|
@@ -807,6 +809,25 @@ export function createMessageRouter({
|
|
|
807
809
|
return;
|
|
808
810
|
}
|
|
809
811
|
|
|
812
|
+
// get-file-access
|
|
813
|
+
if (req.type === 'get-file-access') {
|
|
814
|
+
const { runId } = req.payload || {};
|
|
815
|
+
if (typeof runId !== 'string') {
|
|
816
|
+
ws.send(
|
|
817
|
+
JSON.stringify(
|
|
818
|
+
makeError(req, 'bad_request', 'payload.runId required'),
|
|
819
|
+
),
|
|
820
|
+
);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const proj = resolveProject(ws, req.payload);
|
|
824
|
+
const runDir = resolveRunDir(proj.worcaDir, runId);
|
|
825
|
+
const eventsPath = runDir ? join(runDir, 'events.jsonl') : null;
|
|
826
|
+
const model = buildFileAccessModel(eventsPath, runDir);
|
|
827
|
+
ws.send(JSON.stringify(makeOk(req, { runId, ...model })));
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
|
|
810
831
|
// subscribe-events
|
|
811
832
|
if (req.type === 'subscribe-events') {
|
|
812
833
|
const { runId } = req.payload || {};
|
|
@@ -247,7 +247,11 @@ export function createStatusWatcher({
|
|
|
247
247
|
/* ignore */
|
|
248
248
|
}
|
|
249
249
|
try {
|
|
250
|
-
|
|
250
|
+
// enrich:true — run-snapshot broadcasts below feed the detailed run
|
|
251
|
+
// view's live update, which renders dispatch_events / graph-query counts
|
|
252
|
+
// for the still-running iteration (issue #296 keeps the list path lean,
|
|
253
|
+
// not this one).
|
|
254
|
+
const runs = await discoverRunsAsync(worcaDir, { enrich: true });
|
|
251
255
|
reconcileWorktreeWatchers();
|
|
252
256
|
const subscribedIds = new Set();
|
|
253
257
|
for (const ws of wss.clients) {
|