@worca/ui 0.1.1 → 0.2.0-rc.2

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
@@ -1735,10 +1735,20 @@ sl-details.log-history-panel::part(content) {
1735
1735
  }
1736
1736
 
1737
1737
  .pricing-table sl-input {
1738
- min-width: 90px;
1739
- max-width: 130px;
1738
+ min-width: 72px;
1739
+ max-width: 104px;
1740
+ display: inline-block;
1741
+ }
1742
+
1743
+ sl-input.pricing-input::part(input) {
1744
+ text-align: right;
1740
1745
  }
1741
1746
 
1747
+ .pricing-table--auto {
1748
+ width: auto;
1749
+ }
1750
+
1751
+
1742
1752
  .pricing-info {
1743
1753
  display: flex;
1744
1754
  gap: 16px;
@@ -3160,6 +3170,19 @@ sl-details.learnings-panel::part(content) {
3160
3170
  color: var(--muted);
3161
3171
  }
3162
3172
 
3173
+ .cost-badge {
3174
+ display: inline-flex;
3175
+ align-items: center;
3176
+ gap: 2px;
3177
+ font-size: 0.7rem;
3178
+ padding: 1px 4px;
3179
+ border-radius: 4px;
3180
+ background: var(--bg-tertiary);
3181
+ color: var(--muted);
3182
+ margin-left: 4px;
3183
+ vertical-align: middle;
3184
+ }
3185
+
3163
3186
  /* Preflight checks view */
3164
3187
 
3165
3188
  .preflight-checks-view {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@worca/ui",
3
- "version": "0.1.1",
3
+ "version": "0.2.0-rc.2",
4
4
  "description": "Pipeline monitoring UI for worca-cc",
5
5
  "license": "MIT",
6
6
  "author": "Sinisha Djukic",
@@ -1,13 +1,15 @@
1
- import { execFileSync } from 'node:child_process';
1
+ import { execFile } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
+ import { promisify } from 'node:util';
3
4
 
4
- function runBd(args, dbPath) {
5
+ const execFileAsync = promisify(execFile);
6
+
7
+ async function runBd(args, dbPath) {
5
8
  const fullArgs = [...args, '--json', '--db', dbPath, '--readonly'];
6
- const stdout = execFileSync('bd', fullArgs, {
9
+ const { stdout } = await execFileAsync('bd', fullArgs, {
7
10
  encoding: 'utf8',
8
11
  timeout: 10000,
9
12
  maxBuffer: 10 * 1024 * 1024,
10
- stdio: ['ignore', 'pipe', 'pipe'],
11
13
  });
12
14
  return JSON.parse(stdout);
13
15
  }
@@ -30,12 +32,12 @@ function transformIssue(issue, deps) {
30
32
  };
31
33
  }
32
34
 
33
- function enrichWithDeps(issues, dbPath) {
35
+ async function enrichWithDeps(issues, dbPath) {
34
36
  const needDeps = issues.filter((i) => i.dependency_count > 0);
35
37
  if (needDeps.length === 0) {
36
38
  return issues.map((i) => transformIssue(i, []));
37
39
  }
38
- const detailed = runBd(['show', ...needDeps.map((i) => i.id)], dbPath);
40
+ const detailed = await runBd(['show', ...needDeps.map((i) => i.id)], dbPath);
39
41
  const depMap = new Map(detailed.map((d) => [d.id, d.dependencies || []]));
40
42
  return issues.map((i) => transformIssue(i, depMap.get(i.id) || []));
41
43
  }
@@ -44,18 +46,18 @@ export function dbExists(beadsDb) {
44
46
  return existsSync(beadsDb);
45
47
  }
46
48
 
47
- export function listIssues(beadsDb) {
49
+ export async function listIssues(beadsDb) {
48
50
  try {
49
- const issues = runBd(['list', '--limit', '0'], beadsDb);
51
+ const issues = await runBd(['list', '--limit', '0'], beadsDb);
50
52
  return enrichWithDeps(issues, beadsDb);
51
53
  } catch {
52
54
  return [];
53
55
  }
54
56
  }
55
57
 
56
- export function listIssuesByLabel(beadsDb, label) {
58
+ export async function listIssuesByLabel(beadsDb, label) {
57
59
  try {
58
- const issues = runBd(
60
+ const issues = await runBd(
59
61
  ['list', '--label-any', label, '--all', '--limit', '0'],
60
62
  beadsDb,
61
63
  );
@@ -65,12 +67,12 @@ export function listIssuesByLabel(beadsDb, label) {
65
67
  }
66
68
  }
67
69
 
68
- export function listUnlinkedIssues(beadsDb) {
70
+ export async function listUnlinkedIssues(beadsDb) {
69
71
  try {
70
- const issues = runBd(['list', '--limit', '0'], beadsDb);
72
+ const issues = await runBd(['list', '--limit', '0'], beadsDb);
71
73
  if (issues.length === 0) return [];
72
74
  // bd list doesn't include labels — use bd show to get them
73
- const detailed = runBd(['show', ...issues.map((i) => i.id)], beadsDb);
75
+ const detailed = await runBd(['show', ...issues.map((i) => i.id)], beadsDb);
74
76
  const detailMap = new Map(detailed.map((d) => [d.id, d]));
75
77
  const unlinked = issues.filter((i) => {
76
78
  const d = detailMap.get(i.id);
@@ -87,9 +89,9 @@ export function listUnlinkedIssues(beadsDb) {
87
89
  }
88
90
  }
89
91
 
90
- export function countIssuesByRunLabel(beadsDb) {
92
+ export async function countIssuesByRunLabel(beadsDb) {
91
93
  try {
92
- const rows = runBd(['label', 'list-all'], beadsDb);
94
+ const rows = await runBd(['label', 'list-all'], beadsDb);
93
95
  const counts = {};
94
96
  for (const row of rows) {
95
97
  if (row.label.startsWith('run:')) {
@@ -102,18 +104,18 @@ export function countIssuesByRunLabel(beadsDb) {
102
104
  }
103
105
  }
104
106
 
105
- export function listDistinctRunLabels(beadsDb) {
107
+ export async function listDistinctRunLabels(beadsDb) {
106
108
  try {
107
- const rows = runBd(['label', 'list-all'], beadsDb);
109
+ const rows = await runBd(['label', 'list-all'], beadsDb);
108
110
  return rows.filter((r) => r.label.startsWith('run:')).map((r) => r.label);
109
111
  } catch {
110
112
  return [];
111
113
  }
112
114
  }
113
115
 
114
- export function getIssue(beadsDb, id) {
116
+ export async function getIssue(beadsDb, id) {
115
117
  try {
116
- const results = runBd(['show', id], beadsDb);
118
+ const results = await runBd(['show', id], beadsDb);
117
119
  if (!results || results.length === 0) return null;
118
120
  const issue = results[0];
119
121
  return transformIssue(issue, issue.dependencies || []);
@@ -1174,76 +1174,43 @@ export function createProjectScopedRoutes() {
1174
1174
  });
1175
1175
 
1176
1176
  // GET /api/projects/:projectId/costs — token & cost data
1177
+ // Reads per-iteration token_usage from each run's status.json.
1177
1178
  router.get('/costs', requireWorcaDir, (req, res) => {
1178
1179
  const { worcaDir } = req.project;
1179
- const resultsDir = join(worcaDir, 'results');
1180
- if (!existsSync(resultsDir)) return res.json({ ok: true, tokenData: {} });
1181
-
1180
+ const runs = discoverRuns(worcaDir);
1182
1181
  const tokenData = {};
1183
1182
 
1184
- for (const entry of readdirSync(resultsDir, { withFileTypes: true })) {
1185
- if (!entry.isDirectory()) continue;
1186
- const runDir = join(resultsDir, entry.name);
1187
- const stageNames = [];
1188
- try {
1189
- for (const sub of readdirSync(runDir, { withFileTypes: true })) {
1190
- if (sub.isDirectory()) stageNames.push(sub.name);
1191
- }
1192
- } catch {
1193
- continue;
1194
- }
1195
-
1196
- if (stageNames.length === 0) continue;
1197
- tokenData[entry.name] = {};
1183
+ for (const run of runs) {
1184
+ const stages = run.stages || {};
1185
+ const runEntry = {};
1198
1186
 
1199
- for (const stage of stageNames) {
1200
- const stageDir = join(runDir, stage);
1187
+ for (const [stageName, stage] of Object.entries(stages)) {
1188
+ const iterations = stage.iterations || [];
1201
1189
  const iters = [];
1202
- try {
1203
- const files = readdirSync(stageDir)
1204
- .filter((f) => f.startsWith('iter-') && f.endsWith('.json'))
1205
- .sort();
1206
- for (const file of files) {
1207
- try {
1208
- const data = JSON.parse(
1209
- readFileSync(join(stageDir, file), 'utf8'),
1210
- );
1211
- const mu = data.modelUsage || {};
1212
- let inputTokens = 0,
1213
- outputTokens = 0,
1214
- cacheReadInputTokens = 0,
1215
- cacheCreationInputTokens = 0;
1216
- const models = [];
1217
- for (const [model, usage] of Object.entries(mu)) {
1218
- inputTokens += usage.inputTokens || 0;
1219
- outputTokens += usage.outputTokens || 0;
1220
- cacheReadInputTokens += usage.cacheReadInputTokens || 0;
1221
- cacheCreationInputTokens += usage.cacheCreationInputTokens || 0;
1222
- models.push(model);
1223
- }
1224
- iters.push({
1225
- inputTokens,
1226
- outputTokens,
1227
- cacheReadInputTokens,
1228
- cacheCreationInputTokens,
1229
- models,
1230
- });
1231
- } catch {
1232
- /* skip bad files */
1233
- }
1234
- }
1235
- } catch {
1236
- /* skip */
1190
+ for (const iter of iterations) {
1191
+ const tu = iter.token_usage || {};
1192
+ iters.push({
1193
+ inputTokens: tu.input_tokens || 0,
1194
+ outputTokens: tu.output_tokens || 0,
1195
+ cacheReadInputTokens: tu.cache_read_input_tokens || 0,
1196
+ cacheCreationInputTokens: tu.cache_creation_input_tokens || 0,
1197
+ webSearchRequests: tu.web_search_requests || 0,
1198
+ cacheEphemeral1hTokens: tu.cache_ephemeral_1h_tokens || 0,
1199
+ cacheEphemeral5mTokens: tu.cache_ephemeral_5m_tokens || 0,
1200
+ models: tu.model ? [tu.model] : [],
1201
+ });
1237
1202
  }
1238
- if (iters.length > 0) tokenData[entry.name][stage] = iters;
1203
+ if (iters.length > 0) runEntry[stageName] = iters;
1239
1204
  }
1205
+
1206
+ if (Object.keys(runEntry).length > 0) tokenData[run.id] = runEntry;
1240
1207
  }
1241
1208
 
1242
1209
  res.json({ ok: true, tokenData });
1243
1210
  });
1244
1211
 
1245
1212
  // ─── Beads (project-scoped) ─────────────────────────────────────────
1246
- router.get('/beads/issues', requireWorcaDir, (req, res) => {
1213
+ router.get('/beads/issues', requireWorcaDir, async (req, res) => {
1247
1214
  const beadsDbPath = join(req.project.worcaDir, '..', '.beads', 'beads.db');
1248
1215
  if (!dbExists(beadsDbPath)) {
1249
1216
  return res.json({
@@ -1254,7 +1221,7 @@ export function createProjectScopedRoutes() {
1254
1221
  });
1255
1222
  }
1256
1223
  try {
1257
- const issues = listIssues(beadsDbPath);
1224
+ const issues = await listIssues(beadsDbPath);
1258
1225
  res.json({ ok: true, issues, dbExists: true, dbPath: beadsDbPath });
1259
1226
  } catch (err) {
1260
1227
  res.status(500).json({ ok: false, error: err.message });
@@ -1269,7 +1236,7 @@ export function createProjectScopedRoutes() {
1269
1236
  .json({ ok: false, error: 'Issue ID must be a positive integer' });
1270
1237
  }
1271
1238
  const beadsDbPath = join(req.project.worcaDir, '..', '.beads', 'beads.db');
1272
- const issue = getIssue(beadsDbPath, issueId);
1239
+ const issue = await getIssue(beadsDbPath, issueId);
1273
1240
  if (!issue) {
1274
1241
  return res
1275
1242
  .status(404)
@@ -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 { dirname, join } from 'node:path';
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 from own package.json.
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 pkg = JSON.parse(
182
- readFileSync(join(__dirname, '..', 'package.json'), 'utf8'),
183
- );
184
- return pkg.version || null;
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
  }
package/server/watcher.js CHANGED
@@ -26,22 +26,9 @@ function isTerminal(status) {
26
26
  );
27
27
  }
28
28
 
29
- function isPipelineRunning(worcaDir) {
30
- const pidPath = join(worcaDir, 'pipeline.pid');
31
- if (!existsSync(pidPath)) return false;
32
- try {
33
- const pid = parseInt(readFileSync(pidPath, 'utf8').trim(), 10);
34
- process.kill(pid, 0); // signal 0 = check if alive
35
- return true;
36
- } catch {
37
- return false; // stale PID or unreadable
38
- }
39
- }
40
-
41
29
  export function discoverRuns(worcaDir) {
42
30
  const runs = [];
43
31
  const seenIds = new Set();
44
- const pipelineRunning = isPipelineRunning(worcaDir);
45
32
 
46
33
  // 1. Check active_run pointer for the current run
47
34
  const activeRunPath = join(worcaDir, 'active_run');
@@ -51,7 +38,8 @@ export function discoverRuns(worcaDir) {
51
38
  const candidate = join(worcaDir, 'runs', activeId, 'status.json');
52
39
  if (existsSync(candidate)) {
53
40
  const status = JSON.parse(readFileSync(candidate, 'utf8'));
54
- const active = !isTerminal(status) && pipelineRunning;
41
+ const active =
42
+ !isTerminal(status) && status.pipeline_status === 'running';
55
43
  const id = createRunId(status);
56
44
  runs.push({ id, active, ...status });
57
45
  seenIds.add(id);
@@ -88,7 +76,8 @@ export function discoverRuns(worcaDir) {
88
76
  const status = JSON.parse(readFileSync(statusPath, 'utf8'));
89
77
  const id = createRunId(status);
90
78
  if (!seenIds.has(id)) {
91
- const active = !isTerminal(status) && pipelineRunning;
79
+ const active =
80
+ !isTerminal(status) && status.pipeline_status === 'running';
92
81
  runs.push({ id, active, ...status });
93
82
  seenIds.add(id);
94
83
  }
@@ -142,7 +131,6 @@ export function discoverRuns(worcaDir) {
142
131
  export async function discoverRunsAsync(worcaDir) {
143
132
  const runs = [];
144
133
  const seenIds = new Set();
145
- const pipelineRunning = isPipelineRunning(worcaDir); // cheap check (one stat + one kill)
146
134
 
147
135
  // 1. Active run
148
136
  const activeRunPath = join(worcaDir, 'active_run');
@@ -150,7 +138,7 @@ export async function discoverRunsAsync(worcaDir) {
150
138
  const activeId = (await readFile(activeRunPath, 'utf8')).trim();
151
139
  const candidate = join(worcaDir, 'runs', activeId, 'status.json');
152
140
  const status = JSON.parse(await readFile(candidate, 'utf8'));
153
- const active = !isTerminal(status) && pipelineRunning;
141
+ const active = !isTerminal(status) && status.pipeline_status === 'running';
154
142
  const id = createRunId(status);
155
143
  runs.push({ id, active, ...status });
156
144
  seenIds.add(id);
@@ -191,7 +179,8 @@ export async function discoverRunsAsync(worcaDir) {
191
179
  );
192
180
  const id = createRunId(status);
193
181
  if (!seenIds.has(id)) {
194
- const active = !isTerminal(status) && pipelineRunning;
182
+ const active =
183
+ !isTerminal(status) && status.pipeline_status === 'running';
195
184
  runs.push({ id, active, ...status });
196
185
  seenIds.add(id);
197
186
  }
@@ -21,10 +21,10 @@ export function createBeadsWatcher({ worcaDir, broadcaster, projectId }) {
21
21
 
22
22
  function scheduleBeadsRefresh() {
23
23
  if (BEADS_REFRESH_TIMER) clearTimeout(BEADS_REFRESH_TIMER);
24
- BEADS_REFRESH_TIMER = setTimeout(() => {
24
+ BEADS_REFRESH_TIMER = setTimeout(async () => {
25
25
  BEADS_REFRESH_TIMER = null;
26
26
  try {
27
- const issues = listIssues(beadsDbPath);
27
+ const issues = await listIssues(beadsDbPath);
28
28
  broadcaster.broadcast(
29
29
  'beads-update',
30
30
  {
@@ -531,7 +531,7 @@ export function createMessageRouter({
531
531
  );
532
532
  return;
533
533
  }
534
- const issues = listIssues(beadsDbPath);
534
+ const issues = await listIssues(beadsDbPath);
535
535
  ws.send(
536
536
  JSON.stringify(
537
537
  makeOk(req, { issues, dbExists: true, dbPath: beadsDbPath }),
@@ -552,7 +552,7 @@ export function createMessageRouter({
552
552
  ws.send(JSON.stringify(makeOk(req, { issues: [], dbExists: false })));
553
553
  return;
554
554
  }
555
- const issues = listUnlinkedIssues(beadsDbPath);
555
+ const issues = await listUnlinkedIssues(beadsDbPath);
556
556
  ws.send(JSON.stringify(makeOk(req, { issues, dbExists: true })));
557
557
  return;
558
558
  }
@@ -569,7 +569,7 @@ export function createMessageRouter({
569
569
  ws.send(JSON.stringify(makeOk(req, { refs: [] })));
570
570
  return;
571
571
  }
572
- const refs = listDistinctRunLabels(beadsDbPath);
572
+ const refs = await listDistinctRunLabels(beadsDbPath);
573
573
  ws.send(JSON.stringify(makeOk(req, { refs })));
574
574
  return;
575
575
  }
@@ -586,7 +586,7 @@ export function createMessageRouter({
586
586
  ws.send(JSON.stringify(makeOk(req, { counts: {} })));
587
587
  return;
588
588
  }
589
- const counts = countIssuesByRunLabel(beadsDbPath);
589
+ const counts = await countIssuesByRunLabel(beadsDbPath);
590
590
  ws.send(JSON.stringify(makeOk(req, { counts })));
591
591
  return;
592
592
  }
@@ -612,7 +612,7 @@ export function createMessageRouter({
612
612
  ws.send(JSON.stringify(makeOk(req, { issues: [], runId })));
613
613
  return;
614
614
  }
615
- const issues = listIssuesByLabel(beadsDbPath, `run:${runId}`);
615
+ const issues = await listIssuesByLabel(beadsDbPath, `run:${runId}`);
616
616
  ws.send(JSON.stringify(makeOk(req, { issues, runId })));
617
617
  return;
618
618
  }