@worca/ui 0.2.0 → 0.3.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/styles.css CHANGED
@@ -528,6 +528,13 @@ h1, h2, h3, h4, h5, h6 {
528
528
  font-size: 13px;
529
529
  }
530
530
 
531
+ .run-template {
532
+ display: flex;
533
+ align-items: center;
534
+ gap: 6px;
535
+ font-size: 13px;
536
+ }
537
+
531
538
  .pipeline-cost-strip {
532
539
  display: flex;
533
540
  flex-wrap: wrap;
@@ -1247,6 +1254,15 @@ sl-details.log-history-panel::part(content) {
1247
1254
  font-variant-numeric: tabular-nums;
1248
1255
  }
1249
1256
 
1257
+ .run-card-template {
1258
+ display: flex;
1259
+ align-items: center;
1260
+ gap: 4px;
1261
+ padding-left: 26px;
1262
+ font-size: 12px;
1263
+ color: var(--muted);
1264
+ }
1265
+
1250
1266
  .run-card-stages {
1251
1267
  display: flex;
1252
1268
  flex-wrap: wrap;
@@ -2249,6 +2265,43 @@ sl-details.live-output-panel::part(content) {
2249
2265
  font-weight: 500;
2250
2266
  }
2251
2267
 
2268
+ .new-run-section sl-select {
2269
+ width: 100%;
2270
+ }
2271
+
2272
+ /* Template group labels */
2273
+ .template-group-label {
2274
+ display: block;
2275
+ font-size: 10px;
2276
+ font-weight: 700;
2277
+ letter-spacing: 0.05em;
2278
+ text-transform: uppercase;
2279
+ color: var(--muted);
2280
+ padding: 6px 12px 2px;
2281
+ }
2282
+
2283
+ /* Indent grouped template options and bold the name */
2284
+ sl-option.template-grouped::part(base) {
2285
+ padding-left: 24px;
2286
+ flex-wrap: wrap;
2287
+ }
2288
+
2289
+ sl-option.template-grouped::part(label) {
2290
+ font-weight: 500;
2291
+ }
2292
+
2293
+ /* Description wraps below name, aligned with label */
2294
+ sl-option.template-grouped::part(suffix) {
2295
+ flex-basis: 100%;
2296
+ font-size: 11px;
2297
+ line-height: 1.4;
2298
+ color: var(--muted);
2299
+ white-space: nowrap;
2300
+ overflow: hidden;
2301
+ text-overflow: ellipsis;
2302
+ padding: 0 0 4px;
2303
+ }
2304
+
2252
2305
  /* Plan file autocomplete */
2253
2306
  .plan-autocomplete {
2254
2307
  position: relative;
@@ -3937,3 +3990,120 @@ sl-details.learnings-panel::part(content) {
3937
3990
  .project-worca-version { font-size: 11px; color: var(--muted); font-family: var(--sl-font-mono); margin-top: 2px; }
3938
3991
  .project-worca-version--behind { color: var(--status-failed, #dc2626); }
3939
3992
 
3993
+ /* ─── Bead tooltip content ──────────────────────────────────────────── */
3994
+ .bead-tooltip-content {
3995
+ max-width: 540px;
3996
+ padding: 4px 2px;
3997
+ display: flex;
3998
+ flex-direction: column;
3999
+ gap: 2px;
4000
+ }
4001
+
4002
+ .bead-tooltip-header {
4003
+ display: flex;
4004
+ align-items: center;
4005
+ justify-content: space-between;
4006
+ gap: 12px;
4007
+ }
4008
+
4009
+ sl-tooltip.bead-tooltip::part(body) {
4010
+ pointer-events: auto;
4011
+ }
4012
+
4013
+ .bead-tooltip-badges {
4014
+ display: flex;
4015
+ align-items: center;
4016
+ gap: 4px;
4017
+ }
4018
+
4019
+ .bead-tooltip-id {
4020
+ font-family: var(--sl-font-mono);
4021
+ font-size: 11px;
4022
+ opacity: 0.7;
4023
+ font-weight: 600;
4024
+ text-transform: uppercase;
4025
+ letter-spacing: 0.03em;
4026
+ }
4027
+
4028
+ .bead-tooltip-separator {
4029
+ border: none;
4030
+ border-top: 1px solid rgba(255, 255, 255, 0.2);
4031
+ margin: 4px 0;
4032
+ }
4033
+
4034
+ .bead-tooltip-label {
4035
+ font-size: 11px;
4036
+ opacity: 0.7;
4037
+ font-weight: 600;
4038
+ text-transform: uppercase;
4039
+ letter-spacing: 0.03em;
4040
+ }
4041
+
4042
+ .bead-tooltip-title {
4043
+ font-weight: 600;
4044
+ font-size: 13px;
4045
+ margin-bottom: 4px;
4046
+ }
4047
+
4048
+ .bead-tooltip-excerpt {
4049
+ font-size: 12px;
4050
+ white-space: pre-wrap;
4051
+ margin-bottom: 2px;
4052
+ }
4053
+
4054
+ .bead-tooltip-footer {
4055
+ display: flex;
4056
+ justify-content: flex-end;
4057
+ margin-top: 6px;
4058
+ }
4059
+
4060
+ .bead-tooltip-copy {
4061
+ display: inline-flex;
4062
+ align-items: center;
4063
+ gap: 4px;
4064
+ background: rgba(255, 255, 255, 0.15);
4065
+ color: inherit;
4066
+ border: 1px solid rgba(255, 255, 255, 0.25);
4067
+ border-radius: 4px;
4068
+ padding: 3px 8px;
4069
+ font-size: 11px;
4070
+ cursor: pointer;
4071
+ transition: background 0.15s;
4072
+ }
4073
+
4074
+ .bead-tooltip-copy:hover {
4075
+ background: rgba(255, 255, 255, 0.25);
4076
+ }
4077
+
4078
+ /* ─── Graph node tooltip overlays ─────────────────────────────────── */
4079
+ .run-beads-graph {
4080
+ position: relative;
4081
+ }
4082
+
4083
+ .graph-tooltip-trigger {
4084
+ position: absolute;
4085
+ cursor: pointer;
4086
+ }
4087
+
4088
+ .bead-chip-tooltip {
4089
+ display: flex;
4090
+ flex-direction: column;
4091
+ gap: 3px;
4092
+ max-width: 240px;
4093
+ padding: 2px 0;
4094
+ }
4095
+
4096
+ .bead-chip-tooltip-id {
4097
+ font-family: var(--sl-font-mono);
4098
+ font-size: 12px;
4099
+ font-weight: 600;
4100
+ }
4101
+
4102
+ .bead-chip-tooltip-title {
4103
+ font-size: 12px;
4104
+ color: var(--muted);
4105
+ white-space: nowrap;
4106
+ overflow: hidden;
4107
+ text-overflow: ellipsis;
4108
+ }
4109
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@worca/ui",
3
- "version": "0.2.0",
3
+ "version": "0.3.1-rc.1",
4
4
  "description": "Pipeline monitoring UI for worca-cc",
5
5
  "license": "MIT",
6
6
  "author": "Sinisha Djukic",
@@ -210,6 +210,9 @@ export class ProcessManager {
210
210
  if (opts.branch) {
211
211
  args.push('--branch', opts.branch);
212
212
  }
213
+ if (opts.template) {
214
+ args.push('--template', opts.template);
215
+ }
213
216
 
214
217
  const env = { ...process.env };
215
218
  delete env.CLAUDECODE;
@@ -73,6 +73,9 @@ export function findRunStatusPath(worcaDir, runId) {
73
73
 
74
74
  /** Validate a branch name — alphanumeric, dots, hyphens, underscores, slashes */
75
75
  const BRANCH_RE = /^[\w.\-/]+$/;
76
+
77
+ /** Validate a template identifier — lowercase alphanumeric and hyphens, 1-64 chars */
78
+ const TEMPLATE_RE = /^[a-z0-9-]{1,64}$/;
76
79
  function validateBranch(branch) {
77
80
  return (
78
81
  typeof branch === 'string' && branch.length <= 200 && BRANCH_RE.test(branch)
@@ -472,8 +475,16 @@ export function createProjectScopedRoutes() {
472
475
  router.post('/runs', requireWorcaDir, async (req, res) => {
473
476
  const body = req.body || {};
474
477
 
475
- let { sourceType, sourceValue, prompt, planFile, msize, mloops, branch } =
476
- body;
478
+ let {
479
+ sourceType,
480
+ sourceValue,
481
+ prompt,
482
+ planFile,
483
+ msize,
484
+ mloops,
485
+ branch,
486
+ template,
487
+ } = body;
477
488
  if (body.inputType && sourceType === undefined) {
478
489
  if (body.inputType === 'prompt') {
479
490
  sourceType = 'none';
@@ -534,6 +545,15 @@ export function createProjectScopedRoutes() {
534
545
  }
535
546
  }
536
547
 
548
+ if (template !== undefined && template !== null) {
549
+ if (typeof template !== 'string' || !TEMPLATE_RE.test(template)) {
550
+ return res.status(400).json({
551
+ ok: false,
552
+ error: 'template must match ^[a-z0-9-]{1,64}$',
553
+ });
554
+ }
555
+ }
556
+
537
557
  const hasSource = sourceType !== 'none' && sourceValue;
538
558
  const hasPlan = typeof planFile === 'string' && planFile.trim().length > 0;
539
559
  const hasPrompt = typeof prompt === 'string' && prompt.length > 0;
@@ -561,6 +581,7 @@ export function createProjectScopedRoutes() {
561
581
  mloops: mloopsVal,
562
582
  planFile: hasPlan ? planFile.trim() : undefined,
563
583
  branch: branch || undefined,
584
+ template: template || undefined,
564
585
  });
565
586
  const { broadcast } = req.app.locals;
566
587
  if (broadcast) broadcast('run-started', { pid: result.pid });
@@ -1142,6 +1163,45 @@ export function createProjectScopedRoutes() {
1142
1163
  }
1143
1164
  });
1144
1165
 
1166
+ // GET /api/projects/:projectId/templates — list available pipeline templates
1167
+ router.get('/templates', (req, res) => {
1168
+ const root = req.project.projectRoot;
1169
+ const tiers = [
1170
+ { tier: 'worca', dir: join(root, '.claude', 'worca', 'templates') },
1171
+ { tier: 'project', dir: join(root, '.claude', 'templates') },
1172
+ { tier: 'user', dir: join(homedir(), '.worca', 'templates') },
1173
+ ];
1174
+
1175
+ const templates = [];
1176
+ for (const { tier, dir } of tiers) {
1177
+ if (!existsSync(dir)) continue;
1178
+ let entries;
1179
+ try {
1180
+ entries = readdirSync(dir, { withFileTypes: true });
1181
+ } catch {
1182
+ continue;
1183
+ }
1184
+ for (const entry of entries) {
1185
+ if (!entry.isDirectory()) continue;
1186
+ const manifestPath = join(dir, entry.name, 'template.json');
1187
+ if (!existsSync(manifestPath)) continue;
1188
+ try {
1189
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
1190
+ templates.push({
1191
+ id: manifest.id || entry.name,
1192
+ name: manifest.name || entry.name,
1193
+ description: manifest.description || '',
1194
+ tier,
1195
+ });
1196
+ } catch {
1197
+ /* skip malformed manifests */
1198
+ }
1199
+ }
1200
+ }
1201
+
1202
+ res.json({ ok: true, templates });
1203
+ });
1204
+
1145
1205
  // GET /api/projects/:projectId/worca-status — check worca installation state
1146
1206
  router.get('/worca-status', (req, res) => {
1147
1207
  const installed = checkWorcaInstalled(req.project.projectRoot);
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
  }