@worca/app 1.3.0-rc.2 → 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
package/ui/public/ask-panel.mjs
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// holds no state. All markup is built with DOM APIs and textContent — no
|
|
5
5
|
// innerHTML for content anywhere in this file (the markdown renderer owns the
|
|
6
6
|
// only sanitized-HTML path).
|
|
7
|
+
import { openScheduleSheet, browserTimeZone } from './schedule-sheet.mjs';
|
|
8
|
+
import { formatInstant, describeRule } from '../../src/shared/schedule/recurrence.mjs';
|
|
7
9
|
import { createThreadModel } from './ask-model.mjs';
|
|
8
10
|
import { createMarkdownRenderer } from './ask-markdown.mjs';
|
|
9
11
|
import { createThinkingOrb } from './thinking-orb.mjs';
|
|
@@ -21,7 +23,7 @@ import { portsFnFor } from '../../src/shared/graph/ports.mjs';
|
|
|
21
23
|
* default is ASK_LIMITS.defaultModel/defaultEffort, shipped as `catalog.default`
|
|
22
24
|
* and already validated against the live catalog by src/core/ask/models.mjs.
|
|
23
25
|
*/
|
|
24
|
-
const FALLBACK_PICK = Object.freeze({ model: 'claude-opus-5', effort: 'high' });
|
|
26
|
+
const FALLBACK_PICK = Object.freeze({ model: 'claude-opus-5-5', effort: 'high' });
|
|
25
27
|
|
|
26
28
|
const ICONS = {
|
|
27
29
|
threads: 'M4 6h16M4 12h16M4 18h9',
|
|
@@ -82,6 +84,31 @@ function clipInput(input) {
|
|
|
82
84
|
return s.length > 60 ? `${s.slice(0, 60)}…` : s;
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
const SCRIPT_TOOL_NAMES = new Set(['list_scripts', 'get_script', 'save_script', 'test_script']);
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The script tools' thread line (scripts-workbench-design.md §9.3): the key instead of a JSON
|
|
91
|
+
* blob, plus what came back once the call finished. `block.script` is stamped by the reducer
|
|
92
|
+
* (events.mjs#scriptToolKey at the call, #scriptResultNote at the result) and persisted with the
|
|
93
|
+
* block, so the line survives a reload and a clipped input. null for every other tool — those
|
|
94
|
+
* keep the op / target / preview shape.
|
|
95
|
+
* @returns {{target: string}|null} e.g. { target: 'script runTests → blocking, exit 1' }
|
|
96
|
+
*/
|
|
97
|
+
export function scriptToolLine(short, block = {}) {
|
|
98
|
+
if (!SCRIPT_TOOL_NAMES.has(short)) return null;
|
|
99
|
+
const s = block.script && typeof block.script === 'object' ? block.script : null;
|
|
100
|
+
const key = typeof s?.key === 'string' && s.key ? s.key
|
|
101
|
+
: (block.input && typeof block.input.key === 'string' ? block.input.key : '');
|
|
102
|
+
const bits = [];
|
|
103
|
+
if (s) {
|
|
104
|
+
if (typeof s.saved === 'string' && s.saved) bits.push(s.saved);
|
|
105
|
+
if (typeof s.status === 'string' && s.status) bits.push(s.status);
|
|
106
|
+
if (Number.isInteger(s.exitCode)) bits.push(`exit ${s.exitCode}`);
|
|
107
|
+
}
|
|
108
|
+
const noun = short.split('_').slice(1).join(' ');
|
|
109
|
+
return { target: [noun, key, bits.length ? `→ ${bits.join(', ')}` : ''].filter(Boolean).join(' ') };
|
|
110
|
+
}
|
|
111
|
+
|
|
85
112
|
/** The launcher's shortcut hint: the keydown handler accepts BOTH Meta+K and
|
|
86
113
|
* Ctrl+K, but the glyph shown must match the viewer's OS — '⌘K' is meaningless
|
|
87
114
|
* on Windows/Linux, where the working chord is Ctrl+K. */
|
|
@@ -557,7 +584,8 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
557
584
|
text,
|
|
558
585
|
model: st.picker.model,
|
|
559
586
|
effort: st.picker.effort,
|
|
560
|
-
|
|
587
|
+
// The browser's zone rides along: "tomorrow 02:00" is read in it (docs/scheduled-runs.md "Ask Worca").
|
|
588
|
+
context: { ...scopedContext(getPageContext() || {}), timeZone: browserTimeZone() },
|
|
561
589
|
...(st.pendingFiles.length ? { attachments: st.pendingFiles.map((f) => ({ name: f.name, dataBase64: f.dataBase64 })) } : {}),
|
|
562
590
|
};
|
|
563
591
|
const model = st.model;
|
|
@@ -652,6 +680,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
652
680
|
scopeBtn.appendChild(svgIcon(ICONS.chevronDown, 11, 2));
|
|
653
681
|
scopeBtn.addEventListener('click', () => openScopePopover(scopeBtn));
|
|
654
682
|
el.scopeBtn = scopeBtn;
|
|
683
|
+
scopeBtn.dataset.minLevel = 'advanced'; // interface mode (docs/ui-levels.md): Auto scope is the simple path
|
|
655
684
|
row.appendChild(scopeBtn);
|
|
656
685
|
|
|
657
686
|
row.appendChild(make('span', 'ask-composer-spacer'));
|
|
@@ -664,12 +693,14 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
664
693
|
el.meterCost = make('span', 'ask-meter-cost', '');
|
|
665
694
|
meter.appendChild(el.meterCost);
|
|
666
695
|
{ const sep = make('span', 'ask-meter-sep', '|'); sep.setAttribute('aria-hidden', 'true'); meter.appendChild(sep); }
|
|
696
|
+
meter.dataset.minLevel = 'advanced';
|
|
667
697
|
row.appendChild(meter);
|
|
668
698
|
|
|
669
699
|
const wtBtn = make('button', 'ask-agents-btn ask-wt-btn');
|
|
670
700
|
wtBtn.type = 'button';
|
|
671
701
|
wtBtn.setAttribute('data-ask-wt-btn', '');
|
|
672
702
|
wtBtn.hidden = true;
|
|
703
|
+
wtBtn.dataset.minLevel = 'expert';
|
|
673
704
|
el.wtBtn = wtBtn;
|
|
674
705
|
el.wtBtnLabel = make('span', null, '0 worktrees');
|
|
675
706
|
wtBtn.appendChild(el.wtBtnLabel);
|
|
@@ -680,6 +711,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
680
711
|
const agentsBtn = make('button', 'ask-agents-btn');
|
|
681
712
|
agentsBtn.type = 'button';
|
|
682
713
|
agentsBtn.setAttribute('data-ask-agents-btn', '');
|
|
714
|
+
agentsBtn.dataset.minLevel = 'expert';
|
|
683
715
|
el.agentsBtnLabel = make('span', null, '0 agents');
|
|
684
716
|
agentsBtn.appendChild(el.agentsBtnLabel);
|
|
685
717
|
agentsBtn.appendChild(svgIcon('M6 15l6-6 6 6', 11, 2));
|
|
@@ -689,6 +721,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
689
721
|
const modelBtn = make('button', 'ask-model-btn');
|
|
690
722
|
modelBtn.type = 'button';
|
|
691
723
|
modelBtn.setAttribute('data-ask-model-btn', '');
|
|
724
|
+
modelBtn.dataset.minLevel = 'advanced';
|
|
692
725
|
el.modelBtnLabel = make('span', 'ask-model-btn-label', st.picker.model);
|
|
693
726
|
el.modelBtnEffort = make('span', 'ask-model-btn-effort', st.picker.effort);
|
|
694
727
|
modelBtn.appendChild(el.modelBtnLabel);
|
|
@@ -1210,7 +1243,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
1210
1243
|
const wantedEntry = catalogEntry(wanted.model);
|
|
1211
1244
|
// Unknown stored/default id -> the backend default -> the first model we do
|
|
1212
1245
|
// have that is not a hidden built-in (#422; a hidden id is still a valid pick).
|
|
1213
|
-
const entry = wantedEntry || catalogEntry(fallback.model) || list.find((m) => m && !m.hidden) || list[0] || null;
|
|
1246
|
+
const entry = wantedEntry || catalogEntry(fallback.model) || list.find((m) => m && !m.hidden && !m.needsSignIn) || list[0] || null;
|
|
1214
1247
|
if (!entry) { updatePickerButton(); return; } // empty catalog: keep what we have
|
|
1215
1248
|
const effort = wantedEntry ? wanted.effort : fallback.effort;
|
|
1216
1249
|
const next = { model: entry.id, effort: coerceEffort(entry, effort) };
|
|
@@ -1266,6 +1299,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
1266
1299
|
for (const m of st.catalog ? st.catalog.models : []) {
|
|
1267
1300
|
if (!m || typeof m.id !== 'string') continue;
|
|
1268
1301
|
if (m.hidden && m.id !== st.picker.model) continue; // hidden built-in (#422); the current pick stays
|
|
1302
|
+
if (m.needsSignIn && m.id !== st.picker.model) continue; // bridged, provider not usable (model-bridge §8.5)
|
|
1269
1303
|
if (m.custom === 'global') { primary.push(m); continue; } // user models are never demoted
|
|
1270
1304
|
const fam = familyKey(m);
|
|
1271
1305
|
// The picked model always shows up front so its ✓ is visible and it is one click away.
|
|
@@ -1318,6 +1352,9 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
1318
1352
|
item.appendChild(tag('secret not set', 'is-err',
|
|
1319
1353
|
`${m.secretsMissing.join(', ')} is not set — configure it in the ${m.plugin ? `“${m.plugin}” ` : ''}plugin's Model secrets, or this model will fail.`));
|
|
1320
1354
|
}
|
|
1355
|
+
if (m.needsSignIn) {
|
|
1356
|
+
item.appendChild(tag('needs sign-in', 'is-err', m.signInMessage || 'The provider behind this model is not usable yet — Settings › Models › Providers.'));
|
|
1357
|
+
}
|
|
1321
1358
|
if (m.id === st.picker.model) item.appendChild(make('span', 'ask-model-check', '✓'));
|
|
1322
1359
|
return item;
|
|
1323
1360
|
};
|
|
@@ -1787,9 +1824,11 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
1787
1824
|
if (!wf || !(Array.isArray(wf.nodes) || Array.isArray(wf.steps)) || !Object.keys(registry).length || !cfg) return null;
|
|
1788
1825
|
const config = (cfg.config && typeof cfg.config === 'object') ? cfg.config : { steps: {}, customModels: [] };
|
|
1789
1826
|
const runConfig = (config.workflows && config.workflows[workflowId]) || { nodes: {}, feedbacks: {} };
|
|
1790
|
-
const
|
|
1827
|
+
const models = Array.isArray(cfg.models) ? cfg.models : [];
|
|
1828
|
+
// `models`: a pinned row's hidden pick is healed against this catalog (node-tunables.mjs).
|
|
1829
|
+
const rows = buildNodeConfigRows(wf, registry, runConfig, { ...(workflowId === 'wf_default' ? { legacySteps: config.steps || {} } : {}), models });
|
|
1791
1830
|
return { wf, registry, runConfig, rows, edits: {}, editable: !!projectDir,
|
|
1792
|
-
models
|
|
1831
|
+
models, efforts: Array.isArray(cfg.efforts) ? cfg.efforts : [],
|
|
1793
1832
|
subagentModels: Array.isArray(cfg.subagentModels) ? cfg.subagentModels : [] };
|
|
1794
1833
|
}
|
|
1795
1834
|
const laneEffective = (lane, row) => ({ ...row, ...(lane.edits[row.nodeId] || {}) });
|
|
@@ -1910,15 +1949,17 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
1910
1949
|
const small = make('small');
|
|
1911
1950
|
// "step N" = position in the lane (launch order); row.stepIndex ranks the task card as 0.
|
|
1912
1951
|
if (changed) { small.appendChild(make('span', 'ask-rp-m', 'edited')); small.appendChild(doc.createTextNode(` · step ${i + 1}`)); }
|
|
1913
|
-
else small.textContent = `step ${i + 1} · ${row.modified ? 'project override' : 'workflow default'}`;
|
|
1952
|
+
else small.textContent = `step ${i + 1} · ${row.pinned ? 'model from Settings › Memory' : row.modified ? 'project override' : 'workflow default'}`;
|
|
1914
1953
|
name.appendChild(small);
|
|
1915
1954
|
l1.appendChild(name);
|
|
1916
1955
|
// model
|
|
1917
1956
|
const sel = rpSelect('ask-rp-model', `Model for ${row.label}`);
|
|
1918
1957
|
sel.appendChild(opt('', 'inherit (workflow default)'));
|
|
1919
|
-
for (const m of lane.models) if (!m.hidden || m.id === c.model) sel.appendChild(opt(m.id, m.label || m.id));
|
|
1958
|
+
for (const m of lane.models) if ((!m.hidden && !m.needsSignIn) || m.id === c.model) sel.appendChild(opt(m.id, (m.label || m.id) + (m.needsSignIn ? ' (needs sign-in)' : '')));
|
|
1920
1959
|
sel.value = c.model || '';
|
|
1921
|
-
|
|
1960
|
+
// Settings › Memory pins a defragment run's pair (node-tunables.mjs `pinned`): shown, locked.
|
|
1961
|
+
sel.disabled = !lane.editable || !!row.pinned;
|
|
1962
|
+
if (row.pinned) sel.title = 'Set in Settings › Memory';
|
|
1922
1963
|
sel.addEventListener('change', () => {
|
|
1923
1964
|
const mid = sel.value;
|
|
1924
1965
|
const list = (lane.models.find((m) => m.id === mid) || {}).efforts || [];
|
|
@@ -1937,7 +1978,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
1937
1978
|
b.type = 'button';
|
|
1938
1979
|
b.setAttribute('role', 'radio');
|
|
1939
1980
|
b.setAttribute('aria-checked', String(e === c.effort));
|
|
1940
|
-
b.disabled = !lane.editable || !offered.includes(e);
|
|
1981
|
+
b.disabled = !lane.editable || !!row.pinned || !offered.includes(e);
|
|
1941
1982
|
if (!offered.includes(e) && c.model) b.title = `Not offered by ${modelLabel(lane, c.model)}`;
|
|
1942
1983
|
b.addEventListener('click', () => { laneSet(lane, row, { effort: e }); renderLane(laneSec, lane, lc); });
|
|
1943
1984
|
eff.appendChild(b);
|
|
@@ -1980,9 +2021,76 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
1980
2021
|
if (block.state === 'failed') {
|
|
1981
2022
|
return make('div', 'ask-card-stub ask-card-failed', `Run failed${block.error ? `: ${block.error}` : ''} — ${card.title || card.brief || ''}`);
|
|
1982
2023
|
}
|
|
2024
|
+
if (block.state === 'scheduled') return buildCardScheduled(block);
|
|
1983
2025
|
return make('div', 'ask-card-stub', `Not now — ${card.title || card.brief || 'run proposal'}`);
|
|
1984
2026
|
}
|
|
1985
2027
|
|
|
2028
|
+
// ---- scheduled proposals (docs/scheduled-runs.md "Ask Worca") ----
|
|
2029
|
+
const CLOCK_ICO = 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM12 7.5V12l3 2';
|
|
2030
|
+
const localWhen = (iso) => { const ms = Date.parse(iso || ''); return Number.isFinite(ms) ? formatInstant(ms, browserTimeZone()) : 'later'; };
|
|
2031
|
+
/** The POST /api/run fields of a proposal's schedule (schedule-spec.mjs scheduleRequestFields). */
|
|
2032
|
+
function scheduleFieldsOf(s) {
|
|
2033
|
+
if (s && s.kind === 'once') return { scheduledFor: s.runAt };
|
|
2034
|
+
if (s && s.kind === 'repeat') return { repeat: { rule: s.rule, overlap: s.overlap, maxFailures: s.maxFailures } };
|
|
2035
|
+
if (s && s.kind === 'after') return { after: { kind: s.after.kind, id: s.after.id, title: s.after.title }, afterPolicy: s.policy || 'done', ...(s.sourceFromPrevious ? { sourceFromPrevious: true } : {}) };
|
|
2036
|
+
return null;
|
|
2037
|
+
}
|
|
2038
|
+
/** The schedule sheet's `initial` for a pick (so Change… opens on what the card shows). */
|
|
2039
|
+
function sheetInitialOf(pick) {
|
|
2040
|
+
if (!pick) return {};
|
|
2041
|
+
if (pick.after) return { after: pick.after, afterPolicy: pick.afterPolicy };
|
|
2042
|
+
if (pick.repeat) return { rule: pick.repeat.rule, overlap: pick.repeat.overlap, maxFailures: pick.repeat.maxFailures, ifMissed: pick.ifMissed, graceMin: pick.graceMin };
|
|
2043
|
+
return { scheduledFor: pick.scheduledFor, ifMissed: pick.ifMissed, graceMin: pick.graceMin };
|
|
2044
|
+
}
|
|
2045
|
+
function scheduleLineText(s) {
|
|
2046
|
+
if (s.kind === 'after') return `${s.text}${s.sourceFromPrevious ? ' · from its branch' : ''}`;
|
|
2047
|
+
if (s.kind === 'repeat') return `${s.sentence}${s.next && s.next[0] ? ` · first run ${localWhen(s.next[0].at)}` : ''}`;
|
|
2048
|
+
return `Starts ${localWhen(s.runAt)}`;
|
|
2049
|
+
}
|
|
2050
|
+
function pickedLineText(p) {
|
|
2051
|
+
if (p.after) return `After ‘${p.after.title || p.after.id}’ finishes${p.sourceFromPrevious ? ' · from its branch' : ''}`;
|
|
2052
|
+
if (p.repeat) return describeRule(p.repeat.rule);
|
|
2053
|
+
return `Starts ${localWhen(p.scheduledFor)}`;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
/** A proposal the user scheduled: it waits as a ticket (block.runId) — or, when it became a repeating
|
|
2057
|
+
* schedule, follows the series (block.scheduleId) — until the server starts it. */
|
|
2058
|
+
function buildCardScheduled(block) {
|
|
2059
|
+
const card = block.card || {};
|
|
2060
|
+
const rootEl = make('div', 'ask-card-stub ask-card-sched');
|
|
2061
|
+
rootEl.setAttribute('data-ask-card-scheduled', '');
|
|
2062
|
+
const series = !!block.scheduleId;
|
|
2063
|
+
const title = card.title || card.brief || 'Run';
|
|
2064
|
+
rootEl.append(make('span', 'badge grey', series ? 'Repeats' : block.after ? 'After run' : 'Scheduled'), make('span', 'ask-card-sched-text',
|
|
2065
|
+
series ? `${title} — ${block.sentence || 'repeating schedule'}${block.scheduledFor ? ` · next ${localWhen(block.scheduledFor)}` : ''}`
|
|
2066
|
+
: block.after ? `${title} — after ‘${block.after.title || block.after.id}’ finishes` : `${title} — starts ${localWhen(block.scheduledFor)}`));
|
|
2067
|
+
const err = make('span', 'ask-card-err');
|
|
2068
|
+
const call = async (method, path, btn) => {
|
|
2069
|
+
err.textContent = ''; btn.disabled = true;
|
|
2070
|
+
try {
|
|
2071
|
+
const res = await fetch(path, { method, headers: { 'Content-Type': 'application/json' }, body: method === 'POST' ? '{}' : undefined });
|
|
2072
|
+
const data = await res.json().catch(() => ({}));
|
|
2073
|
+
if (!res.ok) err.textContent = data.error || `request failed (${res.status})`;
|
|
2074
|
+
else if (data.status === 'failed') err.textContent = data.failReason || 'The run could not be started.';
|
|
2075
|
+
} catch { err.textContent = 'network error'; }
|
|
2076
|
+
btn.disabled = false; // the flip frame re-renders the card on success
|
|
2077
|
+
};
|
|
2078
|
+
const target = series ? block.scheduleId : block.runId;
|
|
2079
|
+
const runNow = make('button', 'ask-card-not-now', 'Run now');
|
|
2080
|
+
runNow.type = 'button';
|
|
2081
|
+
runNow.addEventListener('click', () => call('POST', `/api/schedules/${target}/run-now`, runNow));
|
|
2082
|
+
const cancel = make('button', 'ask-card-not-now', series ? 'Delete schedule' : 'Cancel schedule');
|
|
2083
|
+
cancel.type = 'button';
|
|
2084
|
+
cancel.addEventListener('click', async () => {
|
|
2085
|
+
if (series && typeof confirm === 'function' && !(await confirm(`Delete the schedule "${title}"? Runs it already started are kept.`))) return;
|
|
2086
|
+
call('DELETE', `/api/schedules/${target}`, cancel);
|
|
2087
|
+
});
|
|
2088
|
+
const open = make('a', 'ask-card-sched-link', 'Schedules');
|
|
2089
|
+
open.href = '#schedules';
|
|
2090
|
+
rootEl.append(runNow, cancel, open, err);
|
|
2091
|
+
return rootEl;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
1986
2094
|
// ---- Workflow card (spec §8.3, mockup 2026-09-05 §A-§C, plan PD4/PD7/PD12-15) ---------------------------------
|
|
1987
2095
|
const WF_ICO = { check: 'M5 13l4 4L19 7', save: 'M5 12l5 5L20 7' };
|
|
1988
2096
|
const TUNABLE_KEYS = ['model', 'effort', 'fanOut', 'askQuestions'];
|
|
@@ -2078,15 +2186,23 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2078
2186
|
// ---- Metrics card (docs/team-metrics.md "Ask Worca"): a proposed team-metrics configuration change --------------
|
|
2079
2187
|
const MC_KIND_LABEL = { enable: 'Enable team metrics', record: 'Include my runs', workspace_home: 'Metrics home', route_members: 'Route members' };
|
|
2080
2188
|
const MC_APPLY_LABEL = { enable: 'Enable', record: 'Apply', workspace_home: 'Set home', route_members: 'Route members' };
|
|
2189
|
+
// A team-policy card (docs/team-policy.md "Ask Worca") is the same component: other words, plus the
|
|
2190
|
+
// before → after list of an edit.
|
|
2191
|
+
const PC_KIND_LABEL = { enable: 'Set up team policy', edit: 'Edit team policy', workspace_home: 'Policy home', route_members: 'Route members' };
|
|
2192
|
+
const pcApplyLabel = (card) => (card.kind === 'enable' ? (card.mode === 'follow' ? 'Follow' : 'Set up') : card.kind === 'edit' ? 'Publish'
|
|
2193
|
+
: card.kind === 'workspace_home' ? 'Set home' : card.kind === 'route_members' ? 'Route members' : 'Apply');
|
|
2081
2194
|
function buildMetricsCard(block) {
|
|
2082
2195
|
const card = block.card || {};
|
|
2083
|
-
const
|
|
2196
|
+
const isPolicy = card.type === 'policy';
|
|
2197
|
+
const noun = isPolicy ? 'policy change' : 'metrics change';
|
|
2198
|
+
const Noun = isPolicy ? 'Policy change' : 'Metrics change';
|
|
2199
|
+
const summary = card.summary || noun;
|
|
2084
2200
|
if (block.state === 'declined') return { el: make('div', 'ask-card-stub', `Declined — ${summary}`) };
|
|
2085
|
-
const rootEl = make('div', `ask-card ask-mcard is-${block.state}`);
|
|
2086
|
-
rootEl.setAttribute('data-ask-mcard', block.state);
|
|
2201
|
+
const rootEl = make('div', `ask-card ask-mcard${isPolicy ? ' ask-pcard' : ''} is-${block.state}`);
|
|
2202
|
+
rootEl.setAttribute(isPolicy ? 'data-ask-pcard' : 'data-ask-mcard', block.state);
|
|
2087
2203
|
const head = make('div', 'ask-mcard-head');
|
|
2088
|
-
head.appendChild(make('span', 'ask-mcard-title', block.state === 'applied' ?
|
|
2089
|
-
head.appendChild(make('span', 'ask-mcard-kind', MC_KIND_LABEL[card.kind] || card.kind || ''));
|
|
2204
|
+
head.appendChild(make('span', 'ask-mcard-title', block.state === 'applied' ? `Applied ${noun}` : block.state === 'failed' ? `${Noun} failed` : `Proposed ${noun}`));
|
|
2205
|
+
head.appendChild(make('span', 'ask-mcard-kind', (isPolicy ? PC_KIND_LABEL : MC_KIND_LABEL)[card.kind] || card.kind || ''));
|
|
2090
2206
|
rootEl.appendChild(head);
|
|
2091
2207
|
const body = make('div', 'ask-mcard-body');
|
|
2092
2208
|
const target = card.workspaceName ? `workspace ${card.workspaceName}` : card.projectName ? `project ${card.projectName}` : '';
|
|
@@ -2096,6 +2212,20 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2096
2212
|
body.appendChild(sum);
|
|
2097
2213
|
if (target) body.appendChild(make('div', 'ask-mcard-target', target));
|
|
2098
2214
|
if (card.note) body.appendChild(make('div', 'ask-mcard-note', card.note));
|
|
2215
|
+
if (isPolicy && Array.isArray(card.changes) && card.changes.length) {
|
|
2216
|
+
const ul = make('ul', 'ask-mcard-changes');
|
|
2217
|
+
for (const c of card.changes) {
|
|
2218
|
+
const li = make('li');
|
|
2219
|
+
li.appendChild(make('span', 'ask-mcard-change-label', c.label || c.key || ''));
|
|
2220
|
+
const val = make('span', 'ask-mcard-change-val');
|
|
2221
|
+
val.appendChild(make('span', c.before ? 'ask-mcard-before' : 'ask-mcard-before is-unset', c.before || 'unset'));
|
|
2222
|
+
val.appendChild(make('span', 'ask-mcard-arrow', '→'));
|
|
2223
|
+
val.appendChild(make('span', c.after ? 'ask-mcard-after' : 'ask-mcard-after is-unset', c.after || 'unset'));
|
|
2224
|
+
li.appendChild(val);
|
|
2225
|
+
ul.appendChild(li);
|
|
2226
|
+
}
|
|
2227
|
+
body.appendChild(ul);
|
|
2228
|
+
}
|
|
2099
2229
|
if (Array.isArray(card.effects) && card.effects.length && block.state === 'proposed') {
|
|
2100
2230
|
const ul = make('ul', 'ask-mcard-effects');
|
|
2101
2231
|
for (const e of card.effects) ul.appendChild(make('li', null, e));
|
|
@@ -2130,7 +2260,143 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2130
2260
|
};
|
|
2131
2261
|
const decline = btn('ask-card-not-now', 'Decline', 'data-ask-mc-decline');
|
|
2132
2262
|
decline.addEventListener('click', () => postCard(block, rootEl, { state: 'declined' }, decline));
|
|
2133
|
-
const apply = btn('ask-card-start', MC_APPLY_LABEL[card.kind] || 'Apply', 'data-ask-mc-apply', WF_ICO.save);
|
|
2263
|
+
const apply = btn('ask-card-start', isPolicy ? pcApplyLabel(card) : (MC_APPLY_LABEL[card.kind] || 'Apply'), 'data-ask-mc-apply', WF_ICO.save);
|
|
2264
|
+
apply.addEventListener('click', () => postCard(block, rootEl, { state: 'applied' }, apply));
|
|
2265
|
+
actions.append(make('span', 'ask-card-actions-spacer'), decline, apply);
|
|
2266
|
+
rootEl.appendChild(actions);
|
|
2267
|
+
}
|
|
2268
|
+
return { el: rootEl };
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
// ---- Schedule card (docs/scheduled-runs.md "Ask Worca"): a proposed change to an existing schedule ------------
|
|
2272
|
+
const SC_ACTION_LABEL = { run_now: 'Run now', move: 'Change time', edit: 'Edit schedule', cancel: 'Cancel run', delete: 'Delete schedule' };
|
|
2273
|
+
const SC_APPLY_LABEL = { run_now: 'Run now', move: 'Move', edit: 'Apply', cancel: 'Cancel run', delete: 'Delete' };
|
|
2274
|
+
function buildScheduleCard(block) {
|
|
2275
|
+
const card = block.card || {};
|
|
2276
|
+
const summary = card.summary || 'schedule change';
|
|
2277
|
+
if (block.state === 'declined') return { el: make('div', 'ask-card-stub', `Declined — ${summary}`) };
|
|
2278
|
+
const rootEl = make('div', `ask-card ask-mcard ask-scard is-${block.state}`);
|
|
2279
|
+
rootEl.setAttribute('data-ask-scard', block.state);
|
|
2280
|
+
const head = make('div', 'ask-mcard-head');
|
|
2281
|
+
head.appendChild(make('span', 'ask-mcard-title', block.state === 'applied' ? 'Applied schedule change' : block.state === 'failed' ? 'Schedule change failed' : 'Proposed schedule change'));
|
|
2282
|
+
head.appendChild(make('span', 'ask-mcard-kind', SC_ACTION_LABEL[card.action] || card.action || ''));
|
|
2283
|
+
rootEl.appendChild(head);
|
|
2284
|
+
const body = make('div', 'ask-mcard-body');
|
|
2285
|
+
const sum = make('div', 'ask-mcard-summary');
|
|
2286
|
+
if (block.state === 'applied') sum.appendChild(svgIcon(WF_ICO.check, 15, 2.4));
|
|
2287
|
+
sum.appendChild(make('span', null, summary));
|
|
2288
|
+
body.appendChild(sum);
|
|
2289
|
+
if (card.targetName) body.appendChild(make('div', 'ask-mcard-target', card.targetName));
|
|
2290
|
+
if (card.note) body.appendChild(make('div', 'ask-mcard-note', card.note));
|
|
2291
|
+
if (block.state === 'proposed' && (card.before || card.after)) {
|
|
2292
|
+
const kv = make('div', 'ask-scard-kv');
|
|
2293
|
+
const row = (k, v) => { if (!v) return; kv.append(make('span', 'ask-scard-k', k), make('span', 'ask-scard-v', v)); };
|
|
2294
|
+
const b = card.before || {};
|
|
2295
|
+
const a = card.after || {};
|
|
2296
|
+
if (card.action === 'edit') {
|
|
2297
|
+
row('Now', b.sentence);
|
|
2298
|
+
if (a.sentence && a.sentence !== b.sentence) row('Becomes', a.sentence);
|
|
2299
|
+
if (Array.isArray(a.next) && a.next.length) row('Next runs', a.next.map((n) => localWhen(n.at)).join(' · '));
|
|
2300
|
+
} else if (card.action === 'move') {
|
|
2301
|
+
// Run chains: a move to AFTER another run has no instant on either side — say what it is.
|
|
2302
|
+
row('From', b.at ? localWhen(b.at) : b.when || '');
|
|
2303
|
+
row('To', a.at ? localWhen(a.at) : a.text || '');
|
|
2304
|
+
} else if (b.at) row(card.itemKind === 'recurring' ? 'Next run' : 'Scheduled for', localWhen(b.at));
|
|
2305
|
+
if (kv.childNodes.length) body.appendChild(kv);
|
|
2306
|
+
}
|
|
2307
|
+
const result = card.result || null;
|
|
2308
|
+
if (block.state === 'failed') body.appendChild(make('div', 'ask-mcard-failed', `Could not apply: ${block.error || (result && result.error) || 'unknown error'}`));
|
|
2309
|
+
else if (block.state === 'applied' && result && result.detail) body.appendChild(make('div', 'ask-mcard-detail', result.detail));
|
|
2310
|
+
if (block.state !== 'proposed') {
|
|
2311
|
+
const open = make('a', 'ask-card-sched-link', 'Schedules');
|
|
2312
|
+
open.href = '#schedules';
|
|
2313
|
+
body.appendChild(open);
|
|
2314
|
+
}
|
|
2315
|
+
rootEl.appendChild(body);
|
|
2316
|
+
rootEl.appendChild(make('div', 'ask-card-err'));
|
|
2317
|
+
if (block.state === 'proposed') {
|
|
2318
|
+
const actions = make('div', 'ask-mcard-actions');
|
|
2319
|
+
const btn = (cls, text, attr, icon) => {
|
|
2320
|
+
const b = make('button', cls, text); b.type = 'button'; b.setAttribute(attr, '');
|
|
2321
|
+
if (icon) b.prepend(svgIcon(icon, 12, 2.2));
|
|
2322
|
+
return b;
|
|
2323
|
+
};
|
|
2324
|
+
const decline = btn('ask-card-not-now', 'Decline', 'data-ask-sc-decline');
|
|
2325
|
+
decline.addEventListener('click', () => postCard(block, rootEl, { state: 'declined' }, decline));
|
|
2326
|
+
const destructive = card.action === 'cancel' || card.action === 'delete';
|
|
2327
|
+
const apply = btn(destructive ? 'ask-card-start is-danger' : 'ask-card-start', SC_APPLY_LABEL[card.action] || 'Apply', 'data-ask-sc-apply',
|
|
2328
|
+
card.action === 'run_now' ? null : WF_ICO.save);
|
|
2329
|
+
apply.addEventListener('click', () => postCard(block, rootEl, { state: 'applied' }, apply));
|
|
2330
|
+
actions.append(make('span', 'ask-card-actions-spacer'), decline, apply);
|
|
2331
|
+
rootEl.appendChild(actions);
|
|
2332
|
+
}
|
|
2333
|
+
return { el: rootEl };
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
// ---- Model card (docs/models.md "Ask Worca"): a proposed catalog or provider change ------------------------------
|
|
2337
|
+
const MOD_KIND_LABEL = { add_model: 'Add model', edit_model: 'Edit model', remove_model: 'Remove model', provider: 'Provider', import_copilot: 'From Copilot' };
|
|
2338
|
+
const MOD_APPLY_LABEL = { add_model: 'Add', edit_model: 'Apply', remove_model: 'Remove', provider: 'Apply', import_copilot: 'Import' };
|
|
2339
|
+
function buildModelCard(block) {
|
|
2340
|
+
const card = block.card || {};
|
|
2341
|
+
const summary = card.summary || 'model change';
|
|
2342
|
+
if (block.state === 'declined') return { el: make('div', 'ask-card-stub', `Declined — ${summary}`) };
|
|
2343
|
+
const rootEl = make('div', `ask-card ask-mcard ask-modcard is-${block.state}`);
|
|
2344
|
+
rootEl.setAttribute('data-ask-modcard', block.state);
|
|
2345
|
+
const noun = card.kind === 'provider' ? 'provider change' : 'model change';
|
|
2346
|
+
const head = make('div', 'ask-mcard-head');
|
|
2347
|
+
head.appendChild(make('span', 'ask-mcard-title', block.state === 'applied' ? `Applied ${noun}` : block.state === 'failed' ? `${noun[0].toUpperCase()}${noun.slice(1)} failed` : `Proposed ${noun}`));
|
|
2348
|
+
head.appendChild(make('span', 'ask-mcard-kind', MOD_KIND_LABEL[card.kind] || card.kind || ''));
|
|
2349
|
+
rootEl.appendChild(head);
|
|
2350
|
+
const body = make('div', 'ask-mcard-body');
|
|
2351
|
+
const sum = make('div', 'ask-mcard-summary');
|
|
2352
|
+
if (block.state === 'applied') sum.appendChild(svgIcon(WF_ICO.check, 15, 2.4));
|
|
2353
|
+
sum.appendChild(make('span', null, summary));
|
|
2354
|
+
body.appendChild(sum);
|
|
2355
|
+
if (card.note) body.appendChild(make('div', 'ask-mcard-note', card.note));
|
|
2356
|
+
if (block.state === 'proposed' && Array.isArray(card.rows) && card.rows.length) {
|
|
2357
|
+
const ul = make('ul', 'ask-mcard-changes');
|
|
2358
|
+
const oneSided = card.kind === 'add_model' || card.kind === 'remove_model';
|
|
2359
|
+
for (const r of card.rows) {
|
|
2360
|
+
const li = make('li');
|
|
2361
|
+
li.appendChild(make('span', 'ask-mcard-change-label', r.field || ''));
|
|
2362
|
+
const val = make('span', 'ask-mcard-change-val');
|
|
2363
|
+
if (oneSided) val.appendChild(make('span', card.kind === 'remove_model' ? 'ask-mcard-before' : 'ask-mcard-after', (card.kind === 'remove_model' ? r.before : r.after) || ''));
|
|
2364
|
+
else {
|
|
2365
|
+
val.appendChild(make('span', r.before ? 'ask-mcard-before' : 'ask-mcard-before is-unset', r.before || 'unset'));
|
|
2366
|
+
val.appendChild(make('span', 'ask-mcard-arrow', '→'));
|
|
2367
|
+
val.appendChild(make('span', r.after ? 'ask-mcard-after' : 'ask-mcard-after is-unset', r.after || 'unset'));
|
|
2368
|
+
}
|
|
2369
|
+
li.appendChild(val);
|
|
2370
|
+
ul.appendChild(li);
|
|
2371
|
+
}
|
|
2372
|
+
body.appendChild(ul);
|
|
2373
|
+
}
|
|
2374
|
+
if (block.state === 'proposed' && Array.isArray(card.warnings) && card.warnings.length) {
|
|
2375
|
+
const ul = make('ul', 'ask-mcard-effects ask-modcard-warn');
|
|
2376
|
+
for (const w of card.warnings) ul.appendChild(make('li', null, w));
|
|
2377
|
+
body.appendChild(ul);
|
|
2378
|
+
}
|
|
2379
|
+
const result = card.result || null;
|
|
2380
|
+
if (block.state === 'failed') body.appendChild(make('div', 'ask-mcard-failed', `Could not apply: ${block.error || (result && result.error) || 'unknown error'}`));
|
|
2381
|
+
else if (block.state === 'applied' && result && result.detail) body.appendChild(make('div', 'ask-mcard-detail', result.detail));
|
|
2382
|
+
if (block.state !== 'proposed') {
|
|
2383
|
+
const open = make('a', 'ask-card-sched-link', 'Settings › Models');
|
|
2384
|
+
open.href = '#settings/models';
|
|
2385
|
+
body.appendChild(open);
|
|
2386
|
+
}
|
|
2387
|
+
rootEl.appendChild(body);
|
|
2388
|
+
rootEl.appendChild(make('div', 'ask-card-err'));
|
|
2389
|
+
if (block.state === 'proposed') {
|
|
2390
|
+
const actions = make('div', 'ask-mcard-actions');
|
|
2391
|
+
const btn = (cls, text, attr, icon) => {
|
|
2392
|
+
const b = make('button', cls, text); b.type = 'button'; b.setAttribute(attr, '');
|
|
2393
|
+
if (icon) b.prepend(svgIcon(icon, 12, 2.2));
|
|
2394
|
+
return b;
|
|
2395
|
+
};
|
|
2396
|
+
const decline = btn('ask-card-not-now', 'Decline', 'data-ask-mod-decline');
|
|
2397
|
+
decline.addEventListener('click', () => postCard(block, rootEl, { state: 'declined' }, decline));
|
|
2398
|
+
const destructive = card.kind === 'remove_model';
|
|
2399
|
+
const apply = btn(destructive ? 'ask-card-start is-danger' : 'ask-card-start', MOD_APPLY_LABEL[card.kind] || 'Apply', 'data-ask-mod-apply', destructive ? null : WF_ICO.save);
|
|
2134
2400
|
apply.addEventListener('click', () => postCard(block, rootEl, { state: 'applied' }, apply));
|
|
2135
2401
|
actions.append(make('span', 'ask-card-actions-spacer'), decline, apply);
|
|
2136
2402
|
rootEl.appendChild(actions);
|
|
@@ -2307,6 +2573,11 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2307
2573
|
guardDesc.setAttribute('data-for', 'guardrails');
|
|
2308
2574
|
const guardField = rpField('Guardrails', guardSel);
|
|
2309
2575
|
guardField.appendChild(guardDesc);
|
|
2576
|
+
// Interface mode (docs/ui-levels.md). The proposal's own non-default values stay on screen at
|
|
2577
|
+
// any mode: a run must never start under a policy or on a branch the card did not show.
|
|
2578
|
+
const lvTag = (node, min, keep) => { node.dataset.minLevel = min; if (keep) node.dataset.levelKeep = '1'; return node; };
|
|
2579
|
+
lvTag(guardField, 'advanced', !!card.guardrailsId && card.guardrailsId !== 'permissive');
|
|
2580
|
+
lvTag(seg, 'advanced', card.target === 'workspace');
|
|
2310
2581
|
wfRow.append(wfField, guardField);
|
|
2311
2582
|
targetSec.appendChild(wfRow);
|
|
2312
2583
|
rootEl.appendChild(targetSec);
|
|
@@ -2317,6 +2588,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2317
2588
|
|
|
2318
2589
|
// agents lane (reloadLane → renderLane fills laneSec)
|
|
2319
2590
|
const laneSec = make('div', 'ask-rp-sec ask-rp-lane');
|
|
2591
|
+
laneSec.dataset.minLevel = 'expert';
|
|
2320
2592
|
rootEl.appendChild(laneSec);
|
|
2321
2593
|
|
|
2322
2594
|
const briefSec = make('div', 'ask-rp-sec ask-rp-brief-host');
|
|
@@ -2337,6 +2609,23 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2337
2609
|
const count = make('span', 'ask-rp-count');
|
|
2338
2610
|
briefFoot.append(hint, count);
|
|
2339
2611
|
briefSec.appendChild(briefFoot);
|
|
2612
|
+
if (card.source) {
|
|
2613
|
+
// The task IS a tracker task (propose_run source): the run reads it when it starts, so the
|
|
2614
|
+
// card shows the reference — never an editable copy that would silently go stale.
|
|
2615
|
+
briefHead.firstChild.textContent = 'Task';
|
|
2616
|
+
briefHead.children[1].textContent = `from ${card.source.displayName || card.source.plugin} · read when the run starts`;
|
|
2617
|
+
brief.hidden = true;
|
|
2618
|
+
briefFoot.hidden = true;
|
|
2619
|
+
const task = make('div', 'ask-card-task');
|
|
2620
|
+
task.setAttribute('data-ask-card-task', '');
|
|
2621
|
+
task.appendChild(make('span', 'badge grey mono', card.source.taskId));
|
|
2622
|
+
const name = card.source.url ? make('a', 'ask-card-task-title', card.source.title || card.source.taskId) : make('span', 'ask-card-task-title', card.source.title || '');
|
|
2623
|
+
if (card.source.url) { name.href = card.source.url; name.target = '_blank'; name.rel = 'noopener noreferrer'; }
|
|
2624
|
+
task.appendChild(name);
|
|
2625
|
+
if (card.source.profile) task.appendChild(make('span', 'ask-card-task-meta', `profile ${card.source.profile}`));
|
|
2626
|
+
briefSec.insertBefore(task, brief);
|
|
2627
|
+
if (card.sourceWarning) briefSec.insertBefore(make('div', 'ask-card-task-warn', card.sourceWarning), brief);
|
|
2628
|
+
}
|
|
2340
2629
|
rootEl.appendChild(briefSec);
|
|
2341
2630
|
local.pills = Array.isArray(card.attachments) ? card.attachments.filter((a) => a && a.id).map((a) => ({ ...a })) : [];
|
|
2342
2631
|
function renderPills() {
|
|
@@ -2390,6 +2679,9 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2390
2679
|
const openNp = make('button', 'ask-card-open-np', '↗ Open in New Pipeline');
|
|
2391
2680
|
openNp.type = 'button';
|
|
2392
2681
|
openNp.setAttribute('data-ask-card-open-np', '');
|
|
2682
|
+
openNp.dataset.minLevel = 'advanced';
|
|
2683
|
+
// New pipeline's task-source pane cannot be pre-filled from here yet; a tracker task runs from the card.
|
|
2684
|
+
if (card.source) openNp.hidden = true;
|
|
2393
2685
|
openNp.addEventListener('click', () => prefillFromCard(block, rootEl, local));
|
|
2394
2686
|
const dismissBtn = make('button', 'ask-card-not-now', 'Not now');
|
|
2395
2687
|
dismissBtn.type = 'button';
|
|
@@ -2403,8 +2695,58 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2403
2695
|
const playPath = doc.createElementNS('http://www.w3.org/2000/svg', 'path'); playPath.setAttribute('d', 'M6 4l14 8-14 8V4Z');
|
|
2404
2696
|
play.appendChild(playPath); startBtn.appendChild(play);
|
|
2405
2697
|
startBtn.appendChild(doc.createTextNode('Start run'));
|
|
2406
|
-
|
|
2407
|
-
|
|
2698
|
+
// A proposal Ask Worca scheduled starts at its (possibly changed) time; any other starts now.
|
|
2699
|
+
startBtn.addEventListener('click', () => startCard(block, rootEl, local, local.schedulePick || null));
|
|
2700
|
+
// Schedule…: the same request, started later — once, or on a repeat (the card then follows the schedule).
|
|
2701
|
+
const sheetOpts = (initial = {}) => ({
|
|
2702
|
+
mode: 'create', allowRepeat: true, initial, runTitle: (block.card && (block.card.title || block.card.brief)) || '',
|
|
2703
|
+
warning: 'A scheduled run is unattended. If this workflow asks questions, the run waits for your answer — chat notifications can reach you.',
|
|
2704
|
+
// Keyed off the card's LIVE target segment (local.target), not the proposal's frozen card.workspaceId —
|
|
2705
|
+
// the user can switch the card between project and workspace before opening the sheet.
|
|
2706
|
+
candidates: () => fetch(`/api/schedules/after-candidates?${local.target === 'workspace'
|
|
2707
|
+
? `workspaceId=${encodeURIComponent((rootEl.querySelector('.ask-card-workspace-select') || {}).value || card.workspaceId || '')}`
|
|
2708
|
+
: `projectDir=${encodeURIComponent(local.projectDir())}`}`).then((r) => r.json()),
|
|
2709
|
+
});
|
|
2710
|
+
const laterBtn = make('button', 'ask-card-not-now ask-card-later', 'Schedule…');
|
|
2711
|
+
laterBtn.type = 'button';
|
|
2712
|
+
if (card.schedule) {
|
|
2713
|
+
// Ask Worca proposed WHEN (propose_run when / every): scheduling is the answer, so it is the primary
|
|
2714
|
+
// action at every interface level (docs/ui-levels.md rule 4), and Start now is the alternative.
|
|
2715
|
+
local.schedulePick = scheduleFieldsOf(card.schedule);
|
|
2716
|
+
const line = make('div', 'ask-card-sched ask-card-sched-proposed');
|
|
2717
|
+
line.setAttribute('data-ask-card-sched-proposed', '');
|
|
2718
|
+
const badge = make('span', 'badge grey', card.schedule.kind === 'repeat' ? 'Repeats' : card.schedule.kind === 'after' ? 'After run' : 'Scheduled');
|
|
2719
|
+
const text = make('span', 'ask-card-sched-text', scheduleLineText(card.schedule));
|
|
2720
|
+
const change = make('button', 'link-btn ask-card-sched-change', 'Change…');
|
|
2721
|
+
change.type = 'button';
|
|
2722
|
+
change.setAttribute('data-ask-card-sched-change', '');
|
|
2723
|
+
change.addEventListener('click', async () => {
|
|
2724
|
+
const picked = await openScheduleSheet(sheetOpts(sheetInitialOf(local.schedulePick)));
|
|
2725
|
+
if (!picked) return;
|
|
2726
|
+
// A sheet pick that stays "after another run" keeps a sourceFromPrevious the proposal carried.
|
|
2727
|
+
local.schedulePick = { ...picked, ...(local.schedulePick && local.schedulePick.sourceFromPrevious && picked.after ? { sourceFromPrevious: true } : {}) };
|
|
2728
|
+
badge.textContent = picked.repeat ? 'Repeats' : picked.after ? 'After run' : 'Scheduled';
|
|
2729
|
+
text.textContent = pickedLineText(local.schedulePick);
|
|
2730
|
+
});
|
|
2731
|
+
line.append(badge, text, change);
|
|
2732
|
+
rootEl.insertBefore(line, err);
|
|
2733
|
+
play.replaceWith(svgIcon(CLOCK_ICO, 13, 2.2));
|
|
2734
|
+
startBtn.lastChild.textContent = 'Schedule';
|
|
2735
|
+
startBtn.setAttribute('data-ask-card-schedule-go', '');
|
|
2736
|
+
laterBtn.textContent = 'Start now';
|
|
2737
|
+
laterBtn.title = 'Start this run now instead';
|
|
2738
|
+
laterBtn.setAttribute('data-ask-card-start-now', '');
|
|
2739
|
+
laterBtn.addEventListener('click', () => startCard(block, rootEl, local));
|
|
2740
|
+
} else {
|
|
2741
|
+
laterBtn.setAttribute('data-ask-card-schedule', '');
|
|
2742
|
+
laterBtn.title = 'Start this run later';
|
|
2743
|
+
laterBtn.dataset.minLevel = 'advanced';
|
|
2744
|
+
laterBtn.addEventListener('click', async () => {
|
|
2745
|
+
const picked = await openScheduleSheet(sheetOpts());
|
|
2746
|
+
if (picked) startCard(block, rootEl, local, picked);
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
foot.append(openNp, summary, dismissBtn, laterBtn, startBtn);
|
|
2408
2750
|
rootEl.appendChild(foot);
|
|
2409
2751
|
|
|
2410
2752
|
local.projectDir = () => (local.target === 'project' ? ((rootEl.querySelector('.ask-card-project-select') || {}).value || '') : '');
|
|
@@ -2441,7 +2783,8 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2441
2783
|
}
|
|
2442
2784
|
projSel.addEventListener('change', () => { loadBranchesInto(srcSel, projSel.value, '').then(updateTargetSub); updateTargetSub(); reloadLane(); });
|
|
2443
2785
|
srcSel.addEventListener('change', updateTargetSub);
|
|
2444
|
-
grid.append(rpField('Project', projSel), rpField('Source branch', srcSel),
|
|
2786
|
+
grid.append(rpField('Project', projSel), lvTag(rpField('Source branch', srcSel), 'advanced', !!card.sourceBranch),
|
|
2787
|
+
lvTag(rpField('Feature branch', feature, 'created for the run'), 'advanced', !!card.featureBranch));
|
|
2445
2788
|
targetHost.appendChild(grid);
|
|
2446
2789
|
updateTargetSub();
|
|
2447
2790
|
return;
|
|
@@ -2481,7 +2824,8 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2481
2824
|
wsSel.addEventListener('change', renderMembers);
|
|
2482
2825
|
const wsField = rpField('Workspace', wsSel);
|
|
2483
2826
|
wsField.appendChild(members);
|
|
2484
|
-
grid.append(wsField, rpField('Source branch', srcInput, 'default for members'),
|
|
2827
|
+
grid.append(wsField, lvTag(rpField('Source branch', srcInput, 'default for members'), 'advanced', !!card.sourceBranch),
|
|
2828
|
+
lvTag(rpField('Feature branch', feature), 'advanced', !!card.featureBranch));
|
|
2485
2829
|
targetHost.appendChild(grid); // attach BEFORE filling: renderMembers → updateTargetSub finds the select through rootEl
|
|
2486
2830
|
targetHost.appendChild(details);
|
|
2487
2831
|
if (opts) {
|
|
@@ -2500,6 +2844,12 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2500
2844
|
const workflowId = local.workflowId();
|
|
2501
2845
|
const projectDir = local.projectDir();
|
|
2502
2846
|
local.lane = null;
|
|
2847
|
+
if (workflowId === 'wf_auto') {
|
|
2848
|
+
// No graph yet: the run classifies its task and picks the agents when it starts.
|
|
2849
|
+
renderLane(laneSec, null, laneCtx, 'Auto picks the agents when the run starts.');
|
|
2850
|
+
wfDesc.textContent = '';
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2503
2853
|
renderLane(laneSec, null, laneCtx, 'Loading agent settings…');
|
|
2504
2854
|
loadLane(workflowId, projectDir).then((lane) => {
|
|
2505
2855
|
if (st.destroyed || seq !== laneSeq) return; // a later reload won
|
|
@@ -2527,7 +2877,9 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2527
2877
|
loadCardOptions({ fresh: true }).then((opts) => {
|
|
2528
2878
|
if (st.destroyed) return;
|
|
2529
2879
|
local.options = opts;
|
|
2530
|
-
|
|
2880
|
+
// Auto is not a saved workflow: listed only when Ask Worca proposed it (a plain card keeps its list).
|
|
2881
|
+
const autoOpt = card.workflowId === 'wf_auto' ? [{ value: 'wf_auto', label: 'Auto — picks the workflow when the run starts' }] : [];
|
|
2882
|
+
fillSelect(workflowSel, [...autoOpt, ...opts.workflows.map((w) => ({ value: w.id, label: workflowPickerLabel(w, null) || w.name || w.id }))], card.workflowId || 'wf_default');
|
|
2531
2883
|
if (card.workflowId && workflowSel.value !== card.workflowId) markWorkflowUnavailable();
|
|
2532
2884
|
fillSelect(guardSel, opts.guardrails.map((g) => ({ value: g.id, label: g.id === 'permissive' ? 'Permissive' : (g.name || g.id) })), card.guardrailsId || 'normal');
|
|
2533
2885
|
renderTarget();
|
|
@@ -2548,6 +2900,14 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2548
2900
|
// Agent memory (§7.3 / B17): the card's scope rides along only while its workflow is still the
|
|
2549
2901
|
// defragment one — a user who switched the picker to another workflow gets a legacy body.
|
|
2550
2902
|
if (card.memoryScope && body.workflowId === 'wf_memory_defrag') body.memoryScope = card.memoryScope;
|
|
2903
|
+
// A tracker task: the reference, never a prompt (POST /api/run takes source OR prompt).
|
|
2904
|
+
if (card.source) {
|
|
2905
|
+
delete body.prompt;
|
|
2906
|
+
body.source = {
|
|
2907
|
+
type: 'plugin', plugin: card.source.plugin, sourceId: card.source.sourceId, taskId: card.source.taskId,
|
|
2908
|
+
...(card.source.profile ? { profile: card.source.profile } : {}), ...(card.source.inputs ? { inputs: card.source.inputs } : {}),
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2551
2911
|
const feature = rootEl.querySelector('.ask-card-feature').value.trim();
|
|
2552
2912
|
if (feature) body.featureBranch = feature;
|
|
2553
2913
|
if (local.target === 'workspace') {
|
|
@@ -2578,7 +2938,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2578
2938
|
}
|
|
2579
2939
|
}
|
|
2580
2940
|
|
|
2581
|
-
async function startCard(block, rootEl, local) {
|
|
2941
|
+
async function startCard(block, rootEl, local, schedule = null) {
|
|
2582
2942
|
const err = rootEl.querySelector('.ask-card-err');
|
|
2583
2943
|
const startBtn = rootEl.querySelector('[data-ask-card-start]');
|
|
2584
2944
|
err.textContent = '';
|
|
@@ -2590,7 +2950,10 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2590
2950
|
if (ex.error) { err.textContent = ex.error; return; }
|
|
2591
2951
|
const saveErr = await saveLaneEdits(local); // the previous phase's guard, now second
|
|
2592
2952
|
if (saveErr) { err.textContent = saveErr; return; }
|
|
2593
|
-
const body = { ...collectCardBody(rootEl, local, block.card || {}), askThreadId: st.threadId, askCardId: block.id };
|
|
2953
|
+
const body = { ...collectCardBody(rootEl, local, block.card || {}), askThreadId: st.threadId, askCardId: block.id, ...(schedule || {}) };
|
|
2954
|
+
// Run chains: "the branch of the run before it" is a flag, and the wire refuses it next to a branch name.
|
|
2955
|
+
// Keyed off the schedule being POSTED — a Start now on an after-proposal carries none and keeps the picked branch.
|
|
2956
|
+
if (body.sourceFromPrevious) { delete body.sourceBranch; delete body.sourceBranchByKey; }
|
|
2594
2957
|
if (ex.extras.length) body.extras = ex.extras;
|
|
2595
2958
|
let res = null;
|
|
2596
2959
|
try {
|
|
@@ -2704,19 +3067,24 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2704
3067
|
function isProgressBlock(block) {
|
|
2705
3068
|
const card = block.card || {};
|
|
2706
3069
|
if (card.type === PROGRESS_CARD_TYPE) return true;
|
|
2707
|
-
if (card.type === 'workflow' || card.type === 'metrics') return false;
|
|
3070
|
+
if (card.type === 'workflow' || card.type === 'metrics' || card.type === 'policy' || card.type === 'schedule' || card.type === 'model') return false;
|
|
2708
3071
|
return block.state === 'started' || (block.state === 'failed' && !!block.runId);
|
|
2709
3072
|
}
|
|
2710
3073
|
function buildCard(block) {
|
|
2711
3074
|
if (!st.cardEls) st.cardEls = new Map();
|
|
2712
3075
|
const cached = st.cardEls.get(block.id);
|
|
2713
3076
|
const isWorkflow = !!(block.card && block.card.type === 'workflow');
|
|
2714
|
-
|
|
3077
|
+
// A policy card is a metrics card with different words (buildMetricsCard branches on the type).
|
|
3078
|
+
const isMetrics = !!(block.card && (block.card.type === 'metrics' || block.card.type === 'policy'));
|
|
3079
|
+
const isSchedule = !!(block.card && block.card.type === 'schedule');
|
|
3080
|
+
const isModel = !!(block.card && block.card.type === 'model');
|
|
2715
3081
|
const isProgress = isProgressBlock(block);
|
|
2716
|
-
if (cached && cached.state === block.state && (isWorkflow || isMetrics || isProgress || block.state === 'proposed')) return cached.el;
|
|
3082
|
+
if (cached && cached.state === block.state && (isWorkflow || isMetrics || isSchedule || isModel || isProgress || block.state === 'proposed')) return cached.el;
|
|
2717
3083
|
if (cached) disposeCardEntry(cached);
|
|
2718
3084
|
const built = isWorkflow ? buildWorkflowCard(block, cached)
|
|
2719
3085
|
: isMetrics ? buildMetricsCard(block)
|
|
3086
|
+
: isSchedule ? buildScheduleCard(block)
|
|
3087
|
+
: isModel ? buildModelCard(block)
|
|
2720
3088
|
: isProgress ? buildProgressCard(block)
|
|
2721
3089
|
: { el: block.state === 'proposed' ? buildCardForm(block) : buildCardTerminal(block) };
|
|
2722
3090
|
st.cardEls.set(block.id, { el: built.el, state: block.state, handle: built.handle || null, dispose: built.dispose || null, animate: !!built.animate, cancelAnim: null, lastW: -1 });
|
|
@@ -2867,11 +3235,16 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2867
3235
|
|
|
2868
3236
|
function toolRow(block) {
|
|
2869
3237
|
const rowEl = make('div', 'ask-tool-row');
|
|
3238
|
+
rowEl.dataset.minLevel = 'advanced'; // what the assistant ran, step by step
|
|
2870
3239
|
const short = String(block.name || '').replace(/^mcp__worca__/, '');
|
|
2871
3240
|
const parts = short.split('_');
|
|
2872
3241
|
rowEl.appendChild(make('span', 'ask-tool-op', parts[0] || short));
|
|
2873
|
-
|
|
2874
|
-
|
|
3242
|
+
// A script tool reads as `test script runTests → blocking, exit 1` (§9.3): the op column
|
|
3243
|
+
// (a fixed 38 px cell) keeps the verb, the target column carries the key and the outcome —
|
|
3244
|
+
// a script's input is a whole program, so the JSON preview is worth nothing there.
|
|
3245
|
+
const script = scriptToolLine(short, block);
|
|
3246
|
+
const target = script ? script.target : parts.slice(1).join(' ');
|
|
3247
|
+
const preview = script ? '' : clipInput(block.input);
|
|
2875
3248
|
rowEl.appendChild(make('span', 'ask-tool-target', preview ? (target ? `${target} · ${preview}` : preview) : target));
|
|
2876
3249
|
const note = block.status === 'error' ? 'error' : block.status === 'running' ? '…' : fmtElapsed(block.durationMs);
|
|
2877
3250
|
rowEl.appendChild(make('span', 'ask-tool-note', note || ''));
|
|
@@ -2987,6 +3360,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2987
3360
|
const agentBlocks = blocks.filter((b) => b && b.kind === 'agent');
|
|
2988
3361
|
if (agentBlocks.length && !agents) {
|
|
2989
3362
|
agents = make('div', 'ask-agents');
|
|
3363
|
+
agents.dataset.minLevel = 'expert'; // per-agent logs (docs/ui-levels.md)
|
|
2990
3364
|
const cap = make('div', 'ask-agents-cap');
|
|
2991
3365
|
cap.appendChild(make('span', null, 'Sub-agents'));
|
|
2992
3366
|
agentsCount = make('span', 'ask-agents-count', '');
|