@worca/app 1.3.0 → 1.4.0-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/README.md +85 -6
- package/agents/clarify.meta.json +1 -0
- package/agents/memoryDefragmenter.meta.json +2 -1
- package/agents/reviewer.meta.json +60 -0
- package/agents/worca-cc-code-reviewer.md +33 -0
- package/agents/worca-cc-memory-defragmenter.md +5 -3
- package/agents/workspaceScanner.meta.json +1 -0
- package/package.json +14 -10
- package/scripts/git-diff.mjs +25 -0
- package/scripts/gitDiff.meta.json +18 -0
- package/scripts/js-inline.mjs +11 -0
- package/scripts/js.meta.json +22 -0
- package/scripts/py-inline.py +27 -0
- package/scripts/py.meta.json +22 -0
- package/scripts/shell.meta.json +24 -0
- package/skills/worca/SKILL.md +3 -2
- package/src/cli/models.mjs +247 -0
- package/src/cli/render.mjs +72 -4
- package/src/cli/schedule.mjs +494 -0
- package/src/cli/worca-cc.mjs +1001 -22
- package/src/core/agent-registry.mjs +75 -23
- package/src/core/agent-store.mjs +51 -2
- package/src/core/artifacts.mjs +73 -10
- package/src/core/ask/events.mjs +119 -1
- package/src/core/ask/limits.mjs +32 -4
- package/src/core/ask/mcp-stdio.mjs +12 -0
- package/src/core/ask/model-deps.mjs +126 -0
- package/src/core/ask/model-proposal.mjs +370 -0
- package/src/core/ask/models.mjs +12 -0
- package/src/core/ask/policy-deps.mjs +124 -0
- package/src/core/ask/policy-proposal.mjs +363 -0
- package/src/core/ask/prompt.mjs +74 -9
- package/src/core/ask/proposal.mjs +54 -5
- package/src/core/ask/schedule-deps.mjs +83 -0
- package/src/core/ask/schedule-spec.mjs +310 -0
- package/src/core/ask/script-deps.mjs +357 -0
- package/src/core/ask/source-deps.mjs +52 -0
- package/src/core/ask/source-spec.mjs +157 -0
- package/src/core/ask/spawn.mjs +1 -0
- package/src/core/ask/store.mjs +6 -3
- package/src/core/ask/tool-deps.mjs +4 -0
- package/src/core/ask/tools.mjs +657 -37
- package/src/core/ask/turn.mjs +109 -2
- package/src/core/ask-files.mjs +406 -0
- package/src/core/ask-forms.mjs +195 -0
- package/src/core/ask-projection.mjs +72 -0
- package/src/core/bridge/errors.mjs +84 -0
- package/src/core/bridge/provider-ops.mjs +281 -0
- package/src/core/bridge/providers/copilot.mjs +269 -0
- package/src/core/bridge/providers/endpoint.mjs +257 -0
- package/src/core/bridge/registry.mjs +88 -0
- package/src/core/bridge/semaphore.mjs +73 -0
- package/src/core/bridge/server.mjs +184 -0
- package/src/core/bridge/telemetry.mjs +53 -0
- package/src/core/bridge/translate/request.mjs +252 -0
- package/src/core/bridge/translate/response.mjs +82 -0
- package/src/core/bridge/translate/stream.mjs +242 -0
- package/src/core/bridge/upstream.mjs +209 -0
- package/src/core/chat/command-router.mjs +58 -4
- package/src/core/chat/notifier.mjs +14 -1
- package/src/core/chat/renderers.mjs +35 -0
- package/src/core/claude-runner.mjs +126 -19
- package/src/core/config.mjs +212 -31
- package/src/core/cost-budget.mjs +3 -2
- package/src/core/db.mjs +169 -15
- package/src/core/failure-policy.mjs +10 -0
- package/src/core/fs-browse.mjs +16 -4
- package/src/core/git-info.mjs +22 -0
- package/src/core/graph/builtin-workflows.mjs +3 -1
- package/src/core/graph/exec-io.mjs +71 -0
- package/src/core/graph/executor.mjs +139 -70
- package/src/core/graph/human-evidence.mjs +131 -0
- package/src/core/graph/python-probe.mjs +172 -0
- package/src/core/graph/registry-ports.mjs +10 -6
- package/src/core/graph/scheduler.mjs +39 -24
- package/src/core/graph/script-child.mjs +81 -0
- package/src/core/graph/script-runner.mjs +597 -0
- package/src/core/graph/worca_script.py +207 -0
- package/src/core/guardrail-store.mjs +16 -0
- package/src/core/human-backfill.mjs +108 -0
- package/src/core/human-rate.mjs +17 -0
- package/src/core/index-html.mjs +6 -2
- package/src/core/memory-defrag-model.mjs +112 -0
- package/src/core/memory-store.mjs +70 -18
- package/src/core/memory-sync.mjs +22 -11
- package/src/core/metrics/read.mjs +4 -1
- package/src/core/metrics/record.mjs +50 -2
- package/src/core/metrics/sync.mjs +6 -4
- package/src/core/model-env.mjs +149 -0
- package/src/core/model-test.mjs +14 -1
- package/src/core/notifications.mjs +128 -0
- package/src/core/onboarding.mjs +8 -2
- package/src/core/orchestrator.mjs +357 -26
- package/src/core/phases.mjs +95 -6
- package/src/core/plugin-api.mjs +24 -7
- package/src/core/plugin-manifest.mjs +184 -18
- package/src/core/plugin-models.mjs +1 -0
- package/src/core/plugin-script-cases.mjs +118 -0
- package/src/core/plugin-store.mjs +163 -17
- package/src/core/plugin-workflows.mjs +71 -17
- package/src/core/policy/cache.mjs +116 -0
- package/src/core/policy/effective.mjs +175 -0
- package/src/core/policy/gate.mjs +91 -0
- package/src/core/policy/local.mjs +145 -0
- package/src/core/policy/registry.mjs +330 -0
- package/src/core/policy/scope.mjs +61 -0
- package/src/core/policy/state.mjs +79 -0
- package/src/core/policy/sync.mjs +513 -0
- package/src/core/protocol.mjs +43 -0
- package/src/core/run-harness.mjs +421 -72
- package/src/core/scheduler.mjs +980 -0
- package/src/core/script-bench.mjs +628 -0
- package/src/core/script-registry.mjs +116 -0
- package/src/core/script-store.mjs +563 -0
- package/src/core/settings.mjs +489 -14
- package/src/core/stats.mjs +33 -2
- package/src/core/workflow-export.mjs +94 -3
- package/src/core/workflow-share.mjs +67 -18
- package/src/core/workflows.mjs +47 -11
- package/src/core/workspaces.mjs +18 -12
- package/src/shared/forms/answer.mjs +164 -0
- package/src/shared/forms/catalog.mjs +91 -0
- package/src/shared/forms/form-def.mjs +290 -0
- package/src/shared/forms/layout.mjs +67 -0
- package/src/shared/forms/paths.mjs +47 -0
- package/src/shared/forms/project.mjs +309 -0
- package/src/shared/forms/schema.mjs +205 -0
- package/src/shared/graph/agent-meta.mjs +55 -5
- package/src/shared/graph/constants.mjs +14 -2
- package/src/shared/graph/flow-layout.mjs +2 -1
- package/src/shared/graph/isomorphic.mjs +5 -3
- package/src/shared/graph/manifest.mjs +22 -13
- package/src/shared/graph/ports.mjs +45 -19
- package/src/shared/graph/script-cases.mjs +257 -0
- package/src/shared/graph/script-icons.mjs +46 -0
- package/src/shared/graph/script-infer.mjs +259 -0
- package/src/shared/graph/script-meta.mjs +408 -0
- package/src/shared/graph/script-templates.mjs +201 -0
- package/src/shared/graph/template.mjs +4 -4
- package/src/shared/graph/validate.mjs +89 -16
- package/src/shared/human-estimate.mjs +100 -0
- package/src/shared/schedule/recurrence.mjs +353 -0
- package/src/shared/team-metrics/aggregate.mjs +51 -11
- package/{scripts → tools}/install.mjs +3 -3
- package/ui/public/app.js +4390 -683
- package/ui/public/artifact-picker.mjs +189 -0
- package/ui/public/ask/dom.mjs +121 -0
- package/ui/public/ask/form-preview.mjs +55 -0
- package/ui/public/ask/form-renderer.mjs +250 -0
- package/ui/public/ask/registry.mjs +53 -0
- package/ui/public/ask/widgets-display.mjs +370 -0
- package/ui/public/ask/widgets-input.mjs +624 -0
- package/ui/public/ask/widgets-layout.mjs +90 -0
- package/ui/public/ask-panel.mjs +401 -27
- package/ui/public/ask-run-card.mjs +1 -1
- package/ui/public/bridge-view.mjs +694 -0
- package/ui/public/chat-settings-view.mjs +24 -0
- package/ui/public/code-editor.mjs +181 -0
- package/ui/public/getting-started.mjs +34 -7
- package/ui/public/graph/composer.mjs +138 -10
- package/ui/public/graph/inspector.mjs +61 -58
- package/ui/public/graph/palette.mjs +27 -9
- package/ui/public/graph/run-decor.mjs +34 -17
- package/ui/public/graph/run-hosts.mjs +7 -1
- package/ui/public/graph/save-dialog.mjs +3 -0
- package/ui/public/graph/view.mjs +23 -7
- package/ui/public/guardrails-view.mjs +15 -3
- package/ui/public/guide-spot.mjs +87 -9
- package/ui/public/index.html +480 -141
- package/ui/public/memory-view.mjs +22 -4
- package/ui/public/models-view.mjs +162 -17
- package/ui/public/node-tunables.mjs +33 -4
- package/ui/public/plugins-view.mjs +23 -1
- package/ui/public/results-view.mjs +4 -2
- package/ui/public/schedule-sheet.mjs +430 -0
- package/ui/public/schedules-view.mjs +432 -0
- package/ui/public/script-bench-view.mjs +1154 -0
- package/ui/public/script-forms.mjs +282 -0
- package/ui/public/script-wizard.mjs +529 -0
- package/ui/public/scripts-view.mjs +868 -0
- package/ui/public/stats-view.mjs +159 -52
- package/ui/public/style.css +1461 -44
- package/ui/public/team-metrics-surfaces.mjs +77 -16
- package/ui/public/team-metrics-view.mjs +68 -5
- package/ui/public/team-policy-view.mjs +1402 -0
- package/ui/public/ui-level.mjs +237 -0
- package/ui/server.mjs +1827 -73
|
@@ -27,7 +27,7 @@ export function memoryRoute(scopeKey, name = '') {
|
|
|
27
27
|
return name ? `${base}/${encodeURIComponent(name)}` : base;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const BADGES = { fresh: ['No memory yet', ''], ok: ['Healthy', 'green'], due: ['Defragment due', 'amber'], overdue: ['Defragment overdue', 'red'] };
|
|
30
|
+
const BADGES = { fresh: ['No memory yet', ''], ok: ['Healthy', 'green'], due: ['Defragment due', 'amber'], overdue: ['Defragment overdue', 'red'], failing: ['Writes failing', 'red'] };
|
|
31
31
|
export function healthBadge(level) {
|
|
32
32
|
const [text, cls] = BADGES[level] || BADGES.fresh;
|
|
33
33
|
return { text, cls };
|
|
@@ -45,6 +45,17 @@ export function formatWhen(iso) {
|
|
|
45
45
|
|
|
46
46
|
const NO_HOST_HINT = 'Register a project on the Projects page to host the global defragment run.';
|
|
47
47
|
|
|
48
|
+
/** Settings › Memory's defragment model as the host hint names it — ` with Opus 5.5 · high` — from
|
|
49
|
+
* the report's `defragModel` ({ model, effort, label, stale } | null). '' when unset, so the unset
|
|
50
|
+
* sentence stays byte-identical. A model that left the catalog says so without naming a fallback:
|
|
51
|
+
* the run then uses the node layers — the host project's own pick or the template's model — and the
|
|
52
|
+
* global report cannot know which (memory-defrag-model.mjs). */
|
|
53
|
+
export function defragModelPhrase(dm) {
|
|
54
|
+
if (!dm || typeof dm.model !== 'string' || !dm.model) return '';
|
|
55
|
+
if (dm.stale) return ` without the Settings › Memory model (${dm.model} is no longer in the catalog)`;
|
|
56
|
+
return ` with ${dm.label || dm.model}${dm.effort ? ` · ${dm.effort}` : ''}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
/** The defragment glyph: three rows of blocks, the lower ones fragmented. Pure SVG elements (no
|
|
49
60
|
* text node), so the button's textContent stays its label alone. */
|
|
50
61
|
function defragIcon(doc) {
|
|
@@ -66,18 +77,24 @@ function defragIcon(doc) {
|
|
|
66
77
|
* hosts its own, so `host` is ignored there); with no registered project the button is disabled
|
|
67
78
|
* and the hint says so, in visible text as well as the title. While a run is live the button stays
|
|
68
79
|
* ENABLED, reads "Defragmenting… open the run" and carries `data-run-id`: the controller routes to
|
|
69
|
-
* `#running/<id>`.
|
|
80
|
+
* `#running/<id>`. The global host hint also names Settings › Memory's defragment model when one is
|
|
81
|
+
* set (`report.defragModel`, see defragModelPhrase).
|
|
70
82
|
*/
|
|
71
83
|
export function renderHealthCard(report, { doc = globalThis.document, host = null } = {}) {
|
|
72
84
|
const health = report?.health || {};
|
|
73
85
|
const isGlobal = String(report?.scope || 'global') === 'global';
|
|
74
86
|
const card = h(doc, 'div', 'card mem-health');
|
|
87
|
+
// Interface mode (docs/ui-levels.md): byte counters and Defragment are expert upkeep — until the
|
|
88
|
+
// store needs it. An overdue or failing store is a problem the owner must see at any mode.
|
|
89
|
+
card.dataset.minLevel = 'expert';
|
|
90
|
+
if (health.level === 'overdue' || health.level === 'failing') card.dataset.levelKeep = '1';
|
|
75
91
|
const head = h(doc, 'div', 'mem-health-head');
|
|
76
92
|
const { text, cls } = healthBadge(health.level);
|
|
77
93
|
head.appendChild(h(doc, 'span', `badge${cls ? ` ${cls}` : ''}`, text));
|
|
78
94
|
head.appendChild(h(doc, 'span', 'mem-counters',
|
|
79
95
|
`${plural(health.files || 0, 'file')} · ${health.bytes || 0} bytes · ${health.alwaysOnBytes || 0} bytes always loaded · ${plural(health.writesSinceDefrag || 0, 'write')} since the last defragment` +
|
|
80
|
-
(health.lastDefragAt ? ` · last defragmented ${formatWhen(health.lastDefragAt)}` : '')
|
|
96
|
+
(health.lastDefragAt ? ` · last defragmented ${formatWhen(health.lastDefragAt)}` : '') +
|
|
97
|
+
(health.failedWrites ? ` · ${plural(health.failedWrites, 'failed write')}` : '')));
|
|
81
98
|
// The ONE control sits at the head's right edge (margin-left:auto), styled like every other
|
|
82
99
|
// secondary pill (btn-ghost) with the defragment glyph before its label.
|
|
83
100
|
const runId = report?.defragRunId ? String(report.defragRunId) : '';
|
|
@@ -96,7 +113,7 @@ export function renderHealthCard(report, { doc = globalThis.document, host = nul
|
|
|
96
113
|
}
|
|
97
114
|
if (isGlobal && !runId) {
|
|
98
115
|
card.appendChild(h(doc, 'small', 'hint mem-host-hint',
|
|
99
|
-
host ? `Runs on ${host.name || host.key} — pick another project on the New pipeline page.` : NO_HOST_HINT));
|
|
116
|
+
host ? `Runs on ${host.name || host.key}${defragModelPhrase(report?.defragModel)} — pick another project on the New pipeline page.` : NO_HOST_HINT));
|
|
100
117
|
}
|
|
101
118
|
card.appendChild(h(doc, 'small', 'hint', 'A defragment run merges duplicate topics, splits overgrown files, drops stale rules and tightens hooks. It is an ordinary pipeline run; the previous files stay in History.'));
|
|
102
119
|
return card;
|
|
@@ -175,6 +192,7 @@ export function collectEditor(rootEl) {
|
|
|
175
192
|
/** The snapshot ring, newest first, one Restore per row (disabled while a defragment run is live). */
|
|
176
193
|
export function renderMemoryHistory(snapshots, { doc = globalThis.document, locked = false } = {}) {
|
|
177
194
|
const wrap = h(doc, 'div', 'mem-history');
|
|
195
|
+
wrap.dataset.minLevel = 'expert'; // snapshot restore: expert (docs/ui-levels.md)
|
|
178
196
|
wrap.appendChild(h(doc, 'div', 'mem-list-title', 'History'));
|
|
179
197
|
const list = Array.isArray(snapshots) ? [...snapshots].reverse() : [];
|
|
180
198
|
if (!list.length) { wrap.appendChild(h(doc, 'div', 'hist-empty', 'No snapshots yet — every write takes one.')); return wrap; }
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// mv-promote, mv-save, mv-cancel, mv-env-add, mv-env-rm) so app.js wires ONE
|
|
8
8
|
// delegated listener on the list container.
|
|
9
9
|
|
|
10
|
+
import { bridgedBadge, needsSignInPill, degradationLine, renderConnectionSection, collectConnection, applyConnectionMode } from './bridge-view.mjs';
|
|
11
|
+
|
|
10
12
|
function h(doc, tag, cls, text) {
|
|
11
13
|
const n = doc.createElement(tag);
|
|
12
14
|
if (cls) n.className = cls;
|
|
@@ -77,15 +79,54 @@ export function suggestDuplicateId(id, takenIds = []) {
|
|
|
77
79
|
* `globals` come MASKED from GET /api/models. `predefinedShadowedIds` marks
|
|
78
80
|
* built-ins currently overridden by a global entry.
|
|
79
81
|
*/
|
|
80
|
-
|
|
82
|
+
/**
|
|
83
|
+
* The catalog. `query` / `filter` narrow it and `collapsed` folds a group away: with the built-ins,
|
|
84
|
+
* a plugin's models and a team policy's all listed at once the page ran to several screens, and the
|
|
85
|
+
* entry you came for was never the one on top.
|
|
86
|
+
* @param {{query?:string, filter?:string, collapsed?:object, highlight?:string[]}} [o]
|
|
87
|
+
*/
|
|
88
|
+
export function renderModelsList({ globals = [], legacy = [], plugins = [], policy = [], predefined = [], efforts = [], hideBuiltin = false, projectName = '', query = '', filter = 'all', collapsed = {}, highlight = [] } = {}, { doc = globalThis.document } = {}) {
|
|
81
89
|
const root = h(doc, 'div', 'mv-list');
|
|
82
90
|
const predefLc = new Set(predefined.map((m) => m.id.toLowerCase()));
|
|
83
91
|
const pluginLc = new Set(plugins.map((m) => m.id.toLowerCase()));
|
|
92
|
+
const q = String(query || '').trim().toLowerCase();
|
|
93
|
+
const hi = new Set((highlight || []).map((x) => String(x).toLowerCase()));
|
|
94
|
+
const searching = !!q || filter !== 'all';
|
|
95
|
+
|
|
96
|
+
/** Does this entry survive the search box and the chip? */
|
|
97
|
+
const keep = (m, source) => {
|
|
98
|
+
if (filter === 'imported' && !hi.has(String(m.id).toLowerCase())) return false;
|
|
99
|
+
if (filter === 'needs-setup' && !m.needsSignIn) return false;
|
|
100
|
+
if (!['all', 'imported', 'needs-setup'].includes(filter) && filter !== source) return false;
|
|
101
|
+
if (!q) return true;
|
|
102
|
+
return [m.id, m.label, m.plugin, m.home, m.upstream && m.upstream.model].filter(Boolean).some((v) => String(v).toLowerCase().includes(q));
|
|
103
|
+
};
|
|
84
104
|
|
|
85
|
-
|
|
105
|
+
// Each group can fold: the header is the control, and its count is the answer to "is my model
|
|
106
|
+
// in there?" without opening it.
|
|
107
|
+
const sections = [];
|
|
108
|
+
const section = (title, hint, key = '') => {
|
|
86
109
|
const s = h(doc, 'div', 'mv-section');
|
|
87
|
-
s.
|
|
88
|
-
|
|
110
|
+
s.dataset.section = key;
|
|
111
|
+
const head = h(doc, 'div', 'mv-section-head');
|
|
112
|
+
const btn = h(doc, 'button', 'mv-sec-toggle');
|
|
113
|
+
btn.type = 'button';
|
|
114
|
+
btn.dataset.section = key;
|
|
115
|
+
btn.appendChild(h(doc, 'span', 'mv-sec-caret', '▾'));
|
|
116
|
+
btn.appendChild(h(doc, 'h3', 'mv-section-title', title));
|
|
117
|
+
btn.appendChild(h(doc, 'span', 'mv-sec-count', ''));
|
|
118
|
+
head.appendChild(btn);
|
|
119
|
+
s.appendChild(head);
|
|
120
|
+
if (hint) s.appendChild(h(doc, 'small', 'hint mv-sec-hint', hint));
|
|
121
|
+
const body = h(doc, 'div', 'mv-sec-body');
|
|
122
|
+
s.appendChild(body);
|
|
123
|
+
// Searching opens every group that still has a hit — a match hidden inside a fold is a bug.
|
|
124
|
+
const folded = !searching && !!collapsed[key];
|
|
125
|
+
s.classList.toggle('is-folded', folded);
|
|
126
|
+
btn.setAttribute('aria-expanded', folded ? 'false' : 'true');
|
|
127
|
+
sections.push({ el: s, body, btn, key });
|
|
128
|
+
// The rows go into the body; every caller appends to the section, so proxy it.
|
|
129
|
+
s.appendChild = (node) => body.appendChild(node);
|
|
89
130
|
return s;
|
|
90
131
|
};
|
|
91
132
|
|
|
@@ -100,6 +141,26 @@ export function renderModelsList({ globals = [], legacy = [], plugins = [], pred
|
|
|
100
141
|
return b;
|
|
101
142
|
};
|
|
102
143
|
|
|
144
|
+
// ── Toolbar: search + the chip that says which layer you are looking at ──
|
|
145
|
+
const bar = h(doc, 'div', 'mv-toolbar');
|
|
146
|
+
const search = h(doc, 'input', 'input mv-search');
|
|
147
|
+
search.type = 'search';
|
|
148
|
+
search.placeholder = 'Search models by id, label or upstream…';
|
|
149
|
+
search.value = query || '';
|
|
150
|
+
search.setAttribute('aria-label', 'Search models');
|
|
151
|
+
bar.appendChild(search);
|
|
152
|
+
const chips = h(doc, 'div', 'mv-filters');
|
|
153
|
+
const CHIPS = [['all', 'All'], ['global', 'Yours'], ['builtin', 'Built-in'], ['plugin', 'Plugin'], ['policy', 'Team'], ['needs-setup', 'Needs setup']];
|
|
154
|
+
if (highlight.length) CHIPS.push(['imported', 'Just imported']);
|
|
155
|
+
for (const [id, label] of CHIPS) {
|
|
156
|
+
const c = h(doc, 'button', `mv-filter${filter === id ? ' on' : ''}`, label);
|
|
157
|
+
c.type = 'button'; c.dataset.filter = id;
|
|
158
|
+
c.setAttribute('aria-pressed', filter === id ? 'true' : 'false');
|
|
159
|
+
chips.appendChild(c);
|
|
160
|
+
}
|
|
161
|
+
bar.appendChild(chips);
|
|
162
|
+
root.appendChild(bar);
|
|
163
|
+
|
|
103
164
|
// ── Hide built-in models (#422) — one checkbox, top of the pane ──
|
|
104
165
|
const hideRow = h(doc, 'div', 'mv-hide-builtin-row');
|
|
105
166
|
const hideLabel = h(doc, 'label', 'check-row');
|
|
@@ -114,11 +175,11 @@ export function renderModelsList({ globals = [], legacy = [], plugins = [], pred
|
|
|
114
175
|
root.appendChild(hideRow);
|
|
115
176
|
|
|
116
177
|
// ── Your models (global) ──
|
|
117
|
-
const yours = section('Your models', 'Defined once, available in every project. An entry with a built-in id overrides that built-in.');
|
|
178
|
+
const yours = section('Your models', 'Defined once, available in every project. An entry with a built-in id overrides that built-in.', 'global');
|
|
118
179
|
if (!globals.length) {
|
|
119
180
|
yours.appendChild(h(doc, 'div', 'hist-empty', 'No global models yet — Add model to define one.'));
|
|
120
181
|
}
|
|
121
|
-
for (const m of globals) {
|
|
182
|
+
for (const m of globals.filter((x) => keep(x, 'global'))) {
|
|
122
183
|
const card = h(doc, 'section', 'card mv-card');
|
|
123
184
|
card.dataset.id = m.id;
|
|
124
185
|
const body = h(doc, 'div', 'mv-body');
|
|
@@ -128,14 +189,22 @@ export function renderModelsList({ globals = [], legacy = [], plugins = [], pred
|
|
|
128
189
|
else if (pluginLc.has(m.id.toLowerCase())) head.appendChild(h(doc, 'span', 'badge violet mv-shadow', 'overrides plugin'));
|
|
129
190
|
const rb = routedBadge(m);
|
|
130
191
|
if (rb) head.appendChild(rb);
|
|
192
|
+
// Model bridge (model-bridge-design.md §8.5): the provider badge, and the
|
|
193
|
+
// blocking "needs sign-in" pill when that provider is not usable yet.
|
|
194
|
+
const bb = bridgedBadge(m, { doc });
|
|
195
|
+
if (bb) head.appendChild(bb);
|
|
196
|
+
const ns = needsSignInPill(m, { doc });
|
|
197
|
+
if (ns) head.appendChild(ns);
|
|
131
198
|
// The §4.6 "unreliable" badge is meaningless once an override GOVERNS this
|
|
132
199
|
// model's spend — and the backend only lifts the stored flag on the model's
|
|
133
200
|
// next result event, so suppress it here the moment pricing is pinned.
|
|
134
201
|
if (m.costUnreliable && !m.cost) head.appendChild(h(doc, 'span', 'badge waiting mv-cost', 'cost not verified'));
|
|
135
202
|
if (m.cost) head.appendChild(h(doc, 'span', 'badge violet mv-cost-pinned', m.cost.free ? 'free' : 'priced'));
|
|
136
203
|
body.appendChild(head);
|
|
137
|
-
const bits = [m.id, effortsSummary(m.efforts, efforts), envSummary(m.env), costSummary(m.cost)].filter(Boolean);
|
|
204
|
+
const bits = [m.id, effortsSummary(m.efforts, efforts), m.upstream ? `→ ${m.upstream.model}` : '', envSummary(m.env), costSummary(m.cost)].filter(Boolean);
|
|
138
205
|
body.appendChild(h(doc, 'small', 'mv-summary hint', bits.join(' — ')));
|
|
206
|
+
const deg = degradationLine(m);
|
|
207
|
+
if (deg) body.appendChild(h(doc, 'small', 'mv-degradation hint', deg));
|
|
139
208
|
body.appendChild(h(doc, 'small', 'mv-test-result hint')); // app.js paints the Test outcome here
|
|
140
209
|
card.appendChild(body);
|
|
141
210
|
const del = h(doc, 'button', 'btn-ghost mv-delete', 'Delete');
|
|
@@ -185,8 +254,8 @@ export function renderModelsList({ globals = [], legacy = [], plugins = [], pred
|
|
|
185
254
|
const globalLc = new Set(globals.map((m) => m.id.toLowerCase()));
|
|
186
255
|
if (plugins.length) {
|
|
187
256
|
const plug = section('From plugins',
|
|
188
|
-
'Installed by plugins — read-only and updated with the plugin. "Edit a copy" clones one into Your models, which then overrides it.');
|
|
189
|
-
for (const m of plugins) {
|
|
257
|
+
'Installed by plugins — read-only and updated with the plugin. "Edit a copy" clones one into Your models, which then overrides it.', 'plugin');
|
|
258
|
+
for (const m of plugins.filter((x) => keep(x, 'plugin'))) {
|
|
190
259
|
const card = h(doc, 'section', 'card mv-card mv-plugin');
|
|
191
260
|
card.dataset.id = m.id;
|
|
192
261
|
card.dataset.plugin = m.plugin;
|
|
@@ -197,13 +266,19 @@ export function renderModelsList({ globals = [], legacy = [], plugins = [], pred
|
|
|
197
266
|
if (globalLc.has(m.id.toLowerCase())) head.appendChild(h(doc, 'span', 'badge violet mv-shadowed', 'overridden by your copy'));
|
|
198
267
|
const prb = routedBadge(m);
|
|
199
268
|
if (prb) head.appendChild(prb);
|
|
269
|
+
const pbb = bridgedBadge(m, { doc });
|
|
270
|
+
if (pbb) head.appendChild(pbb);
|
|
271
|
+
const pns = needsSignInPill(m, { doc });
|
|
272
|
+
if (pns) head.appendChild(pns);
|
|
200
273
|
// Same rule as a global card: a manifest-pinned price governs the spend,
|
|
201
274
|
// so the §4.6 "unreliable" flag says nothing about it.
|
|
202
275
|
if (m.costUnreliable && !m.cost) head.appendChild(h(doc, 'span', 'badge waiting mv-cost', 'cost not verified'));
|
|
203
276
|
if (m.cost) head.appendChild(h(doc, 'span', 'badge violet mv-cost-pinned', m.cost.free ? 'free' : 'priced'));
|
|
204
277
|
body.appendChild(head);
|
|
205
|
-
const bits = [m.id, effortsSummary(m.efforts, efforts), envSummary(m.env), costSummary(m.cost)].filter(Boolean);
|
|
278
|
+
const bits = [m.id, effortsSummary(m.efforts, efforts), m.upstream ? `→ ${m.upstream.model}` : '', envSummary(m.env), costSummary(m.cost)].filter(Boolean);
|
|
206
279
|
body.appendChild(h(doc, 'small', 'mv-summary hint', bits.join(' — ')));
|
|
280
|
+
const pdeg = degradationLine(m);
|
|
281
|
+
if (pdeg) body.appendChild(h(doc, 'small', 'mv-degradation hint', pdeg));
|
|
207
282
|
for (const s of m.secrets || []) {
|
|
208
283
|
body.appendChild(h(doc, 'small', `mv-secret hint${s.set ? '' : ' err'}`,
|
|
209
284
|
s.set ? `secret ${s.key}: set` : `secret ${s.key}: NOT SET — configure it in the plugin's settings`));
|
|
@@ -226,12 +301,41 @@ export function renderModelsList({ globals = [], legacy = [], plugins = [], pred
|
|
|
226
301
|
root.appendChild(plug);
|
|
227
302
|
}
|
|
228
303
|
|
|
304
|
+
// ── From team policy (read-only; team-policy design §8) ──
|
|
305
|
+
if (policy.length) {
|
|
306
|
+
const pol = section('From team policy',
|
|
307
|
+
'Shipped by a team policy — read-only and updated when the policy changes. Add a model with the same id to Your models to override one on this machine.', 'policy');
|
|
308
|
+
for (const m of policy.filter((x) => keep(x, 'policy'))) {
|
|
309
|
+
const card = h(doc, 'section', 'card mv-card mv-policy');
|
|
310
|
+
card.dataset.id = m.id;
|
|
311
|
+
const body = h(doc, 'div', 'mv-body');
|
|
312
|
+
const head = h(doc, 'div', 'mv-head');
|
|
313
|
+
head.appendChild(h(doc, 'b', 'mv-name', m.label || m.id));
|
|
314
|
+
const badge = h(doc, 'span', 'badge blue mv-origin', 'policy');
|
|
315
|
+
badge.title = `Team policy on ${m.home}`;
|
|
316
|
+
head.appendChild(badge);
|
|
317
|
+
if (globalLc.has(m.id.toLowerCase())) head.appendChild(h(doc, 'span', 'badge violet mv-shadowed', 'overridden by your copy'));
|
|
318
|
+
const rb = routedBadge(m);
|
|
319
|
+
if (rb) head.appendChild(rb);
|
|
320
|
+
body.appendChild(head);
|
|
321
|
+
const bits = [m.id, effortsSummary(m.efforts, efforts), envSummary(m.env), m.home ? `policy ${m.home}` : ''].filter(Boolean);
|
|
322
|
+
body.appendChild(h(doc, 'small', 'mv-summary hint', bits.join(' — ')));
|
|
323
|
+
body.appendChild(h(doc, 'small', 'mv-test-result hint'));
|
|
324
|
+
card.appendChild(body);
|
|
325
|
+
const tst = h(doc, 'button', 'btn-ghost mv-test', 'Test');
|
|
326
|
+
tst.type = 'button'; tst.dataset.id = m.id;
|
|
327
|
+
card.appendChild(tst);
|
|
328
|
+
pol.appendChild(card);
|
|
329
|
+
}
|
|
330
|
+
root.appendChild(pol);
|
|
331
|
+
}
|
|
332
|
+
|
|
229
333
|
// ── Built-ins (read-only) ──
|
|
230
334
|
const builtins = section('Built-in models', hideBuiltin
|
|
231
335
|
? `Hidden from every picker (${predefined.length} built-in${predefined.length === 1 ? '' : 's'}) — untick the box above to show them.`
|
|
232
|
-
: 'Shipped with worca. Add a model with the same id to override its label, efforts, or routing.');
|
|
336
|
+
: 'Shipped with worca. Add a model with the same id to override its label, efforts, or routing.', 'builtin');
|
|
233
337
|
builtins.classList.add(hideBuiltin ? 'mv-builtins-hidden' : 'mv-builtins-shown');
|
|
234
|
-
for (const m of hideBuiltin ? [] : predefined) {
|
|
338
|
+
for (const m of (hideBuiltin ? [] : predefined).filter((x) => keep(x, 'builtin'))) {
|
|
235
339
|
const row = h(doc, 'div', 'mv-builtin');
|
|
236
340
|
row.dataset.id = m.id;
|
|
237
341
|
row.appendChild(h(doc, 'b', 'mv-name', m.label));
|
|
@@ -242,6 +346,18 @@ export function renderModelsList({ globals = [], legacy = [], plugins = [], pred
|
|
|
242
346
|
builtins.appendChild(row);
|
|
243
347
|
}
|
|
244
348
|
root.appendChild(builtins);
|
|
349
|
+
|
|
350
|
+
// Counts on every header, and a group with nothing left drops out while a search is on.
|
|
351
|
+
let shown = 0;
|
|
352
|
+
for (const sec of sections) {
|
|
353
|
+
const n = sec.body.querySelectorAll('.mv-card, .mv-builtin').length;
|
|
354
|
+
shown += n;
|
|
355
|
+
sec.btn.querySelector('.mv-sec-count').textContent = String(n);
|
|
356
|
+
if (searching && !n) sec.el.classList.add('hidden');
|
|
357
|
+
}
|
|
358
|
+
if (searching && !shown) {
|
|
359
|
+
root.appendChild(h(doc, 'div', 'hist-empty mv-no-hits', q ? `No model matches “${query}”.` : 'No model in this group.'));
|
|
360
|
+
}
|
|
245
361
|
return root;
|
|
246
362
|
}
|
|
247
363
|
|
|
@@ -278,7 +394,7 @@ function envRow(doc, key = '', value = '') {
|
|
|
278
394
|
* Returns detached DOM; app.js wires mv-save / mv-cancel / mv-env-add /
|
|
279
395
|
* mv-env-rm and calls collectModelEditor on save.
|
|
280
396
|
*/
|
|
281
|
-
export function renderModelEditor(model, efforts, { doc = globalThis.document } = {}) {
|
|
397
|
+
export function renderModelEditor(model, efforts, { doc = globalThis.document, providers = null, copilotModels = [] } = {}) {
|
|
282
398
|
const editing = !!model;
|
|
283
399
|
const root = h(doc, 'section', 'card mv-editor');
|
|
284
400
|
root.dataset.mode = editing ? 'edit' : 'create';
|
|
@@ -313,6 +429,11 @@ export function renderModelEditor(model, efforts, { doc = globalThis.document }
|
|
|
313
429
|
labelInput.value = editing ? (model.label === model.id ? '' : model.label) : '';
|
|
314
430
|
grid.appendChild(field('Label', labelInput));
|
|
315
431
|
|
|
432
|
+
// ── Connection (model-bridge-design.md §8.3): direct / env / provider ──
|
|
433
|
+
// Rendered first among the routing controls: it decides whether the env
|
|
434
|
+
// rows below carry the routing or Worca's own bridge does.
|
|
435
|
+
grid.appendChild(field('Connection', renderConnectionSection(model, { doc, providers, copilotModels })));
|
|
436
|
+
|
|
316
437
|
const effWrap = h(doc, 'div', 'mv-efforts');
|
|
317
438
|
const selected = new Set(editing && Array.isArray(model.efforts) ? model.efforts : efforts);
|
|
318
439
|
for (const e of efforts) {
|
|
@@ -323,7 +444,9 @@ export function renderModelEditor(model, efforts, { doc = globalThis.document }
|
|
|
323
444
|
lab.appendChild(h(doc, 'span', null, e));
|
|
324
445
|
effWrap.appendChild(lab);
|
|
325
446
|
}
|
|
326
|
-
|
|
447
|
+
const effField = field('Supported efforts', effWrap, 'All checked = every effort (the default).');
|
|
448
|
+
effField.querySelector('.hint').classList.add('mv-efforts-hint'); // applyConnectionMode rewrites it
|
|
449
|
+
grid.appendChild(effField);
|
|
327
450
|
|
|
328
451
|
const envWrap = h(doc, 'div', 'mv-env');
|
|
329
452
|
const rows = editing && model.env ? Object.entries(model.env) : [];
|
|
@@ -331,7 +454,7 @@ export function renderModelEditor(model, efforts, { doc = globalThis.document }
|
|
|
331
454
|
const add = h(doc, 'button', 'btn-ghost mv-env-add', '+ env var');
|
|
332
455
|
add.type = 'button';
|
|
333
456
|
grid.appendChild(field('Routing env (merged into the claude spawn for this model)', envWrap,
|
|
334
|
-
'e.g. ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN. ANTHROPIC_MODEL sets the wire id sent to --model (the id above stays worca’s handle). Stored values show masked; leave masked to keep. WORCA_* and process keys are reserved.'));
|
|
457
|
+
'e.g. ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN. ANTHROPIC_MODEL sets the wire id sent to --model (the id above stays worca’s handle). Stored values show masked; leave masked to keep. WORCA_* and process keys are reserved. Through a provider, the bridge owns ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_API_KEY / ANTHROPIC_MODEL — setting them here is refused.'));
|
|
335
458
|
const envBtns = h(doc, 'div', 'mv-env-btns');
|
|
336
459
|
envBtns.appendChild(add);
|
|
337
460
|
if (rows.length) {
|
|
@@ -386,6 +509,7 @@ export function renderModelEditor(model, efforts, { doc = globalThis.document }
|
|
|
386
509
|
|
|
387
510
|
root.appendChild(grid);
|
|
388
511
|
setModelCost(root, editing ? model.cost : null); // grid is attached now — the block is reachable from root
|
|
512
|
+
applyConnectionModeIn(root); // efforts hint + collapse follow the Connection (now reachable)
|
|
389
513
|
const msg = h(doc, 'p', 'form-msg mv-editor-msg');
|
|
390
514
|
msg.setAttribute('aria-live', 'polite');
|
|
391
515
|
root.appendChild(msg);
|
|
@@ -486,6 +610,10 @@ export function collectModelEditor(rootEl) {
|
|
|
486
610
|
cost = { perMtok }; // empty -> the server rejects it by name, surfaced in the form
|
|
487
611
|
}
|
|
488
612
|
|
|
613
|
+
// Connection (model-bridge-design.md §8.3): the object to store, or null to
|
|
614
|
+
// clear — like 'cli' for pricing, the form shows the truth.
|
|
615
|
+
const { upstream } = collectConnection(rootEl);
|
|
616
|
+
|
|
489
617
|
const body = {
|
|
490
618
|
...(editing ? {} : { id }),
|
|
491
619
|
label,
|
|
@@ -493,10 +621,20 @@ export function collectModelEditor(rootEl) {
|
|
|
493
621
|
efforts: efforts.length === allCount ? [] : efforts,
|
|
494
622
|
env,
|
|
495
623
|
cost,
|
|
624
|
+
// Create mode has nothing to clear, so a null upstream is simply omitted
|
|
625
|
+
// and the POST body stays byte-identical for a non-bridged entry.
|
|
626
|
+
...(upstream === undefined || (!editing && upstream === null) ? {} : { upstream }),
|
|
496
627
|
};
|
|
497
628
|
return { id: editing ? id : null, body };
|
|
498
629
|
}
|
|
499
630
|
|
|
631
|
+
/** applyConnectionMode over an editor root (re-exported here so app.js's one
|
|
632
|
+
* delegated `change` handler for the editor needs a single import). */
|
|
633
|
+
export function applyConnectionModeIn(rootEl) {
|
|
634
|
+
const conn = rootEl && rootEl.querySelector && rootEl.querySelector('.mv-conn');
|
|
635
|
+
if (conn) applyConnectionMode(conn);
|
|
636
|
+
}
|
|
637
|
+
|
|
500
638
|
// ── Share-as-plugin export wizard (design §9.5) ─────────────────────────────
|
|
501
639
|
|
|
502
640
|
// Credential-looking env keys default to "Require at install". Word-ish
|
|
@@ -620,7 +758,14 @@ export function deleteRefsSummary(id, refs) {
|
|
|
620
758
|
...((refs && refs.nodes) || []).map((n) => n.projectKey),
|
|
621
759
|
...((refs && refs.steps) || []).map((s) => s.projectKey),
|
|
622
760
|
]).size;
|
|
623
|
-
|
|
761
|
+
// Settings › Memory's defragment model is the one GLOBAL ref (globalModelRefs `memoryDefrag`).
|
|
762
|
+
const defrag = !!(refs && refs.memoryDefrag);
|
|
763
|
+
if (!nodes && !steps) {
|
|
764
|
+
return defrag
|
|
765
|
+
? `Delete model "${id}"? Memory defragment runs use it (Settings › Memory) — that setting is cleared and they fall back to the default.`
|
|
766
|
+
: `Delete model "${id}"? No pipeline configuration references it.`;
|
|
767
|
+
}
|
|
624
768
|
return `Delete model "${id}"? This also clears ${nodes} node selection${nodes === 1 ? '' : 's'} and ` +
|
|
625
|
-
`${steps} role selection${steps === 1 ? '' : 's'} across ${projects} project${projects === 1 ? '' : 's'}
|
|
769
|
+
`${steps} role selection${steps === 1 ? '' : 's'} across ${projects} project${projects === 1 ? '' : 's'}` +
|
|
770
|
+
(defrag ? ', and the Memory defragment model (Settings › Memory).' : '.');
|
|
626
771
|
}
|
|
@@ -142,9 +142,13 @@ export function pruneNodeSelection(row, next = {}) {
|
|
|
142
142
|
// the setters ("select a model before choosing an effort").
|
|
143
143
|
const inheritPair = (eff.model || '') === (row.def.model || '')
|
|
144
144
|
&& (eff.effort || '') === (row.def.effort || '');
|
|
145
|
+
// A PINNED row (Settings › Memory owns the pair) never edits model/effort, and the setters
|
|
146
|
+
// REPLACE both on every save: re-send the project's own stored pick untouched, so saving
|
|
147
|
+
// another tunable cannot erase it — it applies again the moment the setting is cleared.
|
|
148
|
+
const kept = row.pinned && row.storedPair ? row.storedPair : null;
|
|
145
149
|
return {
|
|
146
|
-
model: inheritPair ? '' : (eff.model || ''),
|
|
147
|
-
effort: inheritPair || !eff.model ? '' : eff.effort,
|
|
150
|
+
model: kept ? kept.model : (inheritPair ? '' : (eff.model || '')),
|
|
151
|
+
effort: kept ? kept.effort : (inheritPair || !eff.model ? '' : eff.effort),
|
|
148
152
|
fanOut: !!eff.fanOut === !!row.def.fanOut ? null : !!eff.fanOut,
|
|
149
153
|
askQuestions: row.askQuestions === null || row.questionsLocked
|
|
150
154
|
? undefined // no capability / locked: never persist a value for it
|
|
@@ -168,6 +172,25 @@ export function buildGraphNodeRows(tpl, registry, runConfig, opts = {}) {
|
|
|
168
172
|
// app.js hands its own panelPortsFn (falls back to the Composer's index for
|
|
169
173
|
// agents the palette omits); the card and the tests use the registry alone.
|
|
170
174
|
const portsFn = typeof opts.portsFn === 'function' ? opts.portsFn : portsFnFor(reg);
|
|
175
|
+
// Settings › Memory (the built-in Memory defragment workflow only): GET /api/workflows/:id
|
|
176
|
+
// stamps `pinnedAgentModel` when a valid pair is stored, and every run of the workflow then
|
|
177
|
+
// uses it for every agent node whatever the project picked (resolveGraph `agentPair`). So the
|
|
178
|
+
// rows SHOW the pair as the default and carry `pinned`; both renderers lock model and effort —
|
|
179
|
+
// an editable model the run ignores would be a control that lies.
|
|
180
|
+
const pin = tpl && tpl.pinnedAgentModel && typeof tpl.pinnedAgentModel.model === 'string' && tpl.pinnedAgentModel.model
|
|
181
|
+
? tpl.pinnedAgentModel : null;
|
|
182
|
+
const withoutPair = ({ model: _m, effort: _e, ...rest }) => rest;
|
|
183
|
+
// A pinned row re-sends the project's hidden pick on every save (pruneNodeSelection), and the
|
|
184
|
+
// setter refuses a model that left the catalog or an effort it no longer offers — so heal the
|
|
185
|
+
// pick against the catalog the caller holds (`opts.models`), dropping exactly what an unpinned
|
|
186
|
+
// row's selects would drop. No catalog (not loaded yet) → the pick as stored.
|
|
187
|
+
const catalog = Array.isArray(opts.models) && opts.models.length ? opts.models : null;
|
|
188
|
+
const healPair = (model, effort) => {
|
|
189
|
+
if (!model || !catalog) return { model: model || '', effort: (model && effort) || '' };
|
|
190
|
+
const hit = catalog.find((m) => m && m.id === model);
|
|
191
|
+
if (!hit) return { model: '', effort: '' };
|
|
192
|
+
return { model, effort: effort && Array.isArray(hit.efforts) && hit.efforts.includes(effort) ? effort : '' };
|
|
193
|
+
};
|
|
171
194
|
const order = classifyLoops(tpl, portsFn).launchOrder;
|
|
172
195
|
const byId = new Map(tpl.nodes.map((n) => [n.id, n]));
|
|
173
196
|
const rank = new Map(order.map((id, i) => [id, i]));
|
|
@@ -176,8 +199,12 @@ export function buildGraphNodeRows(tpl, registry, runConfig, opts = {}) {
|
|
|
176
199
|
for (const node of agentNodes) {
|
|
177
200
|
const meta = reg[node.key] || null;
|
|
178
201
|
const role = legacySteps ? node.key : null;
|
|
179
|
-
const
|
|
180
|
-
const
|
|
202
|
+
const stored = { ...(role ? legacySteps[role] : null), ...nodes[node.id] };
|
|
203
|
+
const authored = (node.config && typeof node.config === 'object') ? node.config : {};
|
|
204
|
+
const saved = pin ? withoutPair(stored) : stored;
|
|
205
|
+
const wfDef = pin
|
|
206
|
+
? { ...withoutPair(authored), model: pin.model, ...(typeof pin.effort === 'string' && pin.effort ? { effort: pin.effort } : {}) }
|
|
207
|
+
: authored;
|
|
181
208
|
const metaFan = meta && typeof meta.fanOut === 'boolean' ? meta.fanOut : false;
|
|
182
209
|
const metaAsks = !!(meta && meta.asksQuestions);
|
|
183
210
|
const metaLocked = !!(meta && meta.questionsLocked);
|
|
@@ -195,6 +222,8 @@ export function buildGraphNodeRows(tpl, registry, runConfig, opts = {}) {
|
|
|
195
222
|
def: t.def, override: t.override,
|
|
196
223
|
modified: modifiedFieldsOf(t, t.def,
|
|
197
224
|
{ asksQuestions: metaAsks, questionsLocked: metaLocked }).length > 0,
|
|
225
|
+
// `storedPair`: the project's own pick the pin hides — pruneNodeSelection re-sends it.
|
|
226
|
+
...(pin ? { pinned: 'settings', storedPair: healPair(stored.model, stored.effort) } : {}),
|
|
198
227
|
});
|
|
199
228
|
}
|
|
200
229
|
return rows;
|
|
@@ -34,7 +34,7 @@ function iconBtn(doc, cls, icon, label) {
|
|
|
34
34
|
function contribSummary(c) {
|
|
35
35
|
const n = (v) => (Array.isArray(v) ? v.length : (Number.isFinite(v) ? v : 0));
|
|
36
36
|
const parts = [
|
|
37
|
-
[n(c && c.agents), 'agent'], [n(c && c.taskSources), 'source'],
|
|
37
|
+
[n(c && c.agents), 'agent'], [n(c && c.scripts), 'script'], [n(c && c.taskSources), 'source'],
|
|
38
38
|
[n(c && c.chatChannels), 'chat channel'], [n(c && c.models), 'model'],
|
|
39
39
|
[n(c && c.skills), 'skill'], [n(c && c.workflows), 'workflow'],
|
|
40
40
|
].filter(([k]) => k > 0).map(([k, w]) => `${k} ${w}${k > 1 ? 's' : ''}`);
|
|
@@ -59,6 +59,8 @@ export function renderPluginList(plugins, { doc = globalThis.document, channelSt
|
|
|
59
59
|
if (p.linked) head.appendChild(h(doc, 'span', 'badge waiting pl-linked', 'linked'));
|
|
60
60
|
if (p.apiMismatch) head.appendChild(h(doc, 'span', 'badge amber pl-api-mismatch', 'needs update'));
|
|
61
61
|
else if (p.broken) head.appendChild(h(doc, 'span', 'badge red pl-broken', 'broken'));
|
|
62
|
+
// A shipped python script on a host without python: a chip, never a block.
|
|
63
|
+
if (p.pythonMissing) head.appendChild(h(doc, 'span', 'badge amber pl-python-missing', 'python not found'));
|
|
62
64
|
// Enabling a plugin acts on a live system the moment it flips, so it reads
|
|
63
65
|
// as a switch. `.switch` MUST be the input's immediate next sibling — the
|
|
64
66
|
// `.sw-input:checked + .switch` rule is what paints the on state.
|
|
@@ -100,6 +102,7 @@ export function renderPluginList(plugins, { doc = globalThis.document, channelSt
|
|
|
100
102
|
const b = h(doc, 'button', `btn-ghost ${cls}`, label);
|
|
101
103
|
b.type = 'button';
|
|
102
104
|
b.dataset.name = p.name;
|
|
105
|
+
if (cls === 'pl-doctor') b.dataset.minLevel = 'expert'; // diagnostics (docs/ui-levels.md)
|
|
103
106
|
actions.appendChild(b);
|
|
104
107
|
}
|
|
105
108
|
card.appendChild(actions);
|
|
@@ -126,6 +129,7 @@ export function channelBadge(doc, c) {
|
|
|
126
129
|
// listener. Empty/nullish input -> childless container (app.js skips mounting).
|
|
127
130
|
export function renderOrphanList(orphans, { doc = globalThis.document } = {}) {
|
|
128
131
|
const root = h(doc, 'div', 'pl-orphans');
|
|
132
|
+
root.dataset.minLevel = 'expert'; // purge of leftover data (docs/ui-levels.md)
|
|
129
133
|
if (!orphans || !orphans.length) return root;
|
|
130
134
|
root.appendChild(h(doc, 'h3', 'pl-orphans-title', 'Leftover data'));
|
|
131
135
|
for (const o of orphans) {
|
|
@@ -159,6 +163,24 @@ export function renderInstallConsent(entry, inventory, { doc = globalThis.docume
|
|
|
159
163
|
for (const a of inv.agents || []) {
|
|
160
164
|
agents.appendChild(h(doc, 'div', 'pl-consent-row mono',
|
|
161
165
|
`${a.key} — tools: ${(a.tools || []).join(', ') || 'none declared'}`));
|
|
166
|
+
// Ask forms (spec §10): how many question forms this agent can put on
|
|
167
|
+
// screen, and which file types they may display from the run folder. An
|
|
168
|
+
// older snapshot has neither key, so both are read defensively.
|
|
169
|
+
const forms = Array.isArray(a.forms) ? a.forms : [];
|
|
170
|
+
if (forms.length) {
|
|
171
|
+
const types = Array.isArray(a.fileTypes) ? a.fileTypes : [];
|
|
172
|
+
agents.appendChild(h(doc, 'div', 'pl-consent-row pl-consent-forms',
|
|
173
|
+
`${forms.length} form${forms.length === 1 ? '' : 's'}: ${forms.join(', ')}`
|
|
174
|
+
+ (types.length ? ` · may display ${types.join(', ')} from the run folder` : '')));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if ((inv.scripts || []).length) {
|
|
178
|
+
const scripts = section(`Scripts (${inv.scripts.length})`);
|
|
179
|
+
for (const s of inv.scripts) {
|
|
180
|
+
scripts.appendChild(h(doc, 'div', 'pl-consent-row mono',
|
|
181
|
+
`${s.key} — ${s.runtime}${s.command ? ` · ${s.command}` : s.file ? ` · ${s.file}` : ''}`
|
|
182
|
+
+ `${s.cases ? ` · ${s.cases} case${s.cases === 1 ? '' : 's'}` : ''}`));
|
|
183
|
+
}
|
|
162
184
|
}
|
|
163
185
|
const sources = section(`Task sources (${(inv.taskSources || []).length})`);
|
|
164
186
|
for (const s of inv.taskSources || []) {
|
|
@@ -124,14 +124,14 @@ export function workflowPickerLabel(wf, enabledPluginNames = []) {
|
|
|
124
124
|
* Rows for the History Overview "Memory changes" block (agent-memory-design.md §6).
|
|
125
125
|
* Pure: `memory` is results.json.memory / the detail's `memory` ({ changes }).
|
|
126
126
|
* One row per change entry that carries at least one file; chips in the order
|
|
127
|
-
* added, modified, deleted, rejected; a rejected chip's `title` is the reason.
|
|
127
|
+
* added, modified, deleted, rejected, failed; a rejected or failed chip's `title` is the reason.
|
|
128
128
|
* Each chip also carries the ref it was built from — `scope` (mount-relative:
|
|
129
129
|
* `global` | `project` | `projects/<key>`) and `name` — so History can link to
|
|
130
130
|
* the file in its Memory view (B6).
|
|
131
131
|
*/
|
|
132
132
|
export function memoryChangesRows(memory) {
|
|
133
133
|
const changes = Array.isArray(memory?.changes) ? memory.changes : [];
|
|
134
|
-
const file = (r) => `${r.scope}
|
|
134
|
+
const file = (r) => `${r.scope ? `${r.scope}/` : ''}${r.name}.md`;
|
|
135
135
|
const rows = [];
|
|
136
136
|
for (const c of changes) {
|
|
137
137
|
const chips = [
|
|
@@ -139,6 +139,8 @@ export function memoryChangesRows(memory) {
|
|
|
139
139
|
...(c.modified || []).map((r) => ({ kind: 'mod', text: `~ ${file(r)}`, scope: r.scope, name: r.name })),
|
|
140
140
|
...(c.deleted || []).map((r) => ({ kind: 'del', text: `− ${file(r)}`, scope: r.scope, name: r.name })),
|
|
141
141
|
...(c.rejected || []).map((r) => ({ kind: 'rej', text: `✕ ${file(r)}`, title: String(r.reason || ''), scope: r.scope, name: r.name })),
|
|
142
|
+
// A write that never reached the mount (a refused tool call): nothing in the store to open.
|
|
143
|
+
...(c.failed || []).map((r) => ({ kind: 'fail', text: `⊘ ${file(r)}`, title: String(r.reason || ''), scope: r.scope, name: r.name })),
|
|
142
144
|
];
|
|
143
145
|
if (chips.length) rows.push({ node: c.agentKey || c.nodeId || 'run', chips });
|
|
144
146
|
}
|