@worca/ui 0.35.0 → 0.36.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/styles.css CHANGED
@@ -2295,6 +2295,12 @@ sl-input.pricing-input::part(input) {
2295
2295
  font-size: 13px;
2296
2296
  }
2297
2297
 
2298
+ /* CRG invocation badge tooltip lists one tool per line (one <div> each, via
2299
+ the tooltip's HTML content slot) — just left-align them. */
2300
+ sl-tooltip.crg-tool-tooltip::part(body) {
2301
+ text-align: left;
2302
+ }
2303
+
2298
2304
  .iteration-tags-sep {
2299
2305
  color: var(--fg);
2300
2306
  opacity: 0.7;
@@ -6616,3 +6622,74 @@ sl-dialog.markdown-dialog::part(body) {
6616
6622
  .graphify-not-installed .settings-tab-description {
6617
6623
  color: var(--status-paused);
6618
6624
  }
6625
+
6626
+ /* Code Review Graph (CRG) — mirrors graphify-* classes above */
6627
+ .crg-codebox {
6628
+ display: block;
6629
+ margin: 0.25rem 0 0.75rem;
6630
+ padding: 0.4rem 0.6rem;
6631
+ background: var(--bg-secondary);
6632
+ border: 1px solid var(--border-subtle);
6633
+ border-radius: var(--radius);
6634
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
6635
+ font-size: 0.85rem;
6636
+ color: var(--fg);
6637
+ word-break: break-all;
6638
+ user-select: all;
6639
+ }
6640
+
6641
+ .crg-copy-row {
6642
+ display: flex;
6643
+ align-items: center;
6644
+ gap: 0.4rem;
6645
+ margin: 0.25rem 0 0.75rem;
6646
+ }
6647
+
6648
+ .crg-copy-row .crg-codebox {
6649
+ flex: 1;
6650
+ min-width: 0;
6651
+ margin: 0;
6652
+ }
6653
+
6654
+ .crg-not-installed {
6655
+ margin: 0 0 0.75rem;
6656
+ }
6657
+
6658
+ .crg-not-installed .settings-tab-description {
6659
+ color: var(--status-paused);
6660
+ }
6661
+
6662
+ .crg-coming-soon {
6663
+ color: var(--muted);
6664
+ font-style: italic;
6665
+ }
6666
+
6667
+ .crg-stage-tools {
6668
+ margin: 1rem 0;
6669
+ }
6670
+
6671
+ .crg-stage-tools-grid {
6672
+ display: grid;
6673
+ gap: 0.4rem;
6674
+ margin-top: 0.5rem;
6675
+ }
6676
+
6677
+ .crg-stage-tools-entry {
6678
+ display: flex;
6679
+ gap: 0.75rem;
6680
+ align-items: baseline;
6681
+ font-size: 0.85rem;
6682
+ }
6683
+
6684
+ .crg-stage-role {
6685
+ font-weight: 600;
6686
+ min-width: 6rem;
6687
+ color: var(--fg);
6688
+ }
6689
+
6690
+ .crg-stage-tool-list {
6691
+ color: var(--muted);
6692
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
6693
+ font-size: 0.8rem;
6694
+ word-break: break-all;
6695
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@worca/ui",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "description": "Pipeline monitoring UI for worca-cc",
5
5
  "license": "MIT",
6
6
  "author": "Sinisha Djukic",
package/server/app.js CHANGED
@@ -15,6 +15,10 @@ import { fileURLToPath } from 'node:url';
15
15
  import express from 'express';
16
16
 
17
17
  import { dbExists, getIssue, listIssues } from './beads-reader.js';
18
+ import {
19
+ _effectiveCrgConfig,
20
+ createCrgStatus,
21
+ } from './code-review-graph-status.js';
18
22
  import { createFleetRouter } from './fleet-routes.js';
19
23
  import {
20
24
  _effectiveConfig,
@@ -1165,6 +1169,150 @@ export function createApp(options = {}) {
1165
1169
  res.sendFile(htmlPath);
1166
1170
  });
1167
1171
 
1172
+ // ─── CRG (code-review-graph) endpoints ─────────────────────────────────
1173
+ if (!app.locals.crgStatus) {
1174
+ app.locals.crgStatus = createCrgStatus({});
1175
+ }
1176
+
1177
+ function readCrgSettings(projectId) {
1178
+ const readJson = (p) => {
1179
+ if (!p) return {};
1180
+ try {
1181
+ return JSON.parse(readFileSync(p, 'utf-8'));
1182
+ } catch {
1183
+ return {};
1184
+ }
1185
+ };
1186
+ const globalSettingsPath = prefsDir
1187
+ ? join(prefsDir, 'settings.json')
1188
+ : settingsPath;
1189
+
1190
+ let projectSettingsPath = settingsPath;
1191
+ let root = projectRoot || process.cwd();
1192
+ if (projectId && prefsDir) {
1193
+ const proj = readProjects(prefsDir).find((p) => p.name === projectId);
1194
+ if (proj) {
1195
+ projectSettingsPath =
1196
+ proj.settingsPath || join(proj.path, '.claude', 'settings.json');
1197
+ root = proj.path;
1198
+ }
1199
+ } else if (!projectSettingsPath && projectRoot) {
1200
+ projectSettingsPath = join(projectRoot, '.claude', 'settings.json');
1201
+ }
1202
+
1203
+ return {
1204
+ globalSettings: readJson(globalSettingsPath),
1205
+ projectSettings: readJson(projectSettingsPath),
1206
+ root,
1207
+ };
1208
+ }
1209
+
1210
+ async function crgStatusPayload(projectId) {
1211
+ const { globalSettings, projectSettings, root } =
1212
+ readCrgSettings(projectId);
1213
+ const result = await app.locals.crgStatus.getStatus({
1214
+ globalSettings,
1215
+ projectSettings,
1216
+ projectRoot: root,
1217
+ });
1218
+ return { ...result, building: Boolean(app.locals.crgBuilding) };
1219
+ }
1220
+
1221
+ app.get('/api/crg/status', async (req, res) => {
1222
+ try {
1223
+ res.json(await crgStatusPayload(req.query.project));
1224
+ } catch (err) {
1225
+ res.status(500).json({ ok: false, error: err.message });
1226
+ }
1227
+ });
1228
+
1229
+ app.post('/api/crg/recheck', async (req, res) => {
1230
+ try {
1231
+ app.locals.crgStatus.invalidate();
1232
+ res.json(await crgStatusPayload(req.query.project));
1233
+ } catch (err) {
1234
+ res.status(500).json({ ok: false, error: err.message });
1235
+ }
1236
+ });
1237
+
1238
+ app.post('/api/crg/build', async (req, res) => {
1239
+ try {
1240
+ const { globalSettings, projectSettings, root } = readCrgSettings(
1241
+ req.query.project,
1242
+ );
1243
+ const effective = _effectiveCrgConfig(globalSettings, projectSettings);
1244
+ if (!effective.enabled) {
1245
+ return res
1246
+ .status(400)
1247
+ .json({ ok: false, error: 'Code-review-graph is not enabled' });
1248
+ }
1249
+ const detection = await app.locals.crgStatus.detect();
1250
+ if (
1251
+ !detection.installed ||
1252
+ !detection.compatible ||
1253
+ !detection.fastmcp_ok
1254
+ ) {
1255
+ return res.status(400).json({
1256
+ ok: false,
1257
+ error:
1258
+ detection.error ||
1259
+ 'Code-review-graph is not installed or not compatible',
1260
+ });
1261
+ }
1262
+ if (app.locals.crgBuilding) {
1263
+ return res.json({ ok: true, status: 'building' });
1264
+ }
1265
+ const snap = snapshotDir(root);
1266
+ if (snap) {
1267
+ try {
1268
+ rmSync(snap, { recursive: true, force: true });
1269
+ } catch {}
1270
+ }
1271
+ app.locals.crgBuilding = true;
1272
+ const child = spawn(
1273
+ 'python3',
1274
+ [
1275
+ '-c',
1276
+ 'from worca.scripts.crg_preflight import run_crg_preflight as r; r()',
1277
+ ],
1278
+ { cwd: root, stdio: 'ignore' },
1279
+ );
1280
+ const done = () => {
1281
+ app.locals.crgBuilding = false;
1282
+ app.locals.crgStatus.invalidate();
1283
+ };
1284
+ child.on('exit', done);
1285
+ child.on('error', done);
1286
+ res.json({ ok: true, status: 'building' });
1287
+ } catch (err) {
1288
+ app.locals.crgBuilding = false;
1289
+ res.status(500).json({ ok: false, error: err.message });
1290
+ }
1291
+ });
1292
+
1293
+ app.post('/api/crg/clear', async (req, res) => {
1294
+ try {
1295
+ const { root } = readCrgSettings(req.query.project);
1296
+ const cleared = clearRepoCache(root);
1297
+ app.locals.crgStatus.invalidate();
1298
+ res.json({ ok: true, cleared });
1299
+ } catch (err) {
1300
+ res.status(500).json({ ok: false, error: err.message });
1301
+ }
1302
+ });
1303
+
1304
+ app.get('/api/crg/graph.html', (req, res) => {
1305
+ const { root } = readCrgSettings(req.query.project);
1306
+ const snap = snapshotDir(root);
1307
+ const htmlPath = snap
1308
+ ? join(snap, 'code-review-graph', 'graph.html')
1309
+ : null;
1310
+ if (!htmlPath || !existsSync(htmlPath)) {
1311
+ return res.status(404).json({ ok: false, error: 'graph.html not found' });
1312
+ }
1313
+ res.sendFile(htmlPath);
1314
+ });
1315
+
1168
1316
  // ─── Dynamic favicon ──────────────────────────────────────────────────
1169
1317
  // Serve mode-specific favicon before express.static so it takes precedence.
1170
1318
  app.get('/favicon.svg', (_req, res) => {
@@ -0,0 +1,206 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, statSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+
5
+ import { repoCacheDir, snapshotDir } from './graphify-status.js';
6
+
7
+ // Mirror of _CRG_DEFAULTS in src/worca/utils/code_review_graph.py — keep in sync.
8
+ const CRG_DEFAULTS = {
9
+ enabled: false,
10
+ embeddings: false,
11
+ update_on: {
12
+ preflight: true,
13
+ post_implement: true,
14
+ guardian_post_commit: true,
15
+ },
16
+ freshness: 'clean_only',
17
+ min_repo_files: 100,
18
+ version_range: '>=2,<3',
19
+ fastmcp_min: '3.2.4',
20
+ preflight_timeout_seconds: 300,
21
+ stage_tools: null,
22
+ };
23
+
24
+ // Mirror of effective_crg_config() in src/worca/utils/code_review_graph.py.
25
+ // Enablement is project-level: the project opts in via code_review_graph.enabled.
26
+ // Global code_review_graph.enabled is purely a kill-switch — an EXPLICIT global
27
+ // `false` disables everywhere; `true`/unset defer to the project. These rules
28
+ // MUST match the Python implementation; the parity is guarded by
29
+ // code-review-graph-status.test.js ("_effectiveCrgConfig parity with Python").
30
+ // Update both together.
31
+ export function _effectiveCrgConfig(globalSettings, projectSettings) {
32
+ const gCrg = globalSettings?.worca?.code_review_graph ?? {};
33
+ const pCrg = projectSettings?.worca?.code_review_graph ?? {};
34
+
35
+ if (gCrg.enabled === false) {
36
+ return _disabledConfig('global-off');
37
+ }
38
+
39
+ const projectEnabled = pCrg.enabled ?? false;
40
+ if (!projectEnabled) {
41
+ return _disabledConfig('project-off');
42
+ }
43
+
44
+ const merged = { ...CRG_DEFAULTS };
45
+ for (const [k, v] of Object.entries(gCrg)) {
46
+ if (v != null || k === 'enabled') merged[k] = v;
47
+ }
48
+ for (const [k, v] of Object.entries(pCrg)) {
49
+ if (v != null || k === 'enabled') merged[k] = v;
50
+ }
51
+
52
+ const defaultsUpdateOn = { ...CRG_DEFAULTS.update_on };
53
+ if (gCrg.update_on && typeof gCrg.update_on === 'object') {
54
+ Object.assign(defaultsUpdateOn, gCrg.update_on);
55
+ }
56
+ if (pCrg.update_on && typeof pCrg.update_on === 'object') {
57
+ Object.assign(defaultsUpdateOn, pCrg.update_on);
58
+ }
59
+
60
+ return {
61
+ enabled: true,
62
+ embeddings: merged.embeddings,
63
+ update_on_preflight: defaultsUpdateOn.preflight ?? true,
64
+ update_on_post_implement: defaultsUpdateOn.post_implement ?? true,
65
+ update_on_guardian_post_commit:
66
+ defaultsUpdateOn.guardian_post_commit ?? true,
67
+ min_repo_files: merged.min_repo_files,
68
+ version_range: merged.version_range,
69
+ fastmcp_min: merged.fastmcp_min,
70
+ preflight_timeout_seconds: merged.preflight_timeout_seconds,
71
+ freshness: merged.freshness,
72
+ stage_tools: merged.stage_tools,
73
+ reason: null,
74
+ };
75
+ }
76
+
77
+ function _disabledConfig(reason) {
78
+ const d = CRG_DEFAULTS;
79
+ const u = d.update_on;
80
+ return {
81
+ enabled: false,
82
+ embeddings: d.embeddings,
83
+ update_on_preflight: u.preflight,
84
+ update_on_post_implement: u.post_implement,
85
+ update_on_guardian_post_commit: u.guardian_post_commit,
86
+ min_repo_files: d.min_repo_files,
87
+ version_range: d.version_range,
88
+ fastmcp_min: d.fastmcp_min,
89
+ preflight_timeout_seconds: d.preflight_timeout_seconds,
90
+ freshness: d.freshness,
91
+ stage_tools: d.stage_tools,
92
+ reason,
93
+ };
94
+ }
95
+
96
+ // ─── Per-commit CRG graph stats ─────────────────────────────────────────
97
+
98
+ const _CRG_SUBDIR = 'code-review-graph';
99
+
100
+ export function _crgGraphStats(snapDir) {
101
+ if (!snapDir) return null;
102
+ const dbPath = join(snapDir, _CRG_SUBDIR, 'graph.db');
103
+ if (!existsSync(dbPath)) return null;
104
+
105
+ const stat = statSync(dbPath);
106
+ const ageSeconds = Math.max(0, (Date.now() - stat.mtimeMs) / 1000);
107
+ const htmlPath = join(snapDir, _CRG_SUBDIR, 'graph.html');
108
+
109
+ return {
110
+ db_path: dbPath,
111
+ snapshot_dir: snapDir,
112
+ age_seconds: ageSeconds,
113
+ size_bytes: stat.size,
114
+ has_html: existsSync(htmlPath),
115
+ };
116
+ }
117
+
118
+ // ─── Detection (delegates to Python detect_code_review_graph) ───────────
119
+
120
+ function defaultDetect() {
121
+ return new Promise((resolve) => {
122
+ const child = spawn(
123
+ 'python3',
124
+ [
125
+ '-c',
126
+ 'import json; from worca.utils.code_review_graph import detect_code_review_graph; d = detect_code_review_graph(); print(json.dumps({"installed": d.installed, "version": d.version, "compatible": d.compatible, "fastmcp_ok": d.fastmcp_ok, "error": d.error}))',
127
+ ],
128
+ { stdio: ['ignore', 'pipe', 'pipe'] },
129
+ );
130
+
131
+ let stdout = '';
132
+ let stderr = '';
133
+ child.stdout.on('data', (chunk) => {
134
+ stdout += chunk.toString();
135
+ });
136
+ child.stderr.on('data', (chunk) => {
137
+ stderr += chunk.toString();
138
+ });
139
+ child.on('error', () => {
140
+ resolve({
141
+ installed: false,
142
+ version: null,
143
+ compatible: false,
144
+ fastmcp_ok: false,
145
+ error: 'python3 not available',
146
+ });
147
+ });
148
+ child.on('exit', (code) => {
149
+ if (code === 0 && stdout.trim()) {
150
+ try {
151
+ resolve(JSON.parse(stdout.trim()));
152
+ return;
153
+ } catch {
154
+ // fall through
155
+ }
156
+ }
157
+ resolve({
158
+ installed: false,
159
+ version: null,
160
+ compatible: false,
161
+ fastmcp_ok: false,
162
+ error: stderr.trim() || `detect exited ${code}`,
163
+ });
164
+ });
165
+ });
166
+ }
167
+
168
+ const DEFAULT_TTL_MS = 60_000;
169
+
170
+ export function createCrgStatus(opts = {}) {
171
+ const detectFn = opts.detectFn || defaultDetect;
172
+ const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
173
+
174
+ let cached = null;
175
+ let cachedAt = 0;
176
+
177
+ async function detect() {
178
+ const now = Date.now();
179
+ if (cached && now - cachedAt < ttlMs) return cached;
180
+ cached = await detectFn();
181
+ cachedAt = Date.now();
182
+ return cached;
183
+ }
184
+
185
+ function invalidate() {
186
+ cached = null;
187
+ cachedAt = 0;
188
+ }
189
+
190
+ async function getStatus({ globalSettings, projectSettings, projectRoot }) {
191
+ const effective = _effectiveCrgConfig(globalSettings, projectSettings);
192
+ const detection = await detect();
193
+ const graphStats = effective.enabled
194
+ ? _crgGraphStats(snapshotDir(projectRoot))
195
+ : null;
196
+ return {
197
+ ok: true,
198
+ effective,
199
+ detection,
200
+ graph_stats: graphStats,
201
+ cache_path: repoCacheDir(projectRoot),
202
+ };
203
+ }
204
+
205
+ return { detect, invalidate, getStatus };
206
+ }