@worca/ui 0.43.0 → 0.44.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 +1070 -986
- package/app/main.bundle.js.map +3 -3
- package/app/styles.css +54 -0
- package/package.json +1 -1
- package/server/paths.js +11 -0
- package/server/project-routes.js +24 -1
- package/server/settings-merge.js +45 -0
- package/server/templates-routes.js +127 -12
package/app/styles.css
CHANGED
|
@@ -8170,6 +8170,60 @@ sl-dialog.markdown-dialog::part(body) {
|
|
|
8170
8170
|
color: var(--fg);
|
|
8171
8171
|
}
|
|
8172
8172
|
|
|
8173
|
+
.template-action-dialog .dialog-collisions {
|
|
8174
|
+
border-left: 3px solid var(--sl-color-warning-500, #f59e0b);
|
|
8175
|
+
padding: 10px 12px;
|
|
8176
|
+
background: var(--sl-color-warning-50, rgba(245, 158, 11, 0.05));
|
|
8177
|
+
border-radius: 4px;
|
|
8178
|
+
min-width: 0;
|
|
8179
|
+
}
|
|
8180
|
+
.template-action-dialog .dialog-collisions-header {
|
|
8181
|
+
margin-bottom: 6px;
|
|
8182
|
+
}
|
|
8183
|
+
.template-action-dialog .dialog-collision-list {
|
|
8184
|
+
list-style: none;
|
|
8185
|
+
margin: 10px 0 0;
|
|
8186
|
+
padding: 0;
|
|
8187
|
+
display: flex;
|
|
8188
|
+
flex-direction: column;
|
|
8189
|
+
gap: 8px;
|
|
8190
|
+
}
|
|
8191
|
+
.template-action-dialog .dialog-collision-row {
|
|
8192
|
+
display: grid;
|
|
8193
|
+
grid-template-columns: minmax(90px, 140px) minmax(0, 1fr) minmax(0, 1fr);
|
|
8194
|
+
align-items: center;
|
|
8195
|
+
gap: 8px;
|
|
8196
|
+
min-width: 0;
|
|
8197
|
+
}
|
|
8198
|
+
.template-action-dialog .dialog-collision-alias {
|
|
8199
|
+
min-width: 0;
|
|
8200
|
+
overflow: hidden;
|
|
8201
|
+
text-overflow: ellipsis;
|
|
8202
|
+
white-space: nowrap;
|
|
8203
|
+
}
|
|
8204
|
+
.template-action-dialog .dialog-collision-alias code {
|
|
8205
|
+
font-family: var(--sl-font-mono);
|
|
8206
|
+
font-size: 13px;
|
|
8207
|
+
color: var(--fg);
|
|
8208
|
+
}
|
|
8209
|
+
.template-action-dialog .collision-action-select,
|
|
8210
|
+
.template-action-dialog .collision-rename-input {
|
|
8211
|
+
width: 100%;
|
|
8212
|
+
min-width: 0;
|
|
8213
|
+
}
|
|
8214
|
+
.template-action-dialog .dialog-collision-row[data-rename="false"] .collision-action-select {
|
|
8215
|
+
grid-column: 2 / span 2;
|
|
8216
|
+
}
|
|
8217
|
+
.template-action-dialog .dialog-new-aliases-hint {
|
|
8218
|
+
margin-top: 8px;
|
|
8219
|
+
}
|
|
8220
|
+
.template-action-dialog .dialog-new-aliases-hint code {
|
|
8221
|
+
font-family: var(--sl-font-mono);
|
|
8222
|
+
font-size: 12px;
|
|
8223
|
+
color: var(--fg);
|
|
8224
|
+
margin-right: 4px;
|
|
8225
|
+
}
|
|
8226
|
+
|
|
8173
8227
|
.editor-content {
|
|
8174
8228
|
flex: 1;
|
|
8175
8229
|
overflow-y: auto;
|
package/package.json
CHANGED
package/server/paths.js
CHANGED
|
@@ -23,6 +23,17 @@ export function worcaHome() {
|
|
|
23
23
|
return join(homedir(), '.worca');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* @returns {string} Absolute path to the user-global settings.json
|
|
28
|
+
* (`~/.worca/settings.json` by default, $WORCA_HOME-aware). The Python
|
|
29
|
+
* runtime reads this layer first and deep-merges project settings on top,
|
|
30
|
+
* so any UI surface that needs to mirror runtime resolution (e.g. model
|
|
31
|
+
* alias dropdowns) must layer this in.
|
|
32
|
+
*/
|
|
33
|
+
export function globalSettingsPath() {
|
|
34
|
+
return join(worcaHome(), 'settings.json');
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
/**
|
|
27
38
|
* @param {string=} override Caller-supplied path that wins over $WORCA_HOME.
|
|
28
39
|
* @returns {string} Absolute path to the fleet-runs directory.
|
package/server/project-routes.js
CHANGED
|
@@ -29,7 +29,7 @@ import { getDefaultBranch } from './git-helpers.js';
|
|
|
29
29
|
import { extractAndStripGlobalKeys } from './global-keys.js';
|
|
30
30
|
import { LaunchLock } from './launch-lock.js';
|
|
31
31
|
import { createModelEnvRouter } from './model-env-routes.js';
|
|
32
|
-
import { preferencesPath } from './paths.js';
|
|
32
|
+
import { globalSettingsPath, preferencesPath } from './paths.js';
|
|
33
33
|
import { readPreferences } from './preferences.js';
|
|
34
34
|
import { ProcessManager } from './process-manager.js';
|
|
35
35
|
import { countRunningPipelinesAcrossProjects } from './process-registry.js';
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
import {
|
|
46
46
|
deepMerge,
|
|
47
47
|
localPathFor,
|
|
48
|
+
readEffectiveSettings,
|
|
48
49
|
readLocalSettings,
|
|
49
50
|
readMergedSettings,
|
|
50
51
|
} from './settings-merge.js';
|
|
@@ -488,6 +489,28 @@ export function createProjectScopedRoutes({
|
|
|
488
489
|
}
|
|
489
490
|
});
|
|
490
491
|
|
|
492
|
+
// GET /api/projects/:projectId/effective-settings
|
|
493
|
+
//
|
|
494
|
+
// Returns the fully-layered settings the Python runtime sees: user-global
|
|
495
|
+
// base → user-global .local → project base → project .local. Used by UI
|
|
496
|
+
// surfaces that must mirror runtime alias resolution (e.g. the per-agent
|
|
497
|
+
// model dropdown in the template editor — without this, aliases that live
|
|
498
|
+
// only in user-global ~/.worca/settings.json are invisible to projects).
|
|
499
|
+
router.get('/effective-settings', (req, res) => {
|
|
500
|
+
const { settingsPath } = req.project;
|
|
501
|
+
try {
|
|
502
|
+
const merged = readEffectiveSettings(settingsPath, globalSettingsPath());
|
|
503
|
+
res.json({
|
|
504
|
+
worca: merged.worca || {},
|
|
505
|
+
permissions: merged.permissions || {},
|
|
506
|
+
});
|
|
507
|
+
} catch (err) {
|
|
508
|
+
res
|
|
509
|
+
.status(500)
|
|
510
|
+
.json({ error: { code: 'read_error', message: err.message } });
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
491
514
|
// POST /api/projects/:projectId/settings
|
|
492
515
|
router.post('/settings', async (req, res) => {
|
|
493
516
|
const { settingsPath } = req.project;
|
package/server/settings-merge.js
CHANGED
|
@@ -81,3 +81,48 @@ export function readLocalSettings(settingsPath) {
|
|
|
81
81
|
return {};
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Read the full effective settings stack the way the Python runtime resolves
|
|
87
|
+
* it: user-global base → user-global .local → project base → project .local.
|
|
88
|
+
* Each layer deep-merges over the previous. Missing files are treated as
|
|
89
|
+
* empty objects. Used by UI surfaces (model alias dropdowns, etc.) that must
|
|
90
|
+
* mirror what `worca.utils.settings.resolve_model` sees at run time.
|
|
91
|
+
*
|
|
92
|
+
* `globalSettingsPath` is passed in so callers can substitute it during
|
|
93
|
+
* tests; production callers thread the result of `paths.globalSettingsPath()`.
|
|
94
|
+
*/
|
|
95
|
+
export function readEffectiveSettings(projectSettingsPath, globalSettingsPath) {
|
|
96
|
+
const layers = [];
|
|
97
|
+
if (globalSettingsPath) {
|
|
98
|
+
try {
|
|
99
|
+
layers.push(JSON.parse(readFileSync(globalSettingsPath, 'utf8')));
|
|
100
|
+
} catch {
|
|
101
|
+
/* missing or invalid — treat as empty */
|
|
102
|
+
}
|
|
103
|
+
const globalLocal = localPathFor(globalSettingsPath);
|
|
104
|
+
if (existsSync(globalLocal)) {
|
|
105
|
+
try {
|
|
106
|
+
layers.push(JSON.parse(readFileSync(globalLocal, 'utf8')));
|
|
107
|
+
} catch {
|
|
108
|
+
/* invalid — skip */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (projectSettingsPath) {
|
|
113
|
+
try {
|
|
114
|
+
layers.push(JSON.parse(readFileSync(projectSettingsPath, 'utf8')));
|
|
115
|
+
} catch {
|
|
116
|
+
/* missing or invalid — treat as empty */
|
|
117
|
+
}
|
|
118
|
+
const projectLocal = localPathFor(projectSettingsPath);
|
|
119
|
+
if (existsSync(projectLocal)) {
|
|
120
|
+
try {
|
|
121
|
+
layers.push(JSON.parse(readFileSync(projectLocal, 'utf8')));
|
|
122
|
+
} catch {
|
|
123
|
+
/* invalid — skip */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return layers.reduce((acc, layer) => deepMerge(acc, layer), {});
|
|
128
|
+
}
|
|
@@ -378,6 +378,50 @@ function exportGist(projectRoot, id) {
|
|
|
378
378
|
return match ? match[0] : null;
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
function _parseResolutionsHeader(req) {
|
|
382
|
+
const raw = req.headers['x-resolutions'];
|
|
383
|
+
if (!raw) return null;
|
|
384
|
+
try {
|
|
385
|
+
const parsed = JSON.parse(raw);
|
|
386
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
387
|
+
return parsed;
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
/* fall through */
|
|
391
|
+
}
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function _writeResolutionsFile(dir, resolutions) {
|
|
396
|
+
if (!resolutions || Object.keys(resolutions).length === 0) return null;
|
|
397
|
+
const path = join(dir, 'resolutions.json');
|
|
398
|
+
writeFileSync(path, JSON.stringify(resolutions), 'utf8');
|
|
399
|
+
return path;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function _baseImportArgs(
|
|
403
|
+
bundlePath,
|
|
404
|
+
dstTier,
|
|
405
|
+
resolutionsPath,
|
|
406
|
+
onModelConflict,
|
|
407
|
+
) {
|
|
408
|
+
const args = [
|
|
409
|
+
'import',
|
|
410
|
+
'--from',
|
|
411
|
+
bundlePath,
|
|
412
|
+
'--scope',
|
|
413
|
+
dstTier,
|
|
414
|
+
'--non-interactive',
|
|
415
|
+
];
|
|
416
|
+
if (resolutionsPath) {
|
|
417
|
+
args.push('--resolutions', resolutionsPath);
|
|
418
|
+
}
|
|
419
|
+
if (onModelConflict) {
|
|
420
|
+
args.push('--on-model-conflict', onModelConflict);
|
|
421
|
+
}
|
|
422
|
+
return args;
|
|
423
|
+
}
|
|
424
|
+
|
|
381
425
|
function handleZipImport(req, res) {
|
|
382
426
|
const dstTier = req.query.dst_tier;
|
|
383
427
|
if (!isValidTier(dstTier)) {
|
|
@@ -389,18 +433,18 @@ function handleZipImport(req, res) {
|
|
|
389
433
|
if (rejectBuiltinWrite(res, dstTier, 'import to')) return;
|
|
390
434
|
|
|
391
435
|
const zipBuffer = req.body;
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
);
|
|
436
|
+
const resolutions = _parseResolutionsHeader(req);
|
|
437
|
+
const onModelConflict = req.query.on_model_conflict || null;
|
|
438
|
+
const dir = mkdtempSync(join(tmpdir(), 'worca-import-'));
|
|
439
|
+
const tmpPath = join(dir, 'bundle.zip');
|
|
396
440
|
try {
|
|
397
441
|
writeFileSync(tmpPath, zipBuffer);
|
|
442
|
+
const resolutionsPath = _writeResolutionsFile(dir, resolutions);
|
|
398
443
|
const stdout = runWorcaTemplates(
|
|
399
444
|
req.project.projectRoot,
|
|
400
|
-
|
|
445
|
+
_baseImportArgs(tmpPath, dstTier, resolutionsPath, onModelConflict),
|
|
401
446
|
{ timeout: 60000 },
|
|
402
447
|
);
|
|
403
|
-
// Best-effort summary: the CLI may print imported template ids to stdout
|
|
404
448
|
const imported = [];
|
|
405
449
|
for (const line of (stdout || '').split('\n')) {
|
|
406
450
|
const m = line.match(/imported[:\s]+([a-z0-9_-]+)/i);
|
|
@@ -412,16 +456,76 @@ function handleZipImport(req, res) {
|
|
|
412
456
|
.status(statusForCliCode(err.cliCode))
|
|
413
457
|
.json({ ok: false, error: err.message, code: err.cliCode });
|
|
414
458
|
} finally {
|
|
459
|
+
cleanupTemp(dir);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function handleImportPreview(req, res) {
|
|
464
|
+
const dstTier = req.query.dst_tier;
|
|
465
|
+
if (!isValidTier(dstTier)) {
|
|
466
|
+
return res.status(400).json({
|
|
467
|
+
ok: false,
|
|
468
|
+
error: `dst_tier must be one of: ${TIERS.join(', ')}`,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (rejectBuiltinWrite(res, dstTier, 'import to')) return;
|
|
472
|
+
|
|
473
|
+
const ctype = req.headers['content-type'] || '';
|
|
474
|
+
const dir = mkdtempSync(join(tmpdir(), 'worca-import-preview-'));
|
|
475
|
+
try {
|
|
476
|
+
let bundlePath;
|
|
477
|
+
if (ctype.startsWith('application/zip')) {
|
|
478
|
+
bundlePath = join(dir, 'bundle.zip');
|
|
479
|
+
writeFileSync(bundlePath, req.body);
|
|
480
|
+
} else {
|
|
481
|
+
const { bundle } = req.body || {};
|
|
482
|
+
if (!bundle || typeof bundle !== 'object') {
|
|
483
|
+
return res
|
|
484
|
+
.status(400)
|
|
485
|
+
.json({ ok: false, error: 'bundle must be a JSON object' });
|
|
486
|
+
}
|
|
487
|
+
bundlePath = join(dir, 'bundle.json');
|
|
488
|
+
writeFileSync(bundlePath, JSON.stringify(bundle), 'utf8');
|
|
489
|
+
}
|
|
490
|
+
const stdout = runWorcaTemplates(
|
|
491
|
+
req.project.projectRoot,
|
|
492
|
+
[
|
|
493
|
+
'import',
|
|
494
|
+
'--from',
|
|
495
|
+
bundlePath,
|
|
496
|
+
'--scope',
|
|
497
|
+
dstTier,
|
|
498
|
+
'--non-interactive',
|
|
499
|
+
'--preview',
|
|
500
|
+
],
|
|
501
|
+
{ timeout: 30000 },
|
|
502
|
+
);
|
|
415
503
|
try {
|
|
416
|
-
|
|
504
|
+
const payload = JSON.parse(stdout);
|
|
505
|
+
res.json({ ok: true, ...payload });
|
|
417
506
|
} catch {
|
|
418
|
-
|
|
507
|
+
res.status(500).json({
|
|
508
|
+
ok: false,
|
|
509
|
+
error: 'CLI preview output was not valid JSON',
|
|
510
|
+
raw: stdout,
|
|
511
|
+
});
|
|
419
512
|
}
|
|
513
|
+
} catch (err) {
|
|
514
|
+
res
|
|
515
|
+
.status(statusForCliCode(err.cliCode))
|
|
516
|
+
.json({ ok: false, error: err.message, code: err.cliCode });
|
|
517
|
+
} finally {
|
|
518
|
+
cleanupTemp(dir);
|
|
420
519
|
}
|
|
421
520
|
}
|
|
422
521
|
|
|
423
522
|
function handleJsonImport(req, res) {
|
|
424
|
-
const {
|
|
523
|
+
const {
|
|
524
|
+
bundle,
|
|
525
|
+
dst_tier: dstTier,
|
|
526
|
+
resolutions,
|
|
527
|
+
on_model_conflict: onModelConflict,
|
|
528
|
+
} = req.body || {};
|
|
425
529
|
if (!bundle || typeof bundle !== 'object') {
|
|
426
530
|
return res
|
|
427
531
|
.status(400)
|
|
@@ -435,7 +539,10 @@ function handleJsonImport(req, res) {
|
|
|
435
539
|
}
|
|
436
540
|
if (rejectBuiltinWrite(res, dstTier, 'import to')) return;
|
|
437
541
|
try {
|
|
438
|
-
const result = importBundle(req.project.projectRoot, bundle, dstTier
|
|
542
|
+
const result = importBundle(req.project.projectRoot, bundle, dstTier, {
|
|
543
|
+
resolutions,
|
|
544
|
+
onModelConflict,
|
|
545
|
+
});
|
|
439
546
|
res.json({ ok: true, ...result });
|
|
440
547
|
} catch (err) {
|
|
441
548
|
res
|
|
@@ -444,17 +551,19 @@ function handleJsonImport(req, res) {
|
|
|
444
551
|
}
|
|
445
552
|
}
|
|
446
553
|
|
|
447
|
-
function importBundle(projectRoot, bundle, tier) {
|
|
554
|
+
function importBundle(projectRoot, bundle, tier, opts = {}) {
|
|
448
555
|
if (!bundle || !Array.isArray(bundle.templates)) {
|
|
449
556
|
throw new Error('Bundle must contain a "templates" array');
|
|
450
557
|
}
|
|
558
|
+
const { resolutions, onModelConflict } = opts;
|
|
451
559
|
const dir = mkdtempSync(join(tmpdir(), 'worca-import-'));
|
|
452
560
|
const bundlePath = join(dir, 'bundle.json');
|
|
453
561
|
try {
|
|
454
562
|
writeFileSync(bundlePath, JSON.stringify(bundle), 'utf8');
|
|
563
|
+
const resolutionsPath = _writeResolutionsFile(dir, resolutions);
|
|
455
564
|
runWorcaTemplates(
|
|
456
565
|
projectRoot,
|
|
457
|
-
|
|
566
|
+
_baseImportArgs(bundlePath, tier, resolutionsPath, onModelConflict),
|
|
458
567
|
{ timeout: 60000 },
|
|
459
568
|
);
|
|
460
569
|
const targetDir = dirForTier(projectRoot, tier);
|
|
@@ -593,6 +702,12 @@ export function createTemplatesRoutes() {
|
|
|
593
702
|
},
|
|
594
703
|
);
|
|
595
704
|
|
|
705
|
+
router.post(
|
|
706
|
+
'/templates/import/preview',
|
|
707
|
+
expressRaw({ type: 'application/zip', limit: '1mb' }),
|
|
708
|
+
(req, res) => handleImportPreview(req, res),
|
|
709
|
+
);
|
|
710
|
+
|
|
596
711
|
/**
|
|
597
712
|
* POST /api/projects/:projectId/templates/validate
|
|
598
713
|
* Body: { config }
|