@worca/app 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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
|
@@ -91,3 +91,27 @@ export function collectChatSettings(root) {
|
|
|
91
91
|
}
|
|
92
92
|
return { notify, channels };
|
|
93
93
|
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The Settings → Ask Worca "Create and run scripts" row (scripts-workbench W20). Lives here
|
|
97
|
+
* with the other chat-pref controls because the value is one of `chatPrefs()`; it is mounted
|
|
98
|
+
* in the Ask Worca card, not in Chat notifications, and saves with that card's Save button.
|
|
99
|
+
* Default ON: only a stored `false` switches the chat's save_script / test_script off.
|
|
100
|
+
*/
|
|
101
|
+
export function renderScriptToolsToggle({ prefs } = {}, { doc = globalThis.document } = {}) {
|
|
102
|
+
const row = h(doc, 'label', 'check-row');
|
|
103
|
+
row.setAttribute('for', 'askScriptTools');
|
|
104
|
+
const cb = h(doc, 'input', 'ask-script-tools');
|
|
105
|
+
cb.type = 'checkbox';
|
|
106
|
+
cb.id = 'askScriptTools';
|
|
107
|
+
cb.checked = prefs?.scriptTools !== false;
|
|
108
|
+
row.appendChild(cb);
|
|
109
|
+
row.appendChild(doc.createTextNode(' Create and run scripts'));
|
|
110
|
+
return row;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** collectScriptToolsToggle(root) -> the POST /api/settings {chat} patch of the Ask Worca card. */
|
|
114
|
+
export function collectScriptToolsToggle(root) {
|
|
115
|
+
const cb = root && typeof root.querySelector === 'function' ? root.querySelector('input.ask-script-tools') : null;
|
|
116
|
+
return { scriptTools: cb ? cb.checked : true };
|
|
117
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// ui/public/code-editor.mjs
|
|
2
|
+
// The app's one code editor (scripts-workbench design §W16): a transparent
|
|
3
|
+
// <textarea> laid over a highlighted <pre><code>. The browser keeps the caret,
|
|
4
|
+
// IME and selection of a plain textarea — and its undo ring for TYPED text (a Tab
|
|
5
|
+
// indent assigns `.value`, which resets that ring: the price of no dependency);
|
|
6
|
+
// the colours are painted underneath. No contenteditable, no dependency, no build step.
|
|
7
|
+
//
|
|
8
|
+
// The highlighter is INJECTED (C11): app.js passes one built on the vendored
|
|
9
|
+
// hljs loader, tests pass an escaping stub, and the DEFAULT escapes HTML — so a
|
|
10
|
+
// script's own source can never reach innerHTML unescaped. That injected
|
|
11
|
+
// function is the ONLY thing in this module allowed near innerHTML.
|
|
12
|
+
//
|
|
13
|
+
// The textarea keeps the caller's `name` as data-field, so an editor dropped
|
|
14
|
+
// into the composer's inspector routes through the same delegated change
|
|
15
|
+
// listener a plain textarea did (P1b Task 6).
|
|
16
|
+
|
|
17
|
+
const ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
18
|
+
|
|
19
|
+
/** Escape the five HTML metacharacters. Pure; the default highlighter. */
|
|
20
|
+
export function escapeHtml(s) {
|
|
21
|
+
return String(s ?? '').replace(/[&<>"']/g, (c) => ESCAPES[c]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Trailing-edge: a burst of keystrokes costs ONE highlight pass. 60 ms is below
|
|
25
|
+
// the threshold where a repaint reads as lag and well above a fast typist's
|
|
26
|
+
// inter-key gap.
|
|
27
|
+
export const HIGHLIGHT_DEBOUNCE_MS = 60;
|
|
28
|
+
// Two spaces, matching the repo's own indentation; a literal tab would render at
|
|
29
|
+
// the browser's default width and fight the highlighted layer's alignment.
|
|
30
|
+
export const TAB_SPACES = ' ';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {object} opts
|
|
34
|
+
* @param {Document} [opts.doc]
|
|
35
|
+
* @param {string} [opts.value] initial text
|
|
36
|
+
* @param {string} [opts.language] hljs language id, mirrored on data-language
|
|
37
|
+
* @param {boolean} [opts.readOnly]
|
|
38
|
+
* @param {number} [opts.rows]
|
|
39
|
+
* @param {string} [opts.name] data-field on the textarea ('' = none)
|
|
40
|
+
* @param {?function} [opts.onInput] called with the new text on every keystroke
|
|
41
|
+
* @param {function} [opts.highlight] (text, language) -> Promise<string html>
|
|
42
|
+
* @returns {{el:HTMLElement, getValue:()=>string, setValue:(v:string)=>void,
|
|
43
|
+
* setLanguage:(l:string)=>void, setReadOnly:(b:boolean)=>void,
|
|
44
|
+
* focus:()=>void, destroy:()=>void}}
|
|
45
|
+
*/
|
|
46
|
+
export function createCodeEditor({
|
|
47
|
+
doc = globalThis.document, value = '', language = 'javascript', readOnly = false,
|
|
48
|
+
rows = 18, name = '', onInput = null, highlight = async (text) => escapeHtml(text),
|
|
49
|
+
} = {}) {
|
|
50
|
+
const win = doc.defaultView || globalThis;
|
|
51
|
+
let lang = String(language || '');
|
|
52
|
+
let seq = 0; // paint token: only the newest result may be applied
|
|
53
|
+
let timer = null;
|
|
54
|
+
let dead = false;
|
|
55
|
+
|
|
56
|
+
const el = doc.createElement('div');
|
|
57
|
+
el.className = `code-editor${readOnly ? ' ro' : ''}`;
|
|
58
|
+
el.dataset.language = lang;
|
|
59
|
+
|
|
60
|
+
const pre = doc.createElement('pre');
|
|
61
|
+
pre.className = 'code-editor-hl';
|
|
62
|
+
pre.setAttribute('aria-hidden', 'true'); // the textarea is the accessible control
|
|
63
|
+
const code = doc.createElement('code');
|
|
64
|
+
pre.appendChild(code);
|
|
65
|
+
|
|
66
|
+
const ta = doc.createElement('textarea');
|
|
67
|
+
ta.className = 'code-editor-ta mono';
|
|
68
|
+
ta.spellcheck = false;
|
|
69
|
+
ta.rows = rows;
|
|
70
|
+
ta.readOnly = Boolean(readOnly);
|
|
71
|
+
ta.value = String(value ?? '');
|
|
72
|
+
if (name) ta.dataset.field = name;
|
|
73
|
+
el.append(pre, ta);
|
|
74
|
+
|
|
75
|
+
/** The one innerHTML write. `html` comes from the injected highlighter (or the
|
|
76
|
+
* escaping default); anything that is not a string falls back to escaped text.
|
|
77
|
+
* The extra '\n' keeps the last row visible — <pre> swallows one trailing
|
|
78
|
+
* newline, which would otherwise scroll the layers out of step. */
|
|
79
|
+
function apply(html, text) {
|
|
80
|
+
code.innerHTML = (typeof html === 'string' ? html : escapeHtml(text)) + '\n';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function paint() {
|
|
84
|
+
const my = ++seq;
|
|
85
|
+
const text = ta.value;
|
|
86
|
+
const at = lang;
|
|
87
|
+
let html = null;
|
|
88
|
+
try { html = await highlight(text, at); } catch { html = null; }
|
|
89
|
+
if (dead || my !== seq) return; // destroyed, or a newer paint won
|
|
90
|
+
apply(html, text);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function schedule() {
|
|
94
|
+
if (timer != null) win.clearTimeout(timer);
|
|
95
|
+
timer = win.setTimeout(() => { timer = null; void paint(); }, HIGHLIGHT_DEBOUNCE_MS);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function onInputEvent() {
|
|
99
|
+
if (typeof onInput === 'function') onInput(ta.value);
|
|
100
|
+
schedule();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function onScroll() {
|
|
104
|
+
pre.scrollTop = ta.scrollTop;
|
|
105
|
+
pre.scrollLeft = ta.scrollLeft;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Escape arms the NEXT Tab to move focus instead of indenting. The textarea is
|
|
109
|
+
// the accessible control here (the <pre> is aria-hidden), and an indent handler
|
|
110
|
+
// that swallows every Tab and Shift+Tab would make it a keyboard trap — no
|
|
111
|
+
// keyboard user could ever leave it. Escape-then-Tab is the editor convention.
|
|
112
|
+
let tabOut = false;
|
|
113
|
+
/** Leaving the field cancels a pending Escape: coming back and pressing Tab must
|
|
114
|
+
* indent again, the way it does on a field nobody armed. */
|
|
115
|
+
function onBlur() { tabOut = false; }
|
|
116
|
+
|
|
117
|
+
/** Tab / Shift+Tab. A caret with no multi-line selection inserts two spaces;
|
|
118
|
+
* anything spanning a newline (and every Shift+Tab) re-indents whole lines,
|
|
119
|
+
* which is what an editor user expects from a code field. Read-only lets Tab
|
|
120
|
+
* through so focus still moves on. */
|
|
121
|
+
function onKeyDown(ev) {
|
|
122
|
+
if (ev.key === 'Escape') { tabOut = true; return; }
|
|
123
|
+
const armed = tabOut;
|
|
124
|
+
tabOut = false;
|
|
125
|
+
if (ev.key !== 'Tab' || ev.ctrlKey || ev.metaKey || ev.altKey || ta.readOnly || armed) return;
|
|
126
|
+
const v = ta.value;
|
|
127
|
+
const start = ta.selectionStart;
|
|
128
|
+
const end = ta.selectionEnd;
|
|
129
|
+
ev.preventDefault();
|
|
130
|
+
if (!ev.shiftKey && !v.slice(start, end).includes('\n')) {
|
|
131
|
+
ta.value = v.slice(0, start) + TAB_SPACES + v.slice(end);
|
|
132
|
+
ta.selectionStart = start + TAB_SPACES.length;
|
|
133
|
+
ta.selectionEnd = ta.selectionStart;
|
|
134
|
+
ta.dispatchEvent(new win.Event('input', { bubbles: true }));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const lineStart = v.lastIndexOf('\n', start - 1) + 1;
|
|
138
|
+
const nl = v.indexOf('\n', end);
|
|
139
|
+
const blockEnd = nl === -1 ? v.length : nl;
|
|
140
|
+
let first = 0;
|
|
141
|
+
let total = 0;
|
|
142
|
+
const lines = v.slice(lineStart, blockEnd).split('\n').map((line, i) => {
|
|
143
|
+
if (ev.shiftKey) {
|
|
144
|
+
const cut = line.startsWith(TAB_SPACES) ? TAB_SPACES.length : (line.startsWith(' ') ? 1 : 0);
|
|
145
|
+
if (i === 0) first = -cut;
|
|
146
|
+
total -= cut;
|
|
147
|
+
return line.slice(cut);
|
|
148
|
+
}
|
|
149
|
+
if (i === 0) first = TAB_SPACES.length;
|
|
150
|
+
total += TAB_SPACES.length;
|
|
151
|
+
return TAB_SPACES + line;
|
|
152
|
+
});
|
|
153
|
+
ta.value = v.slice(0, lineStart) + lines.join('\n') + v.slice(blockEnd);
|
|
154
|
+
ta.selectionStart = Math.max(lineStart, start + first);
|
|
155
|
+
ta.selectionEnd = Math.max(ta.selectionStart, end + total);
|
|
156
|
+
ta.dispatchEvent(new win.Event('input', { bubbles: true }));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
ta.addEventListener('input', onInputEvent);
|
|
160
|
+
ta.addEventListener('scroll', onScroll);
|
|
161
|
+
ta.addEventListener('keydown', onKeyDown);
|
|
162
|
+
ta.addEventListener('blur', onBlur);
|
|
163
|
+
void paint();
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
el,
|
|
167
|
+
getValue: () => ta.value,
|
|
168
|
+
setValue(v) { ta.value = String(v ?? ''); void paint(); },
|
|
169
|
+
setLanguage(l) { lang = String(l || ''); el.dataset.language = lang; void paint(); },
|
|
170
|
+
setReadOnly(b) { ta.readOnly = Boolean(b); el.classList.toggle('ro', Boolean(b)); },
|
|
171
|
+
focus() { if (typeof ta.focus === 'function') ta.focus(); },
|
|
172
|
+
destroy() {
|
|
173
|
+
dead = true;
|
|
174
|
+
if (timer != null) { win.clearTimeout(timer); timer = null; }
|
|
175
|
+
ta.removeEventListener('input', onInputEvent);
|
|
176
|
+
ta.removeEventListener('scroll', onScroll);
|
|
177
|
+
ta.removeEventListener('keydown', onKeyDown);
|
|
178
|
+
ta.removeEventListener('blur', onBlur);
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -13,12 +13,19 @@ export const GETTING_STARTED_STEPS = Object.freeze([
|
|
|
13
13
|
{ id: 'project', label: 'Add your first project', vig: 'project' },
|
|
14
14
|
{ id: 'run', label: 'Watch a run end to end', vig: 'run' },
|
|
15
15
|
{ id: 'ask', label: 'Ask Worca about a run', vig: 'ask' },
|
|
16
|
-
{ id: 'workflows', label: 'Explore the built-in workflows', vig: 'workflow' },
|
|
17
16
|
{ id: 'realRun', label: 'Run a real pipeline', vig: 'realRun' },
|
|
18
|
-
{ id: '
|
|
19
|
-
{ id: '
|
|
17
|
+
{ id: 'workflows', label: 'Explore the built-in workflows', vig: 'workflow', level: 'advanced' },
|
|
18
|
+
{ id: 'workspace', label: 'Group projects into a workspace', vig: 'workspace', level: 'advanced' },
|
|
19
|
+
{ id: 'teamMetrics', label: 'Turn on team metrics', vig: 'teamMetrics', level: 'expert' },
|
|
20
|
+
{ id: 'teamPolicy', label: 'Set a team policy', vig: 'teamPolicy', level: 'expert' },
|
|
20
21
|
]);
|
|
21
22
|
|
|
23
|
+
// `level` (docs/ui-levels.md) is the interface mode a step's controls live in; absent = simple.
|
|
24
|
+
// Every tile shows at every mode — the count stays "n of 9" — but a step above the current mode
|
|
25
|
+
// wears its level, and its guide opens by ringing the mode switch.
|
|
26
|
+
const LEVEL_ORDER = ['simple', 'advanced', 'expert'];
|
|
27
|
+
const LEVEL_LABEL = { advanced: 'Advanced', expert: 'Expert' };
|
|
28
|
+
|
|
22
29
|
/** Seconds a tile holds the reveal sequence before the next arrives: an
|
|
23
30
|
* unfinished tile paints its drawing first, a finished one arrives already
|
|
24
31
|
* drawn and still, so it only needs its own fade. */
|
|
@@ -110,6 +117,18 @@ const VIGNETTES = {
|
|
|
110
117
|
wire(0.65, 'M64 35h30'),
|
|
111
118
|
shape(0.85, 'path', 'gs-chart gs-accent', 'd="M84 46l6-8 5 4 7-10"'),
|
|
112
119
|
],
|
|
120
|
+
// One document, read by every teammate: a page with its rules ticked, wired to three nodes.
|
|
121
|
+
teamPolicy: () => [
|
|
122
|
+
shape(0.05, 'rect', 'gs-frame', 'x="12" y="12" width="40" height="46" rx="6"'),
|
|
123
|
+
shape(0.25, 'path', 'gs-glyph', 'd="M22 24h20M22 33h20"'),
|
|
124
|
+
shape(0.4, 'path', 'gs-chart gs-accent', 'd="M22 45l4 4 8-8"'),
|
|
125
|
+
wire(0.5, 'M52 35 C 70 35, 76 20, 94 20'),
|
|
126
|
+
wire(0.6, 'M52 35h42'),
|
|
127
|
+
wire(0.7, 'M52 35 C 70 35, 76 50, 94 50'),
|
|
128
|
+
node(0.85, 100, 20, 5),
|
|
129
|
+
node(0.9, 100, 35, 5),
|
|
130
|
+
node(0.95, 100, 50, 5),
|
|
131
|
+
],
|
|
113
132
|
};
|
|
114
133
|
|
|
115
134
|
/**
|
|
@@ -138,9 +157,9 @@ export function vignette(doc, id, { animate = true, delay = 0 } = {}) {
|
|
|
138
157
|
* they are done, so any guide can be replayed.
|
|
139
158
|
* @param {Element} host
|
|
140
159
|
* @param {{steps?:Record<string,boolean>, hidden?:boolean}|null} status
|
|
141
|
-
* @param {{onStep?:(id:string)=>void, onHide?:()=>void, animate?:boolean, hideLabel?:string}} [handlers]
|
|
160
|
+
* @param {{onStep?:(id:string)=>void, onHide?:()=>void, animate?:boolean, hideLabel?:string, level?:string}} [handlers] `level`: the current interface mode
|
|
142
161
|
*/
|
|
143
|
-
export function renderGettingStarted(host, status, { onStep, onHide, animate = true, hideLabel = 'Hide' } = {}) {
|
|
162
|
+
export function renderGettingStarted(host, status, { onStep, onHide, animate = true, hideLabel = 'Hide', level = 'expert' } = {}) {
|
|
144
163
|
if (!host) return;
|
|
145
164
|
const doc = host.ownerDocument;
|
|
146
165
|
host.replaceChildren();
|
|
@@ -173,7 +192,7 @@ export function renderGettingStarted(host, status, { onStep, onHide, animate = t
|
|
|
173
192
|
const tiles = doc.createElement('div');
|
|
174
193
|
tiles.className = 'gs-tiles';
|
|
175
194
|
// Tiles arrive one at a time IN ORDER, so a new user reads one idea at a
|
|
176
|
-
// time instead of
|
|
195
|
+
// time instead of nine at once. A finished tile takes its turn but arrives
|
|
177
196
|
// already drawn and still, so it holds the sequence only briefly.
|
|
178
197
|
let at = 0;
|
|
179
198
|
for (const s of GETTING_STARTED_STEPS) {
|
|
@@ -197,6 +216,14 @@ export function renderGettingStarted(host, status, { onStep, onHide, animate = t
|
|
|
197
216
|
label.className = 'gs-label';
|
|
198
217
|
label.textContent = s.label;
|
|
199
218
|
tile.append(mark, vig, label);
|
|
219
|
+
if (s.level && LEVEL_ORDER.indexOf(s.level) > LEVEL_ORDER.indexOf(level)) {
|
|
220
|
+
const lv = doc.createElement('span');
|
|
221
|
+
lv.className = 'lv-pill gs-level';
|
|
222
|
+
lv.dataset.lv = s.level;
|
|
223
|
+
lv.textContent = LEVEL_LABEL[s.level];
|
|
224
|
+
tile.appendChild(lv);
|
|
225
|
+
tile.setAttribute('aria-label', `${tile.getAttribute('aria-label')} — part of ${LEVEL_LABEL[s.level]} mode`);
|
|
226
|
+
}
|
|
200
227
|
tile.addEventListener('click', () => onStep && onStep(s.id));
|
|
201
228
|
tiles.appendChild(tile);
|
|
202
229
|
}
|
|
@@ -207,7 +234,7 @@ export function renderGettingStarted(host, status, { onStep, onHide, animate = t
|
|
|
207
234
|
/* ------------------------------------------------------------------- pill */
|
|
208
235
|
|
|
209
236
|
/**
|
|
210
|
-
* The sidebar row under New pipeline: "Getting started · 2/
|
|
237
|
+
* The sidebar row under New pipeline: "Getting started · 2/9". It follows the
|
|
211
238
|
* user to every view (the guides leave the shelf behind) and vanishes once the
|
|
212
239
|
* checklist is hidden or complete — nothing left to nag about.
|
|
213
240
|
* @param {Element} host
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
import { createGraphView } from './view.mjs';
|
|
14
14
|
import { renderPalette, applyFilter, FLOW_GROUP } from './palette.mjs';
|
|
15
15
|
import { renderNodeInspector, renderWireInspector, renderEmptyInspector } from './inspector.mjs';
|
|
16
|
+
import { paramEditorHook } from '../script-forms.mjs';
|
|
16
17
|
import { renderSaveDialog, openDialog, closeDialog } from './save-dialog.mjs';
|
|
17
18
|
import { PORT_HIT_R, SNAP, ZOOM_MIN, ZOOM_MAX, ZOOM_K, ZOOM_STEP, NODE_W, snap }
|
|
18
19
|
from '../../../src/shared/graph/geometry.mjs';
|
|
19
20
|
import { hitRoute } from '../../../src/shared/graph/route.mjs';
|
|
21
|
+
import { PARAMS_PORT } from '../../../src/shared/graph/constants.mjs';
|
|
20
22
|
import { canWire, newNode, newWire, normalizeTemplate, serializeTemplate }
|
|
21
23
|
from '../../../src/shared/graph/template.mjs';
|
|
22
24
|
import { validateGraph } from '../../../src/shared/graph/validate.mjs';
|
|
@@ -47,7 +49,7 @@ export const pluginOriginName = (origin) => (typeof origin === 'string' && origi
|
|
|
47
49
|
? origin.slice('plugin:'.length) : '');
|
|
48
50
|
const isTyping = (t) => !!t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT' || t.isContentEditable);
|
|
49
51
|
|
|
50
|
-
export function createComposer(hostEls, { doc = globalThis.document, api, raf = null, viewport = null, storage = null, portsFn } = {}) {
|
|
52
|
+
export function createComposer(hostEls, { doc = globalThis.document, api, raf = null, viewport = null, storage = null, portsFn, highlight = null } = {}) {
|
|
51
53
|
const win = doc.defaultView || globalThis;
|
|
52
54
|
const schedule = raf || ((fn) => win.requestAnimationFrame(fn));
|
|
53
55
|
const stats = { rectReads: 0, frames: 0, pointerMoves: 0, validations: 0 };
|
|
@@ -65,7 +67,9 @@ export function createComposer(hostEls, { doc = globalThis.document, api, raf =
|
|
|
65
67
|
let lastReport = { ok: true, errors: [], warnings: [] };
|
|
66
68
|
const undoStack = [];
|
|
67
69
|
const redoStack = [];
|
|
68
|
-
let agents = {}; // key -> meta (palette
|
|
70
|
+
let agents = {}; // key -> agent meta (palette domain groups)
|
|
71
|
+
let scripts = {}; // key -> script meta (palette Scripts group); the view gets the MERGED index
|
|
72
|
+
const headerIndex = () => ({ ...agents, ...scripts }); // keys never collide (D16)
|
|
69
73
|
|
|
70
74
|
const view = createGraphView(hostEls.canvas, {
|
|
71
75
|
doc, mode: 'edit', portsFn, agents, viewport,
|
|
@@ -512,7 +516,7 @@ export function createComposer(hostEls, { doc = globalThis.document, api, raf =
|
|
|
512
516
|
|
|
513
517
|
function paintPalette() {
|
|
514
518
|
renderPalette(hostEls.palette, {
|
|
515
|
-
agents: Object.values(agents), placedKinds: tpl.nodes.map((n) => n.kind), collapsed, query, doc,
|
|
519
|
+
agents: Object.values(agents), scripts: Object.values(scripts), placedKinds: tpl.nodes.map((n) => n.kind), collapsed, query, doc,
|
|
516
520
|
});
|
|
517
521
|
}
|
|
518
522
|
/** Diagonal de-stacker: 24 tries, SNAP*2 per try so each attempt lands one
|
|
@@ -535,10 +539,15 @@ export function createComposer(hostEls, { doc = globalThis.document, api, raf =
|
|
|
535
539
|
const p = at || freeSlot(centerWorld());
|
|
536
540
|
let node = null;
|
|
537
541
|
commit('add', () => {
|
|
538
|
-
node = entry.kind
|
|
539
|
-
? newNode(entry.
|
|
540
|
-
:
|
|
542
|
+
node = entry.kind === 'script'
|
|
543
|
+
? newNode('script', entry.key, snap(p.x), snap(p.y))
|
|
544
|
+
: entry.kind
|
|
545
|
+
? newNode(entry.kind, null, snap(p.x), snap(p.y))
|
|
546
|
+
: newNode('agent', entry.key, snap(p.x), snap(p.y));
|
|
541
547
|
if (entry.kind === 'and' || entry.kind === 'or' || entry.kind === 'combine') node.config.arity = 2;
|
|
548
|
+
// D14: a config-ported script starts from the sidecar's defaultPorts, deep-copied — the card owns its ports from here.
|
|
549
|
+
const meta = entry.kind === 'script' ? scripts[entry.key] : null;
|
|
550
|
+
if (meta && meta.ports === 'config' && meta.defaultPorts) node.config.ports = JSON.parse(JSON.stringify(meta.defaultPorts));
|
|
542
551
|
tpl.nodes.push(node);
|
|
543
552
|
});
|
|
544
553
|
select({ kind: 'node', id: node.id });
|
|
@@ -551,7 +560,9 @@ export function createComposer(hostEls, { doc = globalThis.document, api, raf =
|
|
|
551
560
|
function onPalDown(ev) {
|
|
552
561
|
const btn = ev.target.closest && ev.target.closest('.ap');
|
|
553
562
|
if (!btn || btn.disabled || ev.button !== 0) return;
|
|
554
|
-
|
|
563
|
+
const entry = btn.dataset.kind === 'script' ? { kind: 'script', key: btn.dataset.key }
|
|
564
|
+
: btn.dataset.kind ? { kind: btn.dataset.kind } : { key: btn.dataset.key };
|
|
565
|
+
drag = { entry,
|
|
555
566
|
sx: ev.clientX, sy: ev.clientY, id: ev.pointerId, label: btn.querySelector('.n').textContent, ghost: null, btn };
|
|
556
567
|
try { btn.setPointerCapture?.(ev.pointerId); } catch { /* synthetic */ }
|
|
557
568
|
doc.addEventListener('pointermove', onPalMove);
|
|
@@ -618,15 +629,28 @@ export function createComposer(hostEls, { doc = globalThis.document, api, raf =
|
|
|
618
629
|
paintInspector();
|
|
619
630
|
}
|
|
620
631
|
|
|
632
|
+
// Code editors mounted into the inspector. Each owns a debounce timer and three
|
|
633
|
+
// listeners, so every repaint destroys the previous set before the tree goes —
|
|
634
|
+
// a pending highlight against a detached node is a leak, not a crash, and this
|
|
635
|
+
// is the only place that sees both sides of the swap. With no `highlight`
|
|
636
|
+
// injected the hook is null and renderParamsForm keeps P1b's plain textarea.
|
|
637
|
+
const insEditors = [];
|
|
638
|
+
const insEditorFor = paramEditorHook({ doc, highlight, editors: insEditors });
|
|
639
|
+
function disposeInsEditors() {
|
|
640
|
+
for (const ed of insEditors) ed.destroy();
|
|
641
|
+
insEditors.length = 0;
|
|
642
|
+
}
|
|
643
|
+
|
|
621
644
|
function paintInspector() {
|
|
622
645
|
const hostBody = hostEls.insBody;
|
|
623
646
|
if (!hostBody) return;
|
|
647
|
+
disposeInsEditors();
|
|
624
648
|
if (!sel) return void hostBody.replaceChildren(renderEmptyInspector({ doc }));
|
|
625
649
|
if (sel.kind === 'node') {
|
|
626
650
|
const node = nodeById(sel.id);
|
|
627
651
|
if (!node) return void hostBody.replaceChildren(renderEmptyInspector({ doc }));
|
|
628
|
-
const meta = node.kind === 'agent' ? (agents[node.key] || null) : null;
|
|
629
|
-
return void hostBody.replaceChildren(renderNodeInspector(node, { template: tpl, portsFn, meta, models, efforts, subagentModels, doc }));
|
|
652
|
+
const meta = node.kind === 'agent' ? (agents[node.key] || null) : (node.kind === 'script' ? (scripts[node.key] || null) : null);
|
|
653
|
+
return void hostBody.replaceChildren(renderNodeInspector(node, { template: tpl, portsFn, meta, models, efforts, subagentModels, editorFor: insEditorFor, doc }));
|
|
630
654
|
}
|
|
631
655
|
const wire = wireById(sel.id);
|
|
632
656
|
if (!wire) return void hostBody.replaceChildren(renderEmptyInspector({ doc }));
|
|
@@ -664,6 +688,22 @@ export function createComposer(hostEls, { doc = globalThis.document, api, raf =
|
|
|
664
688
|
setTab(btn.dataset.tab, { persist: true });
|
|
665
689
|
}
|
|
666
690
|
|
|
691
|
+
/** node.config.ports, cloned one level deep so a commit never mutates the undo ring's copy. */
|
|
692
|
+
const clonePorts = (raw) => ({
|
|
693
|
+
inputs: (Array.isArray(raw && raw.inputs) ? raw.inputs : []).map((p) => ({ ...p })),
|
|
694
|
+
outputs: (Array.isArray(raw && raw.outputs) ? raw.outputs : []).map((p) => ({ ...p })),
|
|
695
|
+
});
|
|
696
|
+
const nextPortId = (list, base) => { let i = 0; for (;;) { const id = i ? `${base}${i + 1}` : base; if (!list.some((p) => p.id === id)) return id; i += 1; } };
|
|
697
|
+
/** A param control's value in the declared type; undefined = delete the key. */
|
|
698
|
+
function coerceParam(decl, target) {
|
|
699
|
+
if (!decl) return target.value === '' ? undefined : target.value;
|
|
700
|
+
if (decl.type === 'boolean') return Boolean(target.checked);
|
|
701
|
+
if (decl.type === 'number') { const n = Number(target.value); return target.value === '' || !Number.isFinite(n) ? undefined : n; }
|
|
702
|
+
// A command or code value of blanks alone is no value: the runner trims it away, so V22 must see it as missing.
|
|
703
|
+
if ((decl.type === 'command' || decl.type === 'code') && target.value.trim() === '') return undefined;
|
|
704
|
+
return target.value === '' ? undefined : target.value;
|
|
705
|
+
}
|
|
706
|
+
|
|
667
707
|
function onInspectorChange(ev) {
|
|
668
708
|
const name = ev.target.dataset && ev.target.dataset.field;
|
|
669
709
|
if (!name || !sel) return;
|
|
@@ -680,12 +720,72 @@ export function createComposer(hostEls, { doc = globalThis.document, api, raf =
|
|
|
680
720
|
}
|
|
681
721
|
const node = nodeById(sel.id);
|
|
682
722
|
if (!node) return;
|
|
723
|
+
if (node.kind === 'script' && name.startsWith('param:')) {
|
|
724
|
+
const id = name.slice('param:'.length);
|
|
725
|
+
const decl = ((scripts[node.key] && scripts[node.key].params) || []).find((p) => p.id === id);
|
|
726
|
+
commit(name, () => {
|
|
727
|
+
const params = { ...(node.config.params || {}) };
|
|
728
|
+
const v = coerceParam(decl, ev.target);
|
|
729
|
+
if (v === undefined) delete params[id]; else params[id] = v;
|
|
730
|
+
if (Object.keys(params).length) node.config.params = params; else delete node.config.params;
|
|
731
|
+
});
|
|
732
|
+
paintInspector();
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (node.kind === 'script' && name === 'timeoutMs') {
|
|
736
|
+
const secs = Number(ev.target.value);
|
|
737
|
+
commit(name, () => {
|
|
738
|
+
if (ev.target.value !== '' && Number.isFinite(secs) && secs >= 1) node.config.timeoutMs = Math.round(secs * 1000);
|
|
739
|
+
else delete node.config.timeoutMs;
|
|
740
|
+
});
|
|
741
|
+
paintInspector();
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (node.kind === 'script' && name.startsWith('port:')) {
|
|
745
|
+
const [, dir, idx, fieldName] = name.split(':');
|
|
746
|
+
if (fieldName === 'id') {
|
|
747
|
+
// Wires follow a port BY ID (below), so an id that is blank, reserved or already taken on this side is
|
|
748
|
+
// refused and the box snaps back: passing through '' would strand the wires on a port that cannot be
|
|
749
|
+
// drawn, and two ports sharing an id would hand one port's wires to the other on the next rename.
|
|
750
|
+
const next = ev.target.value.trim();
|
|
751
|
+
const side = node.config.ports && Array.isArray(node.config.ports[dir]) ? node.config.ports[dir] : [];
|
|
752
|
+
const engineOwned = next === 'await' || (dir === 'inputs' && next === PARAMS_PORT.id && node.config.paramsPort === true);
|
|
753
|
+
if (!next || engineOwned || side.some((q, j) => j !== Number(idx) && q && q.id === next)) return void paintInspector();
|
|
754
|
+
}
|
|
755
|
+
commit(name, () => {
|
|
756
|
+
const ports = clonePorts(node.config.ports);
|
|
757
|
+
const p = ports[dir] && ports[dir][Number(idx)];
|
|
758
|
+
if (!p) return;
|
|
759
|
+
if (fieldName === 'required') p.required = Boolean(ev.target.checked);
|
|
760
|
+
else if (fieldName === 'loop') { if (ev.target.checked) p.loop = true; else delete p.loop; }
|
|
761
|
+
else if (fieldName === 'id') {
|
|
762
|
+
// A rename carries the port's wires with it: a wire into a port id that no longer exists is
|
|
763
|
+
// never drawn, so it could be neither selected nor deleted and the graph would stay invalid.
|
|
764
|
+
const old = p.id;
|
|
765
|
+
p.id = ev.target.value.trim();
|
|
766
|
+
if (old && old !== p.id) for (const w of tpl.wires) { const end = dir === 'inputs' ? w.to : w.from; if (end.node === node.id && end.port === old) end.port = p.id; }
|
|
767
|
+
}
|
|
768
|
+
else if (ev.target.value === '') delete p[fieldName];
|
|
769
|
+
else p[fieldName] = ev.target.value;
|
|
770
|
+
if (fieldName === 'type' && ev.target.value === 'void') delete p.filename;
|
|
771
|
+
node.config.ports = ports;
|
|
772
|
+
});
|
|
773
|
+
paintInspector();
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
683
776
|
commit(name, () => {
|
|
684
777
|
if (name === 'arity') {
|
|
685
778
|
const n = Number.parseInt(ev.target.value, 10);
|
|
686
779
|
node.config.arity = Number.isInteger(n) && n >= 2 ? n : 2; // V12 floor
|
|
687
780
|
} else if (ev.target.type === 'checkbox') {
|
|
688
781
|
if (ev.target.checked) node.config[name] = true; else delete node.config[name];
|
|
782
|
+
// The params port leaves with its toggle, and its wires with it — a wire into a port that no longer
|
|
783
|
+
// exists is never drawn, so it could be neither selected nor deleted (same reason as a removed config port).
|
|
784
|
+
// Resolved AFTER the delete: an input still called `params` is the script's OWN (a stuck opt-in V22
|
|
785
|
+
// refuses), and the wires into it are the user's.
|
|
786
|
+
if (name === 'paramsPort' && !ev.target.checked && !(portsFn(node)?.inputs || []).some((p) => p && p.id === PARAMS_PORT.id)) {
|
|
787
|
+
tpl.wires = tpl.wires.filter((w) => !(w.to.node === node.id && w.to.port === PARAMS_PORT.id));
|
|
788
|
+
}
|
|
689
789
|
} else if (ev.target.value === '') {
|
|
690
790
|
delete node.config[name];
|
|
691
791
|
} else {
|
|
@@ -695,6 +795,30 @@ export function createComposer(hostEls, { doc = globalThis.document, api, raf =
|
|
|
695
795
|
paintInspector(); // re-read the committed value
|
|
696
796
|
}
|
|
697
797
|
|
|
798
|
+
/** The port editor's add/remove buttons (script cards only). */
|
|
799
|
+
function onInspectorClick(ev) {
|
|
800
|
+
const add = ev.target.closest && ev.target.closest('[data-port-add]');
|
|
801
|
+
const rm = ev.target.closest && ev.target.closest('[data-port-remove]');
|
|
802
|
+
if ((!add && !rm) || !sel || sel.kind !== 'node') return;
|
|
803
|
+
const node = nodeById(sel.id);
|
|
804
|
+
if (!node || node.kind !== 'script') return;
|
|
805
|
+
commit('ports', () => {
|
|
806
|
+
const ports = clonePorts(node.config.ports);
|
|
807
|
+
if (add) {
|
|
808
|
+
const dir = add.dataset.portAdd;
|
|
809
|
+
if (dir === 'inputs') ports.inputs.push({ id: nextPortId(ports.inputs, 'in'), type: 'md', required: false });
|
|
810
|
+
else { const id = nextPortId(ports.outputs, 'out'); ports.outputs.push({ id, type: 'md', when: 'always', filename: `${id}-cycle{cycle}.md` }); }
|
|
811
|
+
} else {
|
|
812
|
+
const [dir, idx] = rm.dataset.portRemove.split(':');
|
|
813
|
+
const gone = ports[dir] ? ports[dir].splice(Number(idx), 1)[0] : null;
|
|
814
|
+
// The removed port's wires go with it (same reason as the rename above).
|
|
815
|
+
if (gone && gone.id) tpl.wires = tpl.wires.filter((w) => { const end = dir === 'inputs' ? w.to : w.from; return !(end.node === node.id && end.port === gone.id); });
|
|
816
|
+
}
|
|
817
|
+
node.config.ports = ports;
|
|
818
|
+
});
|
|
819
|
+
paintInspector();
|
|
820
|
+
}
|
|
821
|
+
|
|
698
822
|
let dialog = null;
|
|
699
823
|
let savedDomains = [];
|
|
700
824
|
|
|
@@ -812,6 +936,7 @@ export function createComposer(hostEls, { doc = globalThis.document, api, raf =
|
|
|
812
936
|
hostEls.palette?.addEventListener('click', onPalClick);
|
|
813
937
|
hostEls.filter?.addEventListener('input', onFilterInput);
|
|
814
938
|
hostEls.insBody?.addEventListener('change', onInspectorChange);
|
|
939
|
+
hostEls.insBody?.addEventListener('click', onInspectorClick);
|
|
815
940
|
hostEls.insToggle?.addEventListener('click', onRailToggle);
|
|
816
941
|
hostEls.insTabs?.addEventListener('click', onTabClick);
|
|
817
942
|
hostEls.saveBtn?.addEventListener('click', onSaveClick);
|
|
@@ -852,6 +977,8 @@ export function createComposer(hostEls, { doc = globalThis.document, api, raf =
|
|
|
852
977
|
hostEls.palette?.removeEventListener('click', onPalClick);
|
|
853
978
|
hostEls.filter?.removeEventListener('input', onFilterInput);
|
|
854
979
|
hostEls.insBody?.removeEventListener('change', onInspectorChange);
|
|
980
|
+
hostEls.insBody?.removeEventListener('click', onInspectorClick);
|
|
981
|
+
disposeInsEditors();
|
|
855
982
|
hostEls.insToggle?.removeEventListener('click', onRailToggle);
|
|
856
983
|
hostEls.insTabs?.removeEventListener('click', onTabClick);
|
|
857
984
|
hostEls.saveBtn?.removeEventListener('click', onSaveClick);
|
|
@@ -922,7 +1049,8 @@ export function createComposer(hostEls, { doc = globalThis.document, api, raf =
|
|
|
922
1049
|
setReady(v) { ready = v; paintChrome(); },
|
|
923
1050
|
// A registry reload (MAJ-16) must re-validate the OPEN canvas: a wire into a
|
|
924
1051
|
// port the Agents view just deleted is an error now, not at the next edit.
|
|
925
|
-
setAgents(map) { agents = map || {}; view.setAgents(
|
|
1052
|
+
setAgents(map) { agents = map || {}; view.setAgents(headerIndex()); if (tpl.nodes.length) scheduleValidate(); },
|
|
1053
|
+
setScripts(map) { scripts = map || {}; view.setAgents(headerIndex()); if (tpl.nodes.length) scheduleValidate(); },
|
|
926
1054
|
/** Rename: an unsaved edit, so it sets `dirty` — it never clears it. */
|
|
927
1055
|
setName(name) { tpl.name = String(name || ''); metaDirty = true; dirty = true; paintChrome(); },
|
|
928
1056
|
/** 'plugin:<name>' of the loaded row, '' when user-created. */
|