@worca/ui 0.25.1 → 0.27.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/main.bundle.js +1199 -1096
- package/app/main.bundle.js.map +4 -4
- package/app/styles.css +51 -1
- package/package.json +1 -1
- package/scripts/build-frontend.js +29 -2
- package/server/beads-reader.js +9 -2
- package/server/model-env-routes.js +7 -2
- package/server/project-routes.js +47 -5
- package/server/settings-validator.js +15 -2
- package/server/ws-message-router.js +26 -8
package/app/styles.css
CHANGED
|
@@ -2031,7 +2031,7 @@ sl-input [slot="prefix"] {
|
|
|
2031
2031
|
|
|
2032
2032
|
.pricing-model-name {
|
|
2033
2033
|
font-weight: 500;
|
|
2034
|
-
text-transform:
|
|
2034
|
+
text-transform: uppercase;
|
|
2035
2035
|
}
|
|
2036
2036
|
|
|
2037
2037
|
.pricing-table sl-input {
|
|
@@ -5084,6 +5084,56 @@ sl-tooltip.bead-tooltip::part(body) {
|
|
|
5084
5084
|
border-color: var(--status-failed, #ef4444);
|
|
5085
5085
|
}
|
|
5086
5086
|
|
|
5087
|
+
.rename-model-input.is-invalid::part(base) {
|
|
5088
|
+
border-color: var(--status-failed, #ef4444);
|
|
5089
|
+
}
|
|
5090
|
+
|
|
5091
|
+
.rename-model-error {
|
|
5092
|
+
color: var(--status-failed, #ef4444);
|
|
5093
|
+
font-size: 12px;
|
|
5094
|
+
margin: 0.4rem 0 0;
|
|
5095
|
+
}
|
|
5096
|
+
|
|
5097
|
+
.model-env-details::part(base) {
|
|
5098
|
+
border: none;
|
|
5099
|
+
background: transparent;
|
|
5100
|
+
box-shadow: none;
|
|
5101
|
+
}
|
|
5102
|
+
|
|
5103
|
+
.model-env-details::part(header) {
|
|
5104
|
+
padding: 0.25rem 0.4rem;
|
|
5105
|
+
border-radius: 6px;
|
|
5106
|
+
}
|
|
5107
|
+
|
|
5108
|
+
.model-env-details::part(header):hover {
|
|
5109
|
+
background: var(--hover, rgba(0, 0, 0, 0.04));
|
|
5110
|
+
}
|
|
5111
|
+
|
|
5112
|
+
.model-env-details::part(content) {
|
|
5113
|
+
padding: 0.5rem 0 0;
|
|
5114
|
+
}
|
|
5115
|
+
|
|
5116
|
+
.model-env-summary {
|
|
5117
|
+
display: flex;
|
|
5118
|
+
align-items: center;
|
|
5119
|
+
justify-content: space-between;
|
|
5120
|
+
width: 100%;
|
|
5121
|
+
gap: 0.75rem;
|
|
5122
|
+
}
|
|
5123
|
+
|
|
5124
|
+
.model-env-invalid-chip {
|
|
5125
|
+
color: var(--status-failed, #ef4444);
|
|
5126
|
+
font-weight: 600;
|
|
5127
|
+
font-size: 11px;
|
|
5128
|
+
text-transform: uppercase;
|
|
5129
|
+
letter-spacing: 0.5px;
|
|
5130
|
+
}
|
|
5131
|
+
|
|
5132
|
+
.model-env-details.has-invalid::part(header) {
|
|
5133
|
+
border-left: 3px solid var(--status-failed, #ef4444);
|
|
5134
|
+
padding-left: 0.5rem;
|
|
5135
|
+
}
|
|
5136
|
+
|
|
5087
5137
|
.model-env-value::part(input) {
|
|
5088
5138
|
font-size: 12px;
|
|
5089
5139
|
}
|
package/package.json
CHANGED
|
@@ -4,6 +4,25 @@ import { copyFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
7
|
+
function findPython() {
|
|
8
|
+
/* Find a usable Python — prefer python3 on Unix, python on Windows. */
|
|
9
|
+
const candidates =
|
|
10
|
+
process.platform === 'win32'
|
|
11
|
+
? ['python', 'python3']
|
|
12
|
+
: ['python3', 'python'];
|
|
13
|
+
for (const cmd of candidates) {
|
|
14
|
+
try {
|
|
15
|
+
execFileSync(cmd, ['--version'], {
|
|
16
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
17
|
+
});
|
|
18
|
+
return cmd;
|
|
19
|
+
} catch {
|
|
20
|
+
/* not found or not executable — try next */
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
7
26
|
async function run() {
|
|
8
27
|
const thisFile = fileURLToPath(new URL(import.meta.url));
|
|
9
28
|
const repoRoot = path.resolve(path.dirname(thisFile), '..');
|
|
@@ -49,6 +68,14 @@ async function run() {
|
|
|
49
68
|
const utilsDir = path.join(appDir, 'utils');
|
|
50
69
|
mkdirSync(utilsDir, { recursive: true });
|
|
51
70
|
const constantsOut = path.join(utilsDir, 'status-constants.js');
|
|
71
|
+
const python = findPython();
|
|
72
|
+
if (!python) {
|
|
73
|
+
console.error(
|
|
74
|
+
'status-constants codegen failed: python not found (install Python and worca-cc)',
|
|
75
|
+
);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
52
79
|
try {
|
|
53
80
|
const pyScript =
|
|
54
81
|
'import json; from worca.state import status as s; ' +
|
|
@@ -62,7 +89,7 @@ async function run() {
|
|
|
62
89
|
'"PIPELINE_ALL_TERMINAL": sorted(s.PIPELINE_ALL_TERMINAL), ' +
|
|
63
90
|
'"PIPELINE_IN_FLIGHT": sorted(s.PIPELINE_IN_FLIGHT)' +
|
|
64
91
|
'}))';
|
|
65
|
-
const raw = execFileSync(
|
|
92
|
+
const raw = execFileSync(python, ['-c', pyScript], {
|
|
66
93
|
cwd: path.join(repoRoot, '..'),
|
|
67
94
|
encoding: 'utf8',
|
|
68
95
|
});
|
|
@@ -81,7 +108,7 @@ async function run() {
|
|
|
81
108
|
console.log('generated', path.relative(repoRoot, constantsOut));
|
|
82
109
|
} catch (err) {
|
|
83
110
|
console.error(
|
|
84
|
-
|
|
111
|
+
`status-constants codegen failed (is ${python} + worca-cc installed?):`,
|
|
85
112
|
err.message,
|
|
86
113
|
);
|
|
87
114
|
process.exitCode = 1;
|
package/server/beads-reader.js
CHANGED
|
@@ -59,14 +59,21 @@ export async function listIssues(beadsDb) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export async function listIssuesByLabel(beadsDb, label) {
|
|
62
|
-
|
|
62
|
+
const attempt = async () => {
|
|
63
63
|
const issues = await runBd(
|
|
64
64
|
['list', '--label-any', label, '--all', '--limit', '0'],
|
|
65
65
|
beadsDb,
|
|
66
66
|
);
|
|
67
67
|
return await enrichWithDeps(issues, beadsDb);
|
|
68
|
+
};
|
|
69
|
+
try {
|
|
70
|
+
return await attempt();
|
|
68
71
|
} catch {
|
|
69
|
-
|
|
72
|
+
// bd/SQLite contention during active runs is usually sub-second — one
|
|
73
|
+
// retry covers the observed window. If it still fails, propagate so the
|
|
74
|
+
// WS handler can return an error rather than masquerading as "no beads."
|
|
75
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
76
|
+
return await attempt();
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
79
|
|
|
@@ -115,8 +115,13 @@ export function createModelEnvRouter({ settingsPath: staticPath } = {}) {
|
|
|
115
115
|
|
|
116
116
|
let baseChanged = false;
|
|
117
117
|
if (resolvedId) {
|
|
118
|
-
//
|
|
119
|
-
|
|
118
|
+
// When env exists in local, base MUST use the object form `{id}` so
|
|
119
|
+
// deepMerge({id}, {env}) preserves the id. With the string form,
|
|
120
|
+
// deepMerge would see a non-object base and discard it, dropping the id
|
|
121
|
+
// entirely — the bug behind empty Model ID after Duplicate/Paste.
|
|
122
|
+
// String form stays the default when there's no env, to keep JSON minimal.
|
|
123
|
+
const hasEnv = Object.keys(envIn).length > 0;
|
|
124
|
+
const nextBaseEntry = hasEnv ? { id: resolvedId } : resolvedId;
|
|
120
125
|
if (JSON.stringify(baseEntry) !== JSON.stringify(nextBaseEntry)) {
|
|
121
126
|
base.worca.models[model] = nextBaseEntry;
|
|
122
127
|
baseChanged = true;
|
package/server/project-routes.js
CHANGED
|
@@ -478,7 +478,18 @@ export function createProjectScopedRoutes({
|
|
|
478
478
|
});
|
|
479
479
|
}
|
|
480
480
|
|
|
481
|
-
|
|
481
|
+
let existingForValidation = {};
|
|
482
|
+
try {
|
|
483
|
+
if (existsSync(settingsPath)) {
|
|
484
|
+
existingForValidation = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
485
|
+
}
|
|
486
|
+
} catch {
|
|
487
|
+
existingForValidation = {};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const validation = validateSettingsPayload(body, {
|
|
491
|
+
existing: existingForValidation,
|
|
492
|
+
});
|
|
482
493
|
if (!validation.valid) {
|
|
483
494
|
return res.status(400).json({
|
|
484
495
|
error: {
|
|
@@ -558,12 +569,43 @@ export function createProjectScopedRoutes({
|
|
|
558
569
|
atomicWriteSync(settingsPath, `${JSON.stringify(base, null, 2)}\n`);
|
|
559
570
|
}
|
|
560
571
|
|
|
572
|
+
// STEP 3a: strip shadowed worca keys from settings.local.json. Local is
|
|
573
|
+
// deep-merged over base on read, so a stale `worca.<key>` copy in local
|
|
574
|
+
// would resurrect after the user saves a new value. `models` is excluded
|
|
575
|
+
// because its env-portion lives in local by design (see model-env-routes).
|
|
576
|
+
const lp = localPathFor(settingsPath);
|
|
577
|
+
let localChanged = false;
|
|
578
|
+
const localForPrune = readLocalSettings(settingsPath);
|
|
579
|
+
if (
|
|
580
|
+
body.worca &&
|
|
581
|
+
typeof body.worca === 'object' &&
|
|
582
|
+
localForPrune.worca &&
|
|
583
|
+
typeof localForPrune.worca === 'object'
|
|
584
|
+
) {
|
|
585
|
+
for (const key of Object.keys(body.worca)) {
|
|
586
|
+
if (key === 'models') continue;
|
|
587
|
+
if (key in localForPrune.worca) {
|
|
588
|
+
delete localForPrune.worca[key];
|
|
589
|
+
localChanged = true;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (localChanged && Object.keys(localForPrune.worca).length === 0) {
|
|
593
|
+
delete localForPrune.worca;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
561
597
|
if (body.permissions !== undefined) {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
598
|
+
localForPrune.permissions = body.permissions;
|
|
599
|
+
localChanged = true;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (localChanged) {
|
|
565
603
|
mkdirSync(dirname(lp), { recursive: true });
|
|
566
|
-
writeFileSync(
|
|
604
|
+
writeFileSync(
|
|
605
|
+
lp,
|
|
606
|
+
`${JSON.stringify(localForPrune, null, 2)}\n`,
|
|
607
|
+
'utf8',
|
|
608
|
+
);
|
|
567
609
|
}
|
|
568
610
|
|
|
569
611
|
const merged = readMergedSettings(settingsPath);
|
|
@@ -36,8 +36,12 @@ const VALID_PRICING_FIELDS = [
|
|
|
36
36
|
'cache_read_per_mtok',
|
|
37
37
|
];
|
|
38
38
|
|
|
39
|
-
export function validateSettingsPayload(body) {
|
|
39
|
+
export function validateSettingsPayload(body, options = {}) {
|
|
40
40
|
const details = [];
|
|
41
|
+
const existingWorca =
|
|
42
|
+
options.existing && typeof options.existing === 'object'
|
|
43
|
+
? options.existing.worca || {}
|
|
44
|
+
: {};
|
|
41
45
|
|
|
42
46
|
if (body.worca !== undefined) {
|
|
43
47
|
if (
|
|
@@ -49,7 +53,16 @@ export function validateSettingsPayload(body) {
|
|
|
49
53
|
return { valid: false, details };
|
|
50
54
|
}
|
|
51
55
|
const w = body.worca;
|
|
52
|
-
|
|
56
|
+
// Sections like agents/pricing reference model keys that may live in another
|
|
57
|
+
// section saved earlier. Merge persisted models with body-supplied models so
|
|
58
|
+
// a single-section save (e.g. agents-only) doesn't reject custom models.
|
|
59
|
+
const mergedModels = {
|
|
60
|
+
...(existingWorca.models && typeof existingWorca.models === 'object'
|
|
61
|
+
? existingWorca.models
|
|
62
|
+
: {}),
|
|
63
|
+
...(w.models && typeof w.models === 'object' ? w.models : {}),
|
|
64
|
+
};
|
|
65
|
+
const validModels = deriveValidModels({ models: mergedModels });
|
|
53
66
|
|
|
54
67
|
// agents
|
|
55
68
|
if (w.agents !== undefined) {
|
|
@@ -682,14 +682,32 @@ export function createMessageRouter({
|
|
|
682
682
|
ws.send(JSON.stringify(makeOk(req, { issues: [], runId })));
|
|
683
683
|
return;
|
|
684
684
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
685
|
+
try {
|
|
686
|
+
const issues = await listIssuesByLabel(beadsDbPath, `run:${runId}`);
|
|
687
|
+
console.log(
|
|
688
|
+
'[list-beads-by-run] runId=%s count=%d statuses=%o',
|
|
689
|
+
runId,
|
|
690
|
+
issues.length,
|
|
691
|
+
issues.map((i) => i.status),
|
|
692
|
+
);
|
|
693
|
+
ws.send(JSON.stringify(makeOk(req, { issues, runId })));
|
|
694
|
+
} catch (err) {
|
|
695
|
+
// Don't return empty issues on failure — the UI would treat that as
|
|
696
|
+
// "all beads deleted" and tear down the open <sl-details> panel. Let
|
|
697
|
+
// the client keep its last-known-good state until the next poll.
|
|
698
|
+
console.warn(
|
|
699
|
+
`[list-beads-by-run] runId=${runId} failed: ${err?.message || err}`,
|
|
700
|
+
);
|
|
701
|
+
ws.send(
|
|
702
|
+
JSON.stringify(
|
|
703
|
+
makeError(
|
|
704
|
+
req,
|
|
705
|
+
'beads_unavailable',
|
|
706
|
+
`bd query failed: ${err?.message || err}`,
|
|
707
|
+
),
|
|
708
|
+
),
|
|
709
|
+
);
|
|
710
|
+
}
|
|
693
711
|
return;
|
|
694
712
|
}
|
|
695
713
|
|