@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/main.bundle.js +771 -678
- package/app/main.bundle.js.map +3 -3
- package/app/styles.css +170 -0
- package/package.json +1 -1
- package/server/process-manager.js +3 -0
- package/server/project-routes.js +62 -2
- package/server/watcher.js +7 -18
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
package/server/project-routes.js
CHANGED
|
@@ -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 {
|
|
476
|
-
|
|
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 =
|
|
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 =
|
|
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) &&
|
|
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 =
|
|
182
|
+
const active =
|
|
183
|
+
!isTerminal(status) && status.pipeline_status === 'running';
|
|
195
184
|
runs.push({ id, active, ...status });
|
|
196
185
|
seenIds.add(id);
|
|
197
186
|
}
|