@worca/ui 0.1.0-rc.6 → 0.1.1-rc.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.
- package/app/main.bundle.js +386 -343
- package/app/main.bundle.js.map +3 -3
- package/app/styles.css +13 -0
- package/package.json +1 -2
- package/server/beads-reader.js +70 -146
- package/server/project-routes.js +47 -10
- package/server/versions.js +11 -9
- package/server/ws-beads-watcher.js +1 -1
package/app/styles.css
CHANGED
|
@@ -3160,6 +3160,19 @@ sl-details.learnings-panel::part(content) {
|
|
|
3160
3160
|
color: var(--muted);
|
|
3161
3161
|
}
|
|
3162
3162
|
|
|
3163
|
+
.cost-badge {
|
|
3164
|
+
display: inline-flex;
|
|
3165
|
+
align-items: center;
|
|
3166
|
+
gap: 2px;
|
|
3167
|
+
font-size: 0.7rem;
|
|
3168
|
+
padding: 1px 4px;
|
|
3169
|
+
border-radius: 4px;
|
|
3170
|
+
background: var(--bg-tertiary);
|
|
3171
|
+
color: var(--muted);
|
|
3172
|
+
margin-left: 4px;
|
|
3173
|
+
vertical-align: middle;
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3163
3176
|
/* Preflight checks view */
|
|
3164
3177
|
|
|
3165
3178
|
.preflight-checks-view {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@worca/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1-rc.1",
|
|
4
4
|
"description": "Pipeline monitoring UI for worca-cc",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sinisha Djukic",
|
|
@@ -56,7 +56,6 @@
|
|
|
56
56
|
"@xterm/addon-fit": "^0.11.0",
|
|
57
57
|
"@xterm/addon-search": "^0.16.0",
|
|
58
58
|
"@xterm/xterm": "^6.0.0",
|
|
59
|
-
"better-sqlite3": "^12.6.2",
|
|
60
59
|
"express": "^5.2.1",
|
|
61
60
|
"lit-html": "^3.3.1",
|
|
62
61
|
"lucide": "^0.577.0",
|
package/server/beads-reader.js
CHANGED
|
@@ -1,199 +1,123 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
1
2
|
import { existsSync } from 'node:fs';
|
|
2
|
-
|
|
3
|
+
|
|
4
|
+
function runBd(args, dbPath) {
|
|
5
|
+
const fullArgs = [...args, '--json', '--db', dbPath, '--readonly'];
|
|
6
|
+
const stdout = execFileSync('bd', fullArgs, {
|
|
7
|
+
encoding: 'utf8',
|
|
8
|
+
timeout: 10000,
|
|
9
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
10
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
11
|
+
});
|
|
12
|
+
return JSON.parse(stdout);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function transformIssue(issue, deps) {
|
|
16
|
+
const depends_on = (deps || []).map((d) => d.id);
|
|
17
|
+
const blocked_by = (deps || [])
|
|
18
|
+
.filter((d) => d.status !== 'closed' && d.status !== 'tombstone')
|
|
19
|
+
.map((d) => d.id);
|
|
20
|
+
return {
|
|
21
|
+
id: issue.id,
|
|
22
|
+
title: issue.title,
|
|
23
|
+
body: issue.description || '',
|
|
24
|
+
status: issue.status,
|
|
25
|
+
priority: issue.priority,
|
|
26
|
+
created_at: issue.created_at || '',
|
|
27
|
+
external_ref: issue.external_ref || null,
|
|
28
|
+
depends_on,
|
|
29
|
+
blocked_by,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function enrichWithDeps(issues, dbPath) {
|
|
34
|
+
const needDeps = issues.filter((i) => i.dependency_count > 0);
|
|
35
|
+
if (needDeps.length === 0) {
|
|
36
|
+
return issues.map((i) => transformIssue(i, []));
|
|
37
|
+
}
|
|
38
|
+
const detailed = runBd(['show', ...needDeps.map((i) => i.id)], dbPath);
|
|
39
|
+
const depMap = new Map(detailed.map((d) => [d.id, d.dependencies || []]));
|
|
40
|
+
return issues.map((i) => transformIssue(i, depMap.get(i.id) || []));
|
|
41
|
+
}
|
|
3
42
|
|
|
4
43
|
export function dbExists(beadsDb) {
|
|
5
44
|
return existsSync(beadsDb);
|
|
6
45
|
}
|
|
7
46
|
|
|
8
47
|
export function listIssues(beadsDb) {
|
|
9
|
-
let db;
|
|
10
48
|
try {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.prepare(
|
|
14
|
-
`SELECT id, title, description AS body, status, priority, created_at, external_ref
|
|
15
|
-
FROM issues
|
|
16
|
-
WHERE status NOT IN ('closed','tombstone')
|
|
17
|
-
ORDER BY priority ASC, id ASC`,
|
|
18
|
-
)
|
|
19
|
-
.all();
|
|
20
|
-
|
|
21
|
-
const depStmt = db.prepare(
|
|
22
|
-
`SELECT depends_on_id FROM dependencies WHERE issue_id = ?`,
|
|
23
|
-
);
|
|
24
|
-
const statusMap = new Map(rows.map((r) => [r.id, r.status]));
|
|
25
|
-
|
|
26
|
-
return rows.map((row) => {
|
|
27
|
-
const depends_on = depStmt.all(row.id).map((d) => d.depends_on_id);
|
|
28
|
-
const blocked_by = depends_on.filter((depId) => statusMap.has(depId));
|
|
29
|
-
return { ...row, depends_on, blocked_by };
|
|
30
|
-
});
|
|
49
|
+
const issues = runBd(['list', '--limit', '0'], beadsDb);
|
|
50
|
+
return enrichWithDeps(issues, beadsDb);
|
|
31
51
|
} catch {
|
|
32
52
|
return [];
|
|
33
|
-
} finally {
|
|
34
|
-
try {
|
|
35
|
-
db?.close();
|
|
36
|
-
} catch {
|
|
37
|
-
/* ignore */
|
|
38
|
-
}
|
|
39
53
|
}
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
export function listIssuesByLabel(beadsDb, label) {
|
|
43
|
-
let db;
|
|
44
57
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`SELECT i.id, i.title, i.description AS body, i.status, i.priority, i.created_at
|
|
49
|
-
FROM issues i
|
|
50
|
-
JOIN labels l ON l.issue_id = i.id
|
|
51
|
-
WHERE l.label = ?
|
|
52
|
-
ORDER BY i.priority ASC, i.id ASC`,
|
|
53
|
-
)
|
|
54
|
-
.all(label);
|
|
55
|
-
|
|
56
|
-
const depStmt = db.prepare(
|
|
57
|
-
`SELECT depends_on_id FROM dependencies WHERE issue_id = ?`,
|
|
58
|
+
const issues = runBd(
|
|
59
|
+
['list', '--label-any', label, '--all', '--limit', '0'],
|
|
60
|
+
beadsDb,
|
|
58
61
|
);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return rows.map((row) => {
|
|
62
|
-
const depends_on = depStmt.all(row.id).map((d) => d.depends_on_id);
|
|
63
|
-
const blocked_by = depends_on.filter((depId) => {
|
|
64
|
-
const s = statusMap.get(depId);
|
|
65
|
-
return s && s !== 'closed';
|
|
66
|
-
});
|
|
67
|
-
return { ...row, depends_on, blocked_by };
|
|
68
|
-
});
|
|
62
|
+
return enrichWithDeps(issues, beadsDb);
|
|
69
63
|
} catch {
|
|
70
64
|
return [];
|
|
71
|
-
} finally {
|
|
72
|
-
try {
|
|
73
|
-
db?.close();
|
|
74
|
-
} catch {
|
|
75
|
-
/* ignore */
|
|
76
|
-
}
|
|
77
65
|
}
|
|
78
66
|
}
|
|
79
67
|
|
|
80
68
|
export function listUnlinkedIssues(beadsDb) {
|
|
81
|
-
let db;
|
|
82
69
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
`SELECT depends_on_id FROM dependencies WHERE issue_id = ?`,
|
|
98
|
-
);
|
|
99
|
-
const statusMap = new Map(rows.map((r) => [r.id, r.status]));
|
|
100
|
-
|
|
101
|
-
return rows.map((row) => {
|
|
102
|
-
const depends_on = depStmt.all(row.id).map((d) => d.depends_on_id);
|
|
103
|
-
const blocked_by = depends_on.filter((depId) => statusMap.has(depId));
|
|
104
|
-
return { ...row, depends_on, blocked_by };
|
|
70
|
+
const issues = runBd(['list', '--limit', '0'], beadsDb);
|
|
71
|
+
if (issues.length === 0) return [];
|
|
72
|
+
// bd list doesn't include labels — use bd show to get them
|
|
73
|
+
const detailed = runBd(['show', ...issues.map((i) => i.id)], beadsDb);
|
|
74
|
+
const detailMap = new Map(detailed.map((d) => [d.id, d]));
|
|
75
|
+
const unlinked = issues.filter((i) => {
|
|
76
|
+
const d = detailMap.get(i.id);
|
|
77
|
+
const labels = d?.labels || [];
|
|
78
|
+
return !labels.some((l) => l.startsWith('run:'));
|
|
79
|
+
});
|
|
80
|
+
// detailed already has dependencies, use them directly
|
|
81
|
+
return unlinked.map((i) => {
|
|
82
|
+
const d = detailMap.get(i.id);
|
|
83
|
+
return transformIssue(i, d?.dependencies || []);
|
|
105
84
|
});
|
|
106
85
|
} catch {
|
|
107
86
|
return [];
|
|
108
|
-
} finally {
|
|
109
|
-
try {
|
|
110
|
-
db?.close();
|
|
111
|
-
} catch {
|
|
112
|
-
/* ignore */
|
|
113
|
-
}
|
|
114
87
|
}
|
|
115
88
|
}
|
|
116
89
|
|
|
117
90
|
export function countIssuesByRunLabel(beadsDb) {
|
|
118
|
-
let db;
|
|
119
91
|
try {
|
|
120
|
-
|
|
121
|
-
const rows = db
|
|
122
|
-
.prepare(
|
|
123
|
-
`SELECT l.label, COUNT(*) AS count FROM labels l
|
|
124
|
-
WHERE l.label LIKE 'run:%' GROUP BY l.label`,
|
|
125
|
-
)
|
|
126
|
-
.all();
|
|
92
|
+
const rows = runBd(['label', 'list-all'], beadsDb);
|
|
127
93
|
const counts = {};
|
|
128
94
|
for (const row of rows) {
|
|
129
|
-
|
|
130
|
-
|
|
95
|
+
if (row.label.startsWith('run:')) {
|
|
96
|
+
counts[row.label.replace('run:', '')] = row.count;
|
|
97
|
+
}
|
|
131
98
|
}
|
|
132
99
|
return counts;
|
|
133
100
|
} catch {
|
|
134
101
|
return {};
|
|
135
|
-
} finally {
|
|
136
|
-
try {
|
|
137
|
-
db?.close();
|
|
138
|
-
} catch {
|
|
139
|
-
/* ignore */
|
|
140
|
-
}
|
|
141
102
|
}
|
|
142
103
|
}
|
|
143
104
|
|
|
144
105
|
export function listDistinctRunLabels(beadsDb) {
|
|
145
|
-
let db;
|
|
146
106
|
try {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
.prepare(`SELECT DISTINCT label FROM labels WHERE label LIKE 'run:%'`)
|
|
150
|
-
.all();
|
|
151
|
-
return rows.map((r) => r.label);
|
|
107
|
+
const rows = runBd(['label', 'list-all'], beadsDb);
|
|
108
|
+
return rows.filter((r) => r.label.startsWith('run:')).map((r) => r.label);
|
|
152
109
|
} catch {
|
|
153
110
|
return [];
|
|
154
|
-
} finally {
|
|
155
|
-
try {
|
|
156
|
-
db?.close();
|
|
157
|
-
} catch {
|
|
158
|
-
/* ignore */
|
|
159
|
-
}
|
|
160
111
|
}
|
|
161
112
|
}
|
|
162
113
|
|
|
163
114
|
export function getIssue(beadsDb, id) {
|
|
164
|
-
let db;
|
|
165
115
|
try {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
FROM issues WHERE id = ?`,
|
|
171
|
-
)
|
|
172
|
-
.get(id);
|
|
173
|
-
if (!row) return null;
|
|
174
|
-
|
|
175
|
-
const depends_on = db
|
|
176
|
-
.prepare(`SELECT depends_on_id FROM dependencies WHERE issue_id = ?`)
|
|
177
|
-
.all(id)
|
|
178
|
-
.map((d) => d.depends_on_id);
|
|
179
|
-
|
|
180
|
-
const blocked_by = [];
|
|
181
|
-
for (const depId of depends_on) {
|
|
182
|
-
const dep = db
|
|
183
|
-
.prepare(`SELECT status FROM issues WHERE id = ?`)
|
|
184
|
-
.get(depId);
|
|
185
|
-
if (dep && dep.status !== 'closed' && dep.status !== 'tombstone') {
|
|
186
|
-
blocked_by.push(depId);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return { ...row, depends_on, blocked_by };
|
|
116
|
+
const results = runBd(['show', id], beadsDb);
|
|
117
|
+
if (!results || results.length === 0) return null;
|
|
118
|
+
const issue = results[0];
|
|
119
|
+
return transformIssue(issue, issue.dependencies || []);
|
|
190
120
|
} catch {
|
|
191
121
|
return null;
|
|
192
|
-
} finally {
|
|
193
|
-
try {
|
|
194
|
-
db?.close();
|
|
195
|
-
} catch {
|
|
196
|
-
/* ignore */
|
|
197
|
-
}
|
|
198
122
|
}
|
|
199
123
|
}
|
package/server/project-routes.js
CHANGED
|
@@ -676,21 +676,17 @@ export function createProjectScopedRoutes() {
|
|
|
676
676
|
res.json({ ok: true, stopped: true, runId, pid: result.pid });
|
|
677
677
|
} catch (err) {
|
|
678
678
|
if (err.code === 'not_running') {
|
|
679
|
-
const statusPath =
|
|
680
|
-
|
|
681
|
-
'runs',
|
|
682
|
-
runId,
|
|
683
|
-
'status.json',
|
|
684
|
-
);
|
|
685
|
-
if (existsSync(statusPath)) {
|
|
679
|
+
const statusPath = findRunStatusPath(req.project.worcaDir, runId);
|
|
680
|
+
if (statusPath) {
|
|
686
681
|
try {
|
|
687
682
|
const st = JSON.parse(readFileSync(statusPath, 'utf8'));
|
|
688
683
|
if (
|
|
689
684
|
st.pipeline_status === 'paused' ||
|
|
690
685
|
st.pipeline_status === 'running'
|
|
691
686
|
) {
|
|
692
|
-
st.pipeline_status = '
|
|
693
|
-
st.stop_reason = '
|
|
687
|
+
st.pipeline_status = 'cancelled';
|
|
688
|
+
st.stop_reason = 'force_cancelled';
|
|
689
|
+
st.completed_at = new Date().toISOString();
|
|
694
690
|
writeFileSync(
|
|
695
691
|
statusPath,
|
|
696
692
|
`${JSON.stringify(st, null, 2)}\n`,
|
|
@@ -710,6 +706,39 @@ export function createProjectScopedRoutes() {
|
|
|
710
706
|
}
|
|
711
707
|
});
|
|
712
708
|
|
|
709
|
+
// POST /api/projects/:projectId/runs/:id/cancel — force-cancel a stale run
|
|
710
|
+
router.post('/runs/:id/cancel', requireWorcaDir, (req, res) => {
|
|
711
|
+
const runId = req.params.id;
|
|
712
|
+
if (!validateRunId(runId)) {
|
|
713
|
+
return res.status(400).json({ ok: false, error: 'Invalid runId' });
|
|
714
|
+
}
|
|
715
|
+
const { worcaDir } = req.project;
|
|
716
|
+
const statusPath = findRunStatusPath(worcaDir, runId);
|
|
717
|
+
if (!statusPath) {
|
|
718
|
+
return res
|
|
719
|
+
.status(404)
|
|
720
|
+
.json({ ok: false, error: `Run "${runId}" not found` });
|
|
721
|
+
}
|
|
722
|
+
try {
|
|
723
|
+
const st = JSON.parse(readFileSync(statusPath, 'utf8'));
|
|
724
|
+
if (
|
|
725
|
+
st.pipeline_status === 'completed' ||
|
|
726
|
+
st.pipeline_status === 'cancelled'
|
|
727
|
+
) {
|
|
728
|
+
return res.json({ ok: true, already: st.pipeline_status });
|
|
729
|
+
}
|
|
730
|
+
st.pipeline_status = 'cancelled';
|
|
731
|
+
st.stop_reason = 'force_cancelled';
|
|
732
|
+
st.completed_at = new Date().toISOString();
|
|
733
|
+
writeFileSync(statusPath, `${JSON.stringify(st, null, 2)}\n`, 'utf8');
|
|
734
|
+
const { broadcast } = req.app.locals;
|
|
735
|
+
if (broadcast) broadcast('run-stopped', { runId, pid: null });
|
|
736
|
+
res.json({ ok: true, cancelled: true, runId });
|
|
737
|
+
} catch (err) {
|
|
738
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
|
|
713
742
|
// POST /api/projects/:projectId/runs/:id/archive
|
|
714
743
|
router.post('/runs/:id/archive', requireWorcaDir, (req, res) => {
|
|
715
744
|
const runId = req.params.id;
|
|
@@ -1183,20 +1212,28 @@ export function createProjectScopedRoutes() {
|
|
|
1183
1212
|
let inputTokens = 0,
|
|
1184
1213
|
outputTokens = 0,
|
|
1185
1214
|
cacheReadInputTokens = 0,
|
|
1186
|
-
cacheCreationInputTokens = 0
|
|
1215
|
+
cacheCreationInputTokens = 0,
|
|
1216
|
+
webSearchRequests = 0;
|
|
1187
1217
|
const models = [];
|
|
1188
1218
|
for (const [model, usage] of Object.entries(mu)) {
|
|
1189
1219
|
inputTokens += usage.inputTokens || 0;
|
|
1190
1220
|
outputTokens += usage.outputTokens || 0;
|
|
1191
1221
|
cacheReadInputTokens += usage.cacheReadInputTokens || 0;
|
|
1192
1222
|
cacheCreationInputTokens += usage.cacheCreationInputTokens || 0;
|
|
1223
|
+
webSearchRequests += usage.webSearchRequests || 0;
|
|
1193
1224
|
models.push(model);
|
|
1194
1225
|
}
|
|
1226
|
+
const cacheCreation = data.usage?.cache_creation || {};
|
|
1195
1227
|
iters.push({
|
|
1196
1228
|
inputTokens,
|
|
1197
1229
|
outputTokens,
|
|
1198
1230
|
cacheReadInputTokens,
|
|
1199
1231
|
cacheCreationInputTokens,
|
|
1232
|
+
webSearchRequests,
|
|
1233
|
+
cacheEphemeral1hTokens:
|
|
1234
|
+
cacheCreation.ephemeral_1h_input_tokens || 0,
|
|
1235
|
+
cacheEphemeral5mTokens:
|
|
1236
|
+
cacheCreation.ephemeral_5m_input_tokens || 0,
|
|
1200
1237
|
models,
|
|
1201
1238
|
});
|
|
1202
1239
|
} catch {
|
package/server/versions.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
// server/versions.js — version fetching + caching for worca-cc and @worca/ui
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
2
3
|
import { readFileSync } from 'node:fs';
|
|
3
|
-
import {
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
5
|
import { readPreferences } from './preferences.js';
|
|
6
6
|
|
|
7
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
|
|
9
7
|
/** Cache: { data, timestamp } */
|
|
10
8
|
let _cache = null;
|
|
11
9
|
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
@@ -173,15 +171,19 @@ export function getDevPathVersions(sourceRepo) {
|
|
|
173
171
|
}
|
|
174
172
|
|
|
175
173
|
/**
|
|
176
|
-
* Get installed @worca/ui version
|
|
174
|
+
* Get globally installed @worca/ui version via npm.
|
|
175
|
+
* Falls back to own package.json if npm query fails.
|
|
177
176
|
* @returns {string|null}
|
|
178
177
|
*/
|
|
179
178
|
function getInstalledUiVersion() {
|
|
180
179
|
try {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
const output = execFileSync('npm', ['list', '-g', '@worca/ui', '--json'], {
|
|
181
|
+
encoding: 'utf8',
|
|
182
|
+
timeout: 5000,
|
|
183
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
184
|
+
});
|
|
185
|
+
const data = JSON.parse(output);
|
|
186
|
+
return data.dependencies?.['@worca/ui']?.version || null;
|
|
185
187
|
} catch {
|
|
186
188
|
return null;
|
|
187
189
|
}
|
|
@@ -8,7 +8,7 @@ import { existsSync, watch } from 'node:fs';
|
|
|
8
8
|
import { join, resolve } from 'node:path';
|
|
9
9
|
import { listIssues } from './beads-reader.js';
|
|
10
10
|
|
|
11
|
-
const BEADS_DEBOUNCE_MS =
|
|
11
|
+
const BEADS_DEBOUNCE_MS = 300;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* @param {{ worcaDir: string, broadcaster: { broadcast: Function }, projectId?: string }} deps
|