@wenathlan/extension 1.1.52 → 1.1.53

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.
@@ -1,9 +1,324 @@
1
+ // workflow.ts
2
+ function nestedparamof(value) {
3
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
4
+ const candidate = value;
5
+ if (typeof candidate.name !== "string" || !/^[a-z][a-z0-9]*$/.test(candidate.name)) return void 0;
6
+ if (!variablekinds.includes(candidate.kind)) return void 0;
7
+ if (candidate.default !== void 0 && !["string", "number", "boolean"].includes(typeof candidate.default) && !Array.isArray(candidate.default)) return void 0;
8
+ return { name: candidate.name, kind: candidate.kind, ...candidate.default !== void 0 ? { default: candidate.default } : {} };
9
+ }
10
+ function workflowstepof(value) {
11
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
12
+ const candidate = value;
13
+ if (typeof candidate.id !== "string" || !candidate.id.trim()) return void 0;
14
+ if (typeof candidate.kind !== "string" || !/^[a-z]+$/.test(candidate.kind)) return void 0;
15
+ if (typeof candidate.label !== "string" || !candidate.label.trim()) return void 0;
16
+ if (candidate.target !== void 0 && (typeof candidate.target !== "string" || !candidate.target)) return void 0;
17
+ if (candidate.value !== void 0 && typeof candidate.value !== "string") return void 0;
18
+ if (candidate.options !== void 0 && typeof candidate.options !== "string") return void 0;
19
+ if (candidate.breakpoint !== void 0 && typeof candidate.breakpoint !== "boolean") return void 0;
20
+ const bindings = Array.isArray(candidate.bindings) ? candidate.bindings.flatMap((binding) => bindingof(binding) !== void 0 ? [bindingof(binding)] : []) : void 0;
21
+ if (candidate.bindings !== void 0 && bindings === void 0) return void 0;
22
+ if (Array.isArray(candidate.bindings) && bindings !== void 0 && bindings.length !== candidate.bindings.length) return void 0;
23
+ const expression = candidate.expression === void 0 ? void 0 : expressionof(candidate.expression);
24
+ if (candidate.expression !== void 0 && expression === void 0) return void 0;
25
+ const extract = candidate.extract === void 0 ? void 0 : regexruleof(candidate.extract);
26
+ if (candidate.extract !== void 0 && extract === void 0) return void 0;
27
+ const params = Array.isArray(candidate.params) ? candidate.params.flatMap((param) => nestedparamof(param) !== void 0 ? [nestedparamof(param)] : []) : void 0;
28
+ if (candidate.params !== void 0 && params === void 0) return void 0;
29
+ if (Array.isArray(candidate.params) && params !== void 0 && params.length !== candidate.params.length) return void 0;
30
+ return { id: candidate.id, kind: candidate.kind, label: candidate.label, ...candidate.target !== void 0 ? { target: candidate.target } : {}, ...candidate.value !== void 0 ? { value: candidate.value } : {}, ...candidate.options !== void 0 ? { options: candidate.options } : {}, ...bindings !== void 0 && bindings.length > 0 ? { bindings } : {}, ...expression !== void 0 ? { expression } : {}, ...extract !== void 0 ? { extract } : {}, ...candidate.breakpoint === true ? { breakpoint: true } : {}, ...params !== void 0 && params.length > 0 ? { params } : {} };
31
+ }
32
+ function bindingof(value) {
33
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
34
+ const candidate = value;
35
+ if (typeof candidate.variable !== "string" || !/^[a-z][a-z0-9]*$/.test(candidate.variable)) return void 0;
36
+ if (!variablekinds.includes(candidate.kind)) return void 0;
37
+ if (typeof candidate.stepid !== "string" || !candidate.stepid.trim()) return void 0;
38
+ if (candidate.path !== void 0 && (typeof candidate.path !== "string" || !candidate.path.trim())) return void 0;
39
+ return { variable: candidate.variable, kind: candidate.kind, stepid: candidate.stepid, ...candidate.path !== void 0 ? { path: candidate.path } : {} };
40
+ }
41
+ var variablekinds = ["string", "number", "boolean", "list", "element"];
42
+ var expressionoperators = ["add", "subtract", "multiply", "divide", "modulo", "equal", "notequal", "less", "greater", "lessequal", "greaterequal", "and", "or", "not", "concat", "contains", "length"];
43
+ function expressionof(value) {
44
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
45
+ const candidate = value;
46
+ const left = operandof(candidate.left);
47
+ if (!left) return void 0;
48
+ const right = candidate.right === void 0 ? void 0 : operandof(candidate.right);
49
+ if (candidate.right !== void 0 && right === void 0) return void 0;
50
+ if (typeof candidate.operator !== "string" || !expressionoperators.includes(candidate.operator)) return void 0;
51
+ if (typeof candidate.result !== "string" || !/^[a-z][a-z0-9]*$/.test(candidate.result)) return void 0;
52
+ if (!variablekinds.includes(candidate.resultkind)) return void 0;
53
+ return { left, ...right !== void 0 ? { right } : {}, operator: candidate.operator, result: candidate.result, resultkind: candidate.resultkind };
54
+ }
55
+ function operandof(value) {
56
+ if (value === void 0) return void 0;
57
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return { literal: value };
58
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
59
+ const candidate = value;
60
+ if (typeof candidate.ref === "string" && /^[a-z][a-z0-9]*$/.test(candidate.ref)) return { ref: candidate.ref };
61
+ if (typeof candidate.literal === "string" || typeof candidate.literal === "number" || typeof candidate.literal === "boolean") return { literal: candidate.literal };
62
+ return void 0;
63
+ }
64
+ function regexruleof(value) {
65
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
66
+ const candidate = value;
67
+ if (typeof candidate.pattern !== "string" || !candidate.pattern.trim()) return void 0;
68
+ if (typeof candidate.flags !== "string" || !/^[dgimsuvy]*$/.test(candidate.flags)) return void 0;
69
+ const groups = Array.isArray(candidate.groups) ? candidate.groups.flatMap((group) => typeof group === "string" && /^[a-z][a-z0-9]*$/.test(group) ? [group] : []) : [];
70
+ if (candidate.groups !== void 0 && groups.length !== candidate.groups.length) return void 0;
71
+ return { pattern: candidate.pattern, flags: candidate.flags, groups };
72
+ }
73
+
1
74
  // policy.ts
2
75
  var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk", "paginateextract", "resumeextract", "batchdownload", "pausedownload", "resumedownload", "interceptmime", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "cleanupartifacts", "recordscreen", "captureaudio", "downloadimages", "callrest", "callgraphql", "sendmessage", "blockrequest", "mockresponse", "rewriteheaders", "setcookies", "clearcookies", "authflow", "saveapikey", "routeproxy", "postform", "postfiles", "attachcdp", "detachcdp", "cdpcmd", "overridescript", "heapshot", "profilecpu", "capturesourcemaps", "emulatedevice", "emulatenetwork", "emulatelocate", "setuseragent", "overridepermission", "restoresession", "exportsessions", "importsessions", "runworkflow", "visitrule", "urlrule", "menurule", "keyrule", "buttonrule", "cronrule", "intervalrule", "urllistrule", "webhookrule", "eventrule"]);
3
76
  var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction", "capturebodies", "setbreakpoint", "stepcode", "watchexpr", "loop", "repeatuntil", "whileloop", "foreach", "parallel", "trycatch"]);
4
77
  var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance", "verifydownload", "exportnetlog", "namecaptures", "shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet", "capturepdf", "captureframe", "readmedia", "readassets", "probestream", "timelapse", "shotcanvas", "convertimage", "makethumbs", "fetchurl", "parsejson", "parsehtml", "opensocket", "waitmessage", "watchrequests", "readheaders", "mapapi", "subscribesse", "longpoll", "extractapi", "readcookies", "watchconsole", "watcherrors", "watchtasks", "watchcdp", "measureflow", "trackmemory", "watchshifts", "traceload", "annotatetrace", "replaytrace", "blackboxscripts", "persiststate", "capturesession", "namedsessions", "diffsessions", "searchsessions", "composeworkflow", "savetemplate", "dryrun", "delay", "waitelement", "compute", "extractvars", "condition", "branch"]);
5
78
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
6
79
 
80
+ // workfloweditor.ts
81
+ var palettecategories = ["actions", "controlflow", "waits", "variables", "triggers"];
82
+ var noderowheight = 96;
83
+ var blockcolumnwidth = 280;
84
+ var canvasoriginx = 40;
85
+ function snapshotof(model) {
86
+ const { undo, redo, dirty, ...rest } = model;
87
+ void undo;
88
+ void redo;
89
+ void dirty;
90
+ return { ...rest, dirty: true };
91
+ }
92
+ function withundo(model, next) {
93
+ const undo = [...model.undo ?? [], snapshotof(model)];
94
+ const { redo, ...rest } = next;
95
+ void redo;
96
+ return { ...rest, dirty: true, undo };
97
+ }
98
+ function nodeidof(node) {
99
+ return node.id ?? (node.step !== void 0 ? node.step.id : node.invocation !== void 0 ? node.invocation.block : "");
100
+ }
101
+ function layoutsizeof(nodes) {
102
+ const width = Math.max(640, ...nodes.map((node) => node.x + blockcolumnwidth)) + 40;
103
+ const height = Math.max(480, ...nodes.map((node) => node.y + noderowheight)) + 40;
104
+ return { width, height };
105
+ }
106
+ function layouttypeof(nodes, layout) {
107
+ const size = layoutsizeof(nodes);
108
+ if (!layout) return { width: size.width, height: size.height, viewportx: 0, viewporty: 0, zoom: 1 };
109
+ return { width: Math.max(size.width, layout.width), height: Math.max(size.height, layout.height), viewportx: layout.viewportx, viewporty: layout.viewporty, zoom: layout.zoom };
110
+ }
111
+ function snapnode(model, nodeid, x, y, grid = 20) {
112
+ if (!Number.isFinite(grid) || grid <= 0) throw new Error("The snap grid must be a positive number.");
113
+ const index = model.nodes.findIndex((node2) => nodeidof(node2) === nodeid);
114
+ if (index < 0) throw new Error(`No canvas node matches ${nodeid}.`);
115
+ const node = model.nodes[index];
116
+ if (node.step === void 0) throw new Error("A block invocation node attaches through its own definition, not through snapping.");
117
+ const snappedx = Math.round(x / grid) * grid;
118
+ const snappedy = Math.round(y / grid) * grid;
119
+ let attached;
120
+ for (const [blockindex, block] of model.blocks.entries()) {
121
+ const columnx = canvasoriginx + (blockindex + 1) * blockcolumnwidth;
122
+ if (Math.abs(snappedx - columnx) <= blockcolumnwidth / 2) attached = block.name;
123
+ }
124
+ const { block: priorblock, ...rest } = node.step;
125
+ void priorblock;
126
+ const step = { ...rest, ...attached !== void 0 ? { block: attached } : {} };
127
+ const nodes = model.nodes.map((candidate, position) => position === index ? { step, x: snappedx, y: snappedy } : candidate);
128
+ const size = layouttypeof(nodes, model.layout);
129
+ const next = { ...model, nodes, layout: size };
130
+ return withundo(model, { ...next, minimap: renderminimap(next).minimap });
131
+ }
132
+ function reordersteps(model, nodeid, index) {
133
+ const current = model.nodes.findIndex((node) => nodeidof(node) === nodeid);
134
+ if (current < 0) throw new Error(`No canvas node matches ${nodeid}.`);
135
+ if (!Number.isInteger(index) || index < 0 || index > model.nodes.length - 1) throw new Error("The reorder index must address an existing position of the canvas list.");
136
+ const nodes = [...model.nodes];
137
+ const [moved] = nodes.splice(current, 1);
138
+ if (!moved) throw new Error("The reordered canvas node vanished.");
139
+ nodes.splice(index, 0, moved);
140
+ const next = { ...model, nodes };
141
+ return withundo(model, { ...next, minimap: renderminimap(next).minimap });
142
+ }
143
+ function groupselect(model, nodeids, blockname) {
144
+ if (!/^[a-z][a-z0-9]*$/.test(blockname)) throw new Error("The block name must be a unique lowercase word.");
145
+ if (model.blocks.some((block) => block.name === blockname)) throw new Error(`The block name ${blockname} already exists on the canvas.`);
146
+ const selected = nodeids.map((id) => {
147
+ const node = model.nodes.find((candidate) => nodeidof(candidate) === id);
148
+ if (!node || node.step === void 0) throw new Error(`The grouping selection must address step nodes; ${id} is not one.`);
149
+ return node;
150
+ });
151
+ if (selected.length === 0) throw new Error("The grouping selection needs at least one step node.");
152
+ const steps = selected.map((node) => node.step);
153
+ const blocks = [...model.blocks, { name: blockname, label: blockname, steps: steps.map((step) => ({ ...step })) }];
154
+ const firstindex = model.nodes.findIndex((node) => nodeidof(node) === nodeids[0]);
155
+ const invocationnode = { id: blockname, invocation: { block: blockname, label: blockname }, x: selected[0].x, y: selected[0].y };
156
+ const nodes = [];
157
+ model.nodes.forEach((node, index) => {
158
+ if (nodeids.includes(nodeidof(node))) {
159
+ if (index === firstindex) nodes.push(invocationnode);
160
+ return;
161
+ }
162
+ nodes.push(node);
163
+ });
164
+ const next = { ...model, nodes, blocks };
165
+ return withundo(model, { ...next, minimap: renderminimap(next).minimap });
166
+ }
167
+ function addnode(model, step, index) {
168
+ const normalized = workflowstepof(step);
169
+ if (!normalized) throw new Error("The canvas insertion needs one reviewed workflow step.");
170
+ let id = normalized.id;
171
+ let suffix = 2;
172
+ const taken = new Set(model.nodes.map((node) => nodeidof(node)));
173
+ while (taken.has(id)) {
174
+ id = `${normalized.id}${suffix}`;
175
+ suffix += 1;
176
+ }
177
+ const position = index !== void 0 && Number.isInteger(index) && index >= 0 && index <= model.nodes.length ? index : model.nodes.length;
178
+ const nodes = [...model.nodes.slice(0, position), { step: { ...normalized, id }, x: canvasoriginx, y: 60 + position * noderowheight }, ...model.nodes.slice(position)];
179
+ const next = { ...model, nodes };
180
+ return withundo(model, { ...next, minimap: renderminimap(next).minimap });
181
+ }
182
+ function editstep(model, step) {
183
+ const normalized = workflowstepof(step);
184
+ if (!normalized) throw new Error("The step inspector edit needs one reviewed workflow step.");
185
+ const index = model.nodes.findIndex((node2) => node2.step?.id === normalized.id);
186
+ if (index < 0) throw new Error(`No canvas step matches ${normalized.id}.`);
187
+ const node = model.nodes[index];
188
+ const nodes = model.nodes.map((candidate, position) => position === index ? { step: { ...normalized, ...node.step?.block !== void 0 ? { block: node.step.block } : {}, ...node.step?.breakpoint === true ? { breakpoint: true } : {} }, x: node.x, y: node.y } : candidate);
189
+ const next = { ...model, nodes };
190
+ return withundo(model, { ...next, minimap: renderminimap(next).minimap });
191
+ }
192
+ function renderminimap(model, width = 160, height = 100) {
193
+ if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) throw new Error("The mini map size must be positive.");
194
+ const canvaswidth = Math.max(1, model.layout.width);
195
+ const canvasheight = Math.max(1, model.layout.height);
196
+ const scale = Math.min(width / canvaswidth, height / canvasheight);
197
+ const zoom = model.layout.zoom > 0 ? model.layout.zoom : 1;
198
+ const visiblewidth = canvaswidth / zoom;
199
+ const visibleheight = canvasheight / zoom;
200
+ const viewport = {
201
+ x: Math.max(0, Math.min(model.layout.viewportx, canvaswidth)) * scale,
202
+ y: Math.max(0, Math.min(model.layout.viewporty, canvasheight)) * scale,
203
+ width: visiblewidth * scale,
204
+ height: visibleheight * scale
205
+ };
206
+ const nodes = model.nodes.map((node) => ({ id: nodeidof(node), x: node.x * scale, y: node.y * scale }));
207
+ return { minimap: { width, height, scale, zoom, viewport }, nodes };
208
+ }
209
+ function minimapfocus(model, x, y, width = 160, height = 100) {
210
+ const projection = renderminimap(model, width, height);
211
+ if (projection.minimap.scale <= 0) return model;
212
+ const canvasx = x / projection.minimap.scale;
213
+ const canvasy = y / projection.minimap.scale;
214
+ const zoom = model.layout.zoom > 0 ? model.layout.zoom : 1;
215
+ const visiblewidth = model.layout.width / zoom;
216
+ const visibleheight = model.layout.height / zoom;
217
+ const viewportx = Math.max(0, Math.min(canvasx - visiblewidth / 2, Math.max(0, model.layout.width - visiblewidth)));
218
+ const viewporty = Math.max(0, Math.min(canvasy - visibleheight / 2, Math.max(0, model.layout.height - visibleheight)));
219
+ const next = { ...model, layout: { ...model.layout, viewportx, viewporty } };
220
+ return { ...next, minimap: renderminimap(next).minimap };
221
+ }
222
+ function zoomcanvas(model, zoom) {
223
+ if (!Number.isFinite(zoom) || zoom <= 0) throw new Error("The canvas zoom must be a positive number with no code ceiling.");
224
+ const next = { ...model, layout: { ...model.layout, zoom } };
225
+ const labelscale = zoom < 1 ? 1 / zoom : 1;
226
+ return { model: { ...next, minimap: renderminimap(next).minimap }, labelscale };
227
+ }
228
+ function searchsteps(model, query) {
229
+ const needle = query.trim().toLowerCase();
230
+ if (!needle) return [];
231
+ const results = [];
232
+ for (const node of model.nodes) {
233
+ if (node.step === void 0) continue;
234
+ const matched = [];
235
+ if (node.step.label.toLowerCase().includes(needle)) matched.push("label");
236
+ if (node.step.kind.toLowerCase().includes(needle)) matched.push("kind");
237
+ const variables = [
238
+ ...model.edges.filter((edge) => edge.to === node.step?.id || edge.from === node.step?.id).map((edge) => edge.variable),
239
+ ...node.step.expression !== void 0 ? [node.step.expression.result] : [],
240
+ ...node.step.extract !== void 0 ? node.step.extract.groups : []
241
+ ];
242
+ if (variables.some((name) => name.toLowerCase().includes(needle))) matched.push("variable");
243
+ if (matched.length > 0) results.push({ id: node.step.id, label: node.step.label, kind: node.step.kind, matched });
244
+ }
245
+ return results;
246
+ }
247
+ function markbreakpoint(model, stepid) {
248
+ const toggle = (step) => {
249
+ const { breakpoint, ...rest } = step;
250
+ void breakpoint;
251
+ return breakpoint === true ? rest : { ...rest, breakpoint: true };
252
+ };
253
+ const index = model.nodes.findIndex((node) => node.step?.id === stepid);
254
+ if (index >= 0) {
255
+ const node = model.nodes[index];
256
+ const step = node.step;
257
+ const nodes = model.nodes.map((candidate, position) => position === index ? { step: toggle(step), x: candidate.x, y: candidate.y } : candidate);
258
+ const next2 = { ...model, nodes };
259
+ return withundo(model, { ...next2, minimap: renderminimap(next2).minimap });
260
+ }
261
+ const blocks = model.blocks.map((block) => {
262
+ const stepindex = block.steps.findIndex((entry) => "kind" in entry && "label" in entry && !("block" in entry) && entry.id === stepid);
263
+ if (stepindex < 0) return block;
264
+ const steps = block.steps.map((entry, position) => position === stepindex ? toggle(entry) : entry);
265
+ return { ...block, steps };
266
+ });
267
+ if (blocks.every((block, position) => block === model.blocks[position])) throw new Error(`No canvas step matches ${stepid}.`);
268
+ const next = { ...model, blocks };
269
+ return withundo(model, next);
270
+ }
271
+ function bindparam(model, blockname, param) {
272
+ if (!/^[a-z][a-z0-9]*$/.test(param.name)) throw new Error("The nested parameter name must be a lowercase word.");
273
+ const index = model.nodes.findIndex((node2) => node2.invocation?.block === blockname);
274
+ if (index < 0) throw new Error(`No block invocation of ${blockname} sits on the canvas.`);
275
+ const node = model.nodes[index];
276
+ const invocation = node.invocation;
277
+ const params = [...(invocation.params ?? []).filter((existing) => existing.name !== param.name), { ...param }];
278
+ const nodes = model.nodes.map((candidate, position) => position === index ? { invocation: { ...invocation, params }, x: candidate.x, y: candidate.y } : candidate);
279
+ const next = { ...model, nodes };
280
+ return withundo(model, { ...next, minimap: renderminimap(next).minimap });
281
+ }
282
+ function addedge(model, edge) {
283
+ const from = model.nodes.findIndex((node) => nodeidof(node) === edge.from);
284
+ const to = model.nodes.findIndex((node) => nodeidof(node) === edge.to);
285
+ if (from < 0) throw new Error(`The canvas edge references the unknown source step ${edge.from}.`);
286
+ if (to < 0) throw new Error(`The canvas edge references the unknown target step ${edge.to}.`);
287
+ if (from >= to) throw new Error(`The canvas edge of ${edge.variable} would run backwards from ${edge.from} into ${edge.to} and form a cycle.`);
288
+ if (!/^[a-z][a-z0-9]*$/.test(edge.variable)) throw new Error("The bound variable name must be a lowercase word.");
289
+ const edges = [...model.edges.filter((candidate) => !(candidate.from === edge.from && candidate.to === edge.to && candidate.variable === edge.variable)), { ...edge, ...edge.path !== void 0 ? { path: edge.path } : {} }];
290
+ const next = { ...model, edges };
291
+ return withundo(model, next);
292
+ }
293
+ function removeedge(model, from, to, variable) {
294
+ const edges = model.edges.filter((candidate) => !(candidate.from === from && candidate.to === to && candidate.variable === variable));
295
+ if (edges.length === model.edges.length) throw new Error(`No canvas edge of ${variable} links ${from} into ${to}.`);
296
+ const next = { ...model, edges };
297
+ return withundo(model, next);
298
+ }
299
+ function removenode(model, nodeid) {
300
+ const index = model.nodes.findIndex((node) => nodeidof(node) === nodeid);
301
+ if (index < 0) throw new Error(`No canvas node matches ${nodeid}.`);
302
+ const nodes = model.nodes.filter((_, position) => position !== index);
303
+ const edges = model.edges.filter((edge) => edge.from !== nodeid && edge.to !== nodeid);
304
+ const next = { ...model, nodes, edges };
305
+ return withundo(model, { ...next, minimap: renderminimap(next).minimap });
306
+ }
307
+ function undoedit(model) {
308
+ const undo = model.undo ?? [];
309
+ if (undo.length === 0) return model;
310
+ const previous = undo[undo.length - 1];
311
+ const current = snapshotof(model);
312
+ return { ...previous, undo: undo.slice(0, -1), redo: [...model.redo ?? [], current] };
313
+ }
314
+ function redoedit(model) {
315
+ const redo = model.redo ?? [];
316
+ if (redo.length === 0) return model;
317
+ const next = redo[redo.length - 1];
318
+ const current = snapshotof(model);
319
+ return { ...next, redo: redo.slice(0, -1), undo: [...model.undo ?? [], current] };
320
+ }
321
+
7
322
  // extension/tabscommand.ts
8
323
  function switcherlist(tabs, recency, filter) {
9
324
  const needle = filter.trim().toLowerCase();
@@ -75,10 +390,26 @@ var emulationroot = document.querySelector("#emulation");
75
390
  var netviewroot = document.querySelector("#netview");
76
391
  var sessionsroot = document.querySelector("#sessions");
77
392
  var workflowsroot = document.querySelector("#workflows");
393
+ var workfloweditorroot = document.querySelector("#workfloweditor");
78
394
  var triggersroot = document.querySelector("#triggers");
79
395
  var triggerview = { manual: void 0, history: void 0 };
80
396
  var sessionsview = { term: "", window: "all", diffselection: [], restorereview: void 0, importreview: void 0 };
81
397
  var workflowview = { review: void 0, selected: "" };
398
+ var editorview = {
399
+ workflowid: "",
400
+ model: void 0,
401
+ selected: [],
402
+ inspector: "",
403
+ palettesearch: "",
404
+ librarysearch: "",
405
+ stepsearch: "",
406
+ historyfilter: { workflowid: "", outcome: "" },
407
+ history: void 0,
408
+ importreview: void 0,
409
+ diff: void 0,
410
+ library: void 0,
411
+ palette: void 0
412
+ };
82
413
  var statusnode = document.querySelector("#status");
83
414
  var progressnode = document.querySelector("#planprogress");
84
415
  var capabilitiestext = document.querySelector("#capabilitiestext");
@@ -2541,6 +2872,7 @@ async function refresh() {
2541
2872
  renderemulation(context);
2542
2873
  rendersessions(context);
2543
2874
  renderworkflows(context);
2875
+ renderworkfloweditor(context);
2544
2876
  rendertriggers(context);
2545
2877
  renderconsolediff();
2546
2878
  renderaudit(context.audit);
@@ -3202,6 +3534,957 @@ function renderworkflows(context) {
3202
3534
  }
3203
3535
  }
3204
3536
  }
3537
+ function savefile(filename, contents) {
3538
+ const url = URL.createObjectURL(new Blob([contents], { type: "application/octet-stream" }));
3539
+ const anchor = document.createElement("a");
3540
+ anchor.href = url;
3541
+ anchor.download = filename;
3542
+ anchor.click();
3543
+ setTimeout(() => URL.revokeObjectURL(url), 1e4);
3544
+ }
3545
+ function renderworkfloweditor(context) {
3546
+ if (!workfloweditorroot) return;
3547
+ workfloweditorroot.replaceChildren();
3548
+ const editor = context.editor;
3549
+ const workflows = context.workflow?.workflows ?? [];
3550
+ const title = document.createElement("p");
3551
+ title.textContent = `${workflows.length} workflow${workflows.length === 1 ? "" : "s"} in the library \xB7 ${editor?.versions.length ?? 0} version${(editor?.versions.length ?? 0) === 1 ? "" : "s"} \xB7 ${editor?.history.length ?? 0} run history entr${(editor?.history.length ?? 0) === 1 ? "y" : "ies"} \xB7 ${editor?.imports.length ?? 0} pending import${(editor?.imports.length ?? 0) === 1 ? "" : "s"} \xB7 ${editor?.overrides.length ?? 0} site override${(editor?.overrides.length ?? 0) === 1 ? "" : "s"} \xB7 ${editor?.watchdog.events.length ?? 0} watchdog event${(editor?.watchdog.events.length ?? 0) === 1 ? "" : "s"}.`;
3552
+ workfloweditorroot.append(title);
3553
+ const openrow = document.createElement("div");
3554
+ openrow.className = "actions";
3555
+ for (const record of workflows) {
3556
+ openrow.append(button(`${record.name} v${record.version}`, async () => {
3557
+ const loaded = await request({ kind: "editormodel", workflowid: record.id });
3558
+ editorview.workflowid = record.id;
3559
+ editorview.model = loaded.model;
3560
+ editorview.selected = [];
3561
+ editorview.inspector = "";
3562
+ status(`Opened ${record.name} v${record.version} on the canvas with ${loaded.model.nodes.length} nodes.`);
3563
+ await refresh();
3564
+ }), " ");
3565
+ }
3566
+ if (editorview.model !== void 0) openrow.append(button("Close canvas", async () => {
3567
+ editorview.workflowid = "";
3568
+ editorview.model = void 0;
3569
+ editorview.selected = [];
3570
+ editorview.inspector = "";
3571
+ editorview.diff = void 0;
3572
+ status("Canvas closed; the stored versions survive.");
3573
+ await refresh();
3574
+ }));
3575
+ workfloweditorroot.append(openrow);
3576
+ const model = editorview.model;
3577
+ if (model !== void 0) {
3578
+ const nodeelement = (node) => {
3579
+ const element = document.createElement("div");
3580
+ element.className = "editornode";
3581
+ element.style.left = `${node.x}px`;
3582
+ element.style.top = `${node.y}px`;
3583
+ const id = node.id ?? node.step?.id ?? node.invocation?.block ?? "";
3584
+ element.dataset.selected = editorview.selected.includes(id) ? "true" : "false";
3585
+ element.dataset.breakpoint = node.step?.breakpoint === true ? "true" : "false";
3586
+ element.dataset.invocation = node.invocation !== void 0 ? "true" : "false";
3587
+ const kind = document.createElement("p");
3588
+ kind.className = "nodekind";
3589
+ kind.textContent = node.step !== void 0 ? node.step.kind : `block ${node.invocation?.block ?? ""}`;
3590
+ element.append(kind);
3591
+ const label = document.createElement("p");
3592
+ label.textContent = node.step !== void 0 ? node.step.label : node.invocation?.label ?? "";
3593
+ element.append(label);
3594
+ if (node.invocation !== void 0) {
3595
+ const nested = model.blocks.find((block) => block.name === node.invocation?.block);
3596
+ for (const entry of nested?.steps ?? []) {
3597
+ const child = document.createElement("p");
3598
+ child.textContent = entry && "kind" in entry ? `\xB7 ${entry.label} (${entry.kind})` : `\xB7 block ${entry.block}`;
3599
+ element.append(child);
3600
+ }
3601
+ }
3602
+ const sockets = document.createElement("p");
3603
+ const targets = node.invocation !== void 0 ? [id, ...(model.blocks.find((block) => block.name === node.invocation?.block)?.steps ?? []).flatMap((entry) => "id" in entry ? [entry.id] : [])] : [id];
3604
+ for (const edge of model.edges.filter((candidate) => targets.includes(candidate.to))) {
3605
+ const socket = document.createElement("span");
3606
+ socket.className = "socket in";
3607
+ socket.textContent = `${edge.variable} (${edge.kind})`;
3608
+ sockets.append(socket, " ");
3609
+ }
3610
+ for (const edge of model.edges.filter((candidate) => candidate.from === id)) {
3611
+ const socket = document.createElement("span");
3612
+ socket.className = "socket out";
3613
+ socket.textContent = `${edge.variable} \u2192`;
3614
+ sockets.append(socket, " ");
3615
+ }
3616
+ if (node.invocation?.params !== void 0 && node.invocation.params.length > 0) {
3617
+ for (const param of node.invocation.params) {
3618
+ const socket = document.createElement("span");
3619
+ socket.className = "socket";
3620
+ socket.textContent = `${param.name}: ${param.kind}`;
3621
+ sockets.append(socket, " ");
3622
+ }
3623
+ }
3624
+ element.append(sockets);
3625
+ element.addEventListener("click", () => {
3626
+ editorview.selected = [id];
3627
+ editorview.inspector = id;
3628
+ void refresh();
3629
+ });
3630
+ if (node.step !== void 0) {
3631
+ element.addEventListener("pointerdown", (event) => {
3632
+ if (event.button !== 0) return;
3633
+ const startx = event.clientX;
3634
+ const starty = event.clientY;
3635
+ const originx = node.x;
3636
+ const originy = node.y;
3637
+ element.setPointerCapture(event.pointerId);
3638
+ const move = (moveevent) => {
3639
+ element.style.left = `${originx + moveevent.clientX - startx}px`;
3640
+ element.style.top = `${originy + moveevent.clientY - starty}px`;
3641
+ };
3642
+ const drop = (upevent) => {
3643
+ element.removeEventListener("pointermove", move);
3644
+ element.removeEventListener("pointerup", drop);
3645
+ void (async () => {
3646
+ if (editorview.model === void 0) return;
3647
+ try {
3648
+ editorview.model = snapnode(editorview.model, id, originx + upevent.clientX - startx, originy + upevent.clientY - starty);
3649
+ status(`Snapped ${id} onto the block grid; drop it near a block column to attach.`);
3650
+ } catch (error) {
3651
+ status(error instanceof Error ? error.message : String(error), true);
3652
+ }
3653
+ await refresh();
3654
+ })();
3655
+ };
3656
+ element.addEventListener("pointermove", move);
3657
+ element.addEventListener("pointerup", drop);
3658
+ });
3659
+ }
3660
+ return element;
3661
+ };
3662
+ const canvascard = document.createElement("div");
3663
+ canvascard.className = "sessionrow";
3664
+ const toolbar = document.createElement("div");
3665
+ toolbar.className = "actions";
3666
+ toolbar.append(button("Undo", async () => {
3667
+ if (editorview.model === void 0) return;
3668
+ editorview.model = undoedit(editorview.model);
3669
+ status("Canvas edit undone; the redo stack keeps it.");
3670
+ await refresh();
3671
+ }, (model.undo ?? []).length === 0));
3672
+ toolbar.append(" ", button("Redo", async () => {
3673
+ if (editorview.model === void 0) return;
3674
+ editorview.model = redoedit(editorview.model);
3675
+ status("Canvas edit redone.");
3676
+ await refresh();
3677
+ }, (model.redo ?? []).length === 0));
3678
+ toolbar.append(" ", button("Toggle breakpoint", async () => {
3679
+ if (editorview.model === void 0 || editorview.inspector === "") {
3680
+ status("Select a step node first.", true);
3681
+ return;
3682
+ }
3683
+ editorview.model = markbreakpoint(editorview.model, editorview.inspector);
3684
+ status(`Breakpoint toggled on ${editorview.inspector}; a debug run pauses before it.`);
3685
+ await refresh();
3686
+ }));
3687
+ toolbar.append(" ", button("Remove selected", async () => {
3688
+ if (editorview.model === void 0 || editorview.selected.length === 0) {
3689
+ status("Select a node first.", true);
3690
+ return;
3691
+ }
3692
+ try {
3693
+ for (const id of editorview.selected) editorview.model = removenode(editorview.model, id);
3694
+ editorview.selected = [];
3695
+ editorview.inspector = "";
3696
+ status("Node removed with its edges; undo brings it back.");
3697
+ } catch (error) {
3698
+ status(error instanceof Error ? error.message : String(error), true);
3699
+ }
3700
+ await refresh();
3701
+ }));
3702
+ toolbar.append(" ", button("Move up", async () => {
3703
+ if (editorview.model === void 0 || editorview.inspector === "") return;
3704
+ const index = editorview.model.nodes.findIndex((node) => (node.id ?? node.step?.id ?? node.invocation?.block ?? "") === editorview.inspector);
3705
+ try {
3706
+ if (index > 0) editorview.model = reordersteps(editorview.model, editorview.inspector, index - 1);
3707
+ } catch (error) {
3708
+ status(error instanceof Error ? error.message : String(error), true);
3709
+ }
3710
+ await refresh();
3711
+ }));
3712
+ toolbar.append(" ", button("Move down", async () => {
3713
+ if (editorview.model === void 0 || editorview.inspector === "") return;
3714
+ const index = editorview.model.nodes.findIndex((node) => (node.id ?? node.step?.id ?? node.invocation?.block ?? "") === editorview.inspector);
3715
+ try {
3716
+ if (index >= 0 && index < editorview.model.nodes.length - 1) editorview.model = reordersteps(editorview.model, editorview.inspector, index + 1);
3717
+ } catch (error) {
3718
+ status(error instanceof Error ? error.message : String(error), true);
3719
+ }
3720
+ await refresh();
3721
+ }));
3722
+ const grouprow = document.createElement("div");
3723
+ grouprow.className = "actions";
3724
+ const groupinput = document.createElement("input");
3725
+ groupinput.type = "text";
3726
+ groupinput.placeholder = "blockname";
3727
+ grouprow.append(groupinput, " ", button("Group selection into block", async () => {
3728
+ if (editorview.model === void 0 || editorview.selected.length === 0) {
3729
+ status("Select step nodes first.", true);
3730
+ return;
3731
+ }
3732
+ try {
3733
+ editorview.model = groupselect(editorview.model, editorview.selected, groupinput.value.trim());
3734
+ editorview.selected = [];
3735
+ status(`Grouped the selection into the block ${groupinput.value.trim()}.`);
3736
+ } catch (error) {
3737
+ status(error instanceof Error ? error.message : String(error), true);
3738
+ }
3739
+ await refresh();
3740
+ }));
3741
+ canvascard.append(toolbar, grouprow);
3742
+ const canvas = document.createElement("div");
3743
+ canvas.className = "editorcanvas";
3744
+ const layer = document.createElement("div");
3745
+ layer.style.position = "absolute";
3746
+ layer.style.transformOrigin = "0 0";
3747
+ layer.style.left = "0";
3748
+ layer.style.top = "0";
3749
+ layer.style.width = `${model.layout.width}px`;
3750
+ layer.style.height = `${model.layout.height}px`;
3751
+ const zoom = model.layout.zoom > 0 ? model.layout.zoom : 1;
3752
+ layer.style.transform = `translate(${-Math.max(0, model.layout.viewportx)}px, ${-Math.max(0, model.layout.viewporty)}px) scale(${zoom})`;
3753
+ for (const block of model.blocks) {
3754
+ const members = model.nodes.filter((node) => node.invocation?.block === block.name);
3755
+ if (members.length === 0) continue;
3756
+ const container = document.createElement("div");
3757
+ container.className = "editorblock";
3758
+ const left = Math.min(...members.map((node) => node.x)) - 14;
3759
+ const top = Math.min(...members.map((node) => node.y)) - 14;
3760
+ container.style.left = `${left}px`;
3761
+ container.style.top = `${top}px`;
3762
+ container.style.width = `${Math.max(...members.map((node) => node.x)) - left + 234}px`;
3763
+ container.style.height = `${Math.max(...members.map((node) => node.y)) - top + 110}px`;
3764
+ const name = document.createElement("span");
3765
+ name.textContent = block.name;
3766
+ container.append(name);
3767
+ layer.append(container);
3768
+ }
3769
+ for (const node of model.nodes) layer.append(nodeelement(node));
3770
+ canvas.append(layer);
3771
+ canvascard.append(canvas);
3772
+ const minimap = document.createElement("div");
3773
+ minimap.className = "editorminimap";
3774
+ const projection = renderminimap(model);
3775
+ for (const dot of projection.nodes) {
3776
+ const point = document.createElement("span");
3777
+ point.className = "dot";
3778
+ point.style.left = `${Math.min(dot.x, model.minimap.width - 5)}px`;
3779
+ point.style.top = `${Math.min(dot.y, model.minimap.height - 5)}px`;
3780
+ minimap.append(point);
3781
+ }
3782
+ const rect = document.createElement("span");
3783
+ rect.className = "viewportrect";
3784
+ rect.style.left = `${Math.max(0, model.minimap.viewport.x)}px`;
3785
+ rect.style.top = `${Math.max(0, model.minimap.viewport.y)}px`;
3786
+ rect.style.width = `${Math.max(8, model.minimap.viewport.width)}px`;
3787
+ rect.style.height = `${Math.max(6, model.minimap.viewport.height)}px`;
3788
+ minimap.append(rect);
3789
+ minimap.addEventListener("click", (event) => {
3790
+ void (async () => {
3791
+ if (editorview.model === void 0) return;
3792
+ const bounds = minimap.getBoundingClientRect();
3793
+ try {
3794
+ editorview.model = minimapfocus(editorview.model, event.clientX - bounds.left, event.clientY - bounds.top);
3795
+ status("Canvas jumped to the mini map region.");
3796
+ } catch (error) {
3797
+ status(error instanceof Error ? error.message : String(error), true);
3798
+ }
3799
+ await refresh();
3800
+ })();
3801
+ });
3802
+ canvascard.append(minimap);
3803
+ const zoomrow = document.createElement("div");
3804
+ zoomrow.className = "actions";
3805
+ const zoominput = document.createElement("input");
3806
+ zoominput.type = "number";
3807
+ zoominput.min = "0.1";
3808
+ zoominput.step = "0.1";
3809
+ zoominput.value = String(zoom);
3810
+ zoomrow.append(zoominput, " ", button("Apply zoom", async () => {
3811
+ if (editorview.model === void 0) return;
3812
+ try {
3813
+ const applied = zoomcanvas(editorview.model, Number(zoominput.value));
3814
+ editorview.model = applied.model;
3815
+ status(`Canvas zoom ${Number(zoominput.value)} with label scale ${applied.labelscale.toFixed(2)} so every step label stays readable.`);
3816
+ } catch (error) {
3817
+ status(error instanceof Error ? error.message : String(error), true);
3818
+ }
3819
+ await refresh();
3820
+ }));
3821
+ const searchinput = document.createElement("input");
3822
+ searchinput.type = "text";
3823
+ searchinput.placeholder = "search steps by label, kind or variable";
3824
+ searchinput.value = editorview.stepsearch;
3825
+ searchinput.addEventListener("input", () => {
3826
+ editorview.stepsearch = searchinput.value;
3827
+ });
3828
+ zoomrow.append(searchinput, " ", button("Search steps", async () => {
3829
+ await refresh();
3830
+ }));
3831
+ canvascard.append(zoomrow);
3832
+ const results = searchsteps(model, editorview.stepsearch);
3833
+ if (results.length > 0) {
3834
+ const list = document.createElement("ul");
3835
+ for (const result of results) {
3836
+ const line = document.createElement("li");
3837
+ line.textContent = `${result.label} (${result.kind}) matched ${result.matched.join(", ")}`;
3838
+ list.append(line);
3839
+ }
3840
+ canvascard.append(list);
3841
+ }
3842
+ workfloweditorroot.append(canvascard);
3843
+ const inspector = model.nodes.find((node) => (node.id ?? node.step?.id ?? node.invocation?.block ?? "") === editorview.inspector);
3844
+ if (inspector !== void 0) {
3845
+ const card = document.createElement("div");
3846
+ card.className = "sessionrow";
3847
+ const headline = document.createElement("p");
3848
+ headline.textContent = inspector.step !== void 0 ? `Step inspector of ${inspector.step.id}` : `Invocation inspector of block ${inspector.invocation?.block ?? ""}`;
3849
+ card.append(headline);
3850
+ if (inspector.step !== void 0) {
3851
+ const inspectedstep = inspector.step;
3852
+ const grid = document.createElement("div");
3853
+ grid.className = "editorgrid";
3854
+ const labelinput = document.createElement("input");
3855
+ labelinput.type = "text";
3856
+ labelinput.value = inspectedstep.label;
3857
+ const targetinput = document.createElement("input");
3858
+ targetinput.type = "text";
3859
+ targetinput.placeholder = "css target";
3860
+ targetinput.value = inspectedstep.target ?? "";
3861
+ const valueinput = document.createElement("input");
3862
+ valueinput.type = "text";
3863
+ valueinput.placeholder = "value";
3864
+ valueinput.value = inspectedstep.value ?? "";
3865
+ const optionsinput = document.createElement("input");
3866
+ optionsinput.type = "text";
3867
+ optionsinput.placeholder = "json options";
3868
+ optionsinput.value = inspectedstep.options ?? "";
3869
+ for (const [labeltext, input] of [["label", labelinput], ["target", targetinput], ["value", valueinput], ["options json", optionsinput]]) {
3870
+ const fieldlabel = document.createElement("label");
3871
+ fieldlabel.textContent = labeltext;
3872
+ fieldlabel.append(input);
3873
+ grid.append(fieldlabel);
3874
+ }
3875
+ card.append(grid, button("Save step edits", async () => {
3876
+ if (editorview.model === void 0 || inspectedstep === void 0) return;
3877
+ const options2 = optionsinput.value.trim() === "" ? void 0 : optionsinput.value.trim();
3878
+ try {
3879
+ editorview.model = editstep(editorview.model, { ...inspectedstep, label: labelinput.value.trim(), ...targetinput.value.trim() !== "" ? { target: targetinput.value.trim() } : {}, ...valueinput.value.trim() !== "" ? { value: valueinput.value.trim() } : {}, ...options2 !== void 0 ? { options: options2 } : {} });
3880
+ status(`Saved the edits of ${inspectedstep.id}; undo covers them.`);
3881
+ } catch (error) {
3882
+ status(error instanceof Error ? error.message : String(error), true);
3883
+ }
3884
+ await refresh();
3885
+ }));
3886
+ const bindings = document.createElement("details");
3887
+ bindings.className = "sessiongroup";
3888
+ const bindingssummary = document.createElement("summary");
3889
+ bindingssummary.textContent = `Bindings and nested params (${model.edges.filter((edge) => edge.to === inspectedstep.id || edge.from === inspectedstep.id).length} edges)`;
3890
+ bindings.append(bindingssummary);
3891
+ for (const edge of model.edges.filter((candidate) => candidate.to === inspectedstep?.id)) {
3892
+ const line = document.createElement("p");
3893
+ line.textContent = `${edge.variable} (${edge.kind}) from ${edge.from}${edge.path !== void 0 ? ` path ${edge.path}` : ""}`;
3894
+ line.append(" ", button("Remove binding", async () => {
3895
+ if (editorview.model === void 0) return;
3896
+ try {
3897
+ editorview.model = removeedge(editorview.model, edge.from, edge.to, edge.variable);
3898
+ status(`Removed the binding ${edge.variable}.`);
3899
+ } catch (error) {
3900
+ status(error instanceof Error ? error.message : String(error), true);
3901
+ }
3902
+ await refresh();
3903
+ }));
3904
+ bindings.append(line);
3905
+ }
3906
+ const source = document.createElement("select");
3907
+ for (const node of model.nodes) {
3908
+ if (node.step === void 0 || node.step.id === inspectedstep.id) continue;
3909
+ const option = document.createElement("option");
3910
+ option.value = node.step.id;
3911
+ option.textContent = `${node.step.id} (${node.step.kind})`;
3912
+ source.append(option);
3913
+ }
3914
+ const variableinput = document.createElement("input");
3915
+ variableinput.type = "text";
3916
+ variableinput.placeholder = "variable";
3917
+ const kindselect = document.createElement("select");
3918
+ for (const kind of ["string", "number", "boolean", "list", "element"]) {
3919
+ const option = document.createElement("option");
3920
+ option.value = kind;
3921
+ option.textContent = kind;
3922
+ kindselect.append(option);
3923
+ }
3924
+ const pathinput = document.createElement("input");
3925
+ pathinput.type = "text";
3926
+ pathinput.placeholder = "path into outcome details";
3927
+ bindings.append(source, " ", variableinput, " ", kindselect, " ", pathinput, " ", button("Bind variable", async () => {
3928
+ if (editorview.model === void 0) return;
3929
+ try {
3930
+ editorview.model = addedge(editorview.model, { from: source.value, to: inspectedstep?.id ?? "", variable: variableinput.value.trim(), kind: kindselect.value, ...pathinput.value.trim() !== "" ? { path: pathinput.value.trim() } : {} });
3931
+ status(`Bound ${variableinput.value.trim()} from ${source.value}.`);
3932
+ } catch (error) {
3933
+ status(error instanceof Error ? error.message : String(error), true);
3934
+ }
3935
+ await refresh();
3936
+ }));
3937
+ card.append(bindings);
3938
+ }
3939
+ if (inspector.invocation !== void 0) {
3940
+ const paramgrid = document.createElement("div");
3941
+ paramgrid.className = "editorgrid";
3942
+ const paramname = document.createElement("input");
3943
+ paramname.type = "text";
3944
+ paramname.placeholder = "param name";
3945
+ const paramkind = document.createElement("select");
3946
+ for (const kind of ["string", "number", "boolean", "list", "element"]) {
3947
+ const option = document.createElement("option");
3948
+ option.value = kind;
3949
+ option.textContent = kind;
3950
+ paramkind.append(option);
3951
+ }
3952
+ const paramdefault = document.createElement("input");
3953
+ paramdefault.type = "text";
3954
+ paramdefault.placeholder = "default value";
3955
+ paramgrid.append(paramname, paramkind, paramdefault);
3956
+ card.append(paramgrid, button("Bind nested param", async () => {
3957
+ if (editorview.model === void 0) return;
3958
+ try {
3959
+ const parseddefault = paramdefault.value.trim() === "" ? void 0 : paramkind.value === "number" ? Number(paramdefault.value) : paramkind.value === "boolean" ? paramdefault.value === "true" : paramkind.value === "list" ? paramdefault.value.split(",").map((part) => part.trim()) : paramdefault.value;
3960
+ editorview.model = bindparam(editorview.model, inspector.invocation?.block ?? "", { name: paramname.value.trim(), kind: paramkind.value, ...parseddefault !== void 0 ? { default: parseddefault } : {} });
3961
+ status(`Bound the nested param ${paramname.value.trim()} into ${inspector.invocation?.block ?? ""}.`);
3962
+ } catch (error) {
3963
+ status(error instanceof Error ? error.message : String(error), true);
3964
+ }
3965
+ await refresh();
3966
+ }));
3967
+ }
3968
+ workfloweditorroot.append(card);
3969
+ }
3970
+ const saverow = document.createElement("div");
3971
+ saverow.className = "sessionrow";
3972
+ const nameinput = document.createElement("input");
3973
+ nameinput.type = "text";
3974
+ nameinput.value = model.name;
3975
+ const originsinput = document.createElement("input");
3976
+ originsinput.type = "text";
3977
+ originsinput.value = model.origins.join(", ");
3978
+ const noteinput2 = document.createElement("input");
3979
+ noteinput2.type = "text";
3980
+ noteinput2.placeholder = "change note for the version timeline";
3981
+ const versioninput = document.createElement("input");
3982
+ versioninput.type = "number";
3983
+ versioninput.min = "1";
3984
+ versioninput.value = String(model.version + 1);
3985
+ saverow.append(nameinput, " ", originsinput, " ", versioninput, " ", noteinput2, " ", button("Save canvas as new version", async () => {
3986
+ if (editorview.model === void 0) return;
3987
+ editorview.model = { ...editorview.model, name: nameinput.value.trim(), origins: originsinput.value.split(",").map((origin) => origin.trim()).filter((origin) => origin !== ""), version: Number(versioninput.value) };
3988
+ try {
3989
+ const saved = await request({ kind: "editorsave", model: editorview.model, note: noteinput2.value.trim() });
3990
+ status(`Saved ${saved.workflowid} as version ${saved.version}: ${saved.steps} expanded steps graded ${saved.risk} through the full grammar.`);
3991
+ } catch (error) {
3992
+ status(error instanceof Error ? error.message : String(error), true);
3993
+ }
3994
+ await refresh();
3995
+ }));
3996
+ workfloweditorroot.append(saverow);
3997
+ } else {
3998
+ const empty = document.createElement("p");
3999
+ empty.textContent = "No canvas open; open a composed workflow above or import a workflow file below.";
4000
+ workfloweditorroot.append(empty);
4001
+ }
4002
+ const palettecard = document.createElement("details");
4003
+ palettecard.className = "sessiongroup";
4004
+ const palettesummary = document.createElement("summary");
4005
+ palettesummary.textContent = "Block palette and step library";
4006
+ palettecard.append(palettesummary);
4007
+ const paletteactions = document.createElement("div");
4008
+ paletteactions.className = "actions";
4009
+ const paletteinput = document.createElement("input");
4010
+ paletteinput.type = "text";
4011
+ paletteinput.placeholder = "search the palette by block or category";
4012
+ paletteinput.value = editorview.palettesearch;
4013
+ paletteinput.addEventListener("input", () => {
4014
+ editorview.palettesearch = paletteinput.value;
4015
+ });
4016
+ const libraryinput = document.createElement("input");
4017
+ libraryinput.type = "text";
4018
+ libraryinput.placeholder = "search the step library by kind or category";
4019
+ libraryinput.value = editorview.librarysearch;
4020
+ libraryinput.addEventListener("input", () => {
4021
+ editorview.librarysearch = libraryinput.value;
4022
+ });
4023
+ paletteactions.append(paletteinput, " ", libraryinput, " ", button("Load palette and library", async () => {
4024
+ const loaded = await request({ kind: "steplibrarystore" });
4025
+ editorview.palette = loaded.palette;
4026
+ editorview.library = loaded.library;
4027
+ status(`Loaded ${loaded.palette.length} palette blocks and ${loaded.library.length} library kinds.`);
4028
+ await refresh();
4029
+ }));
4030
+ palettecard.append(paletteactions);
4031
+ const palettebody = document.createElement("div");
4032
+ palettebody.className = "editorpalette";
4033
+ if (editorview.palette === void 0) {
4034
+ const hint = document.createElement("p");
4035
+ hint.textContent = "Load the palette to browse the curated drop blocks and every reviewed action kind grouped by category.";
4036
+ palettebody.append(hint);
4037
+ } else {
4038
+ for (const category of palettecategories) {
4039
+ const blocks = editorview.palette.filter((node) => node.category === category && `${node.label} ${node.kind} ${node.category}`.toLowerCase().includes(editorview.palettesearch.toLowerCase()));
4040
+ if (blocks.length === 0) continue;
4041
+ const head = document.createElement("p");
4042
+ head.className = "palettecategory";
4043
+ head.textContent = category;
4044
+ palettebody.append(head);
4045
+ for (const block of blocks) {
4046
+ palettebody.append(button(block.label, async () => {
4047
+ if (editorview.model === void 0) {
4048
+ status("Open a canvas first.", true);
4049
+ return;
4050
+ }
4051
+ try {
4052
+ editorview.model = addnode(editorview.model, { id: block.kind, kind: block.kind, label: block.label });
4053
+ status(`Dropped ${block.label} onto the canvas; the step inspector edits its target and options.`);
4054
+ } catch (error) {
4055
+ status(error instanceof Error ? error.message : String(error), true);
4056
+ }
4057
+ await refresh();
4058
+ }), " ");
4059
+ }
4060
+ }
4061
+ }
4062
+ if (editorview.library !== void 0) {
4063
+ for (const category of palettecategories) {
4064
+ const kinds = editorview.library.filter((entry) => entry.category === category && `${entry.kind} ${entry.category}`.toLowerCase().includes(editorview.librarysearch.toLowerCase()));
4065
+ if (kinds.length === 0) continue;
4066
+ const head = document.createElement("p");
4067
+ head.className = "palettecategory";
4068
+ head.textContent = `${category} library`;
4069
+ palettebody.append(head);
4070
+ const list = document.createElement("ul");
4071
+ for (const entry of kinds) {
4072
+ const line = document.createElement("li");
4073
+ line.textContent = `${entry.kind}${entry.optionschema.length > 0 ? ` \xB7 options: ${entry.optionschema.map((option) => `${option.name} ${option.kind}${option.required === true ? " (required)" : ""}`).join(", ")}` : ""}`;
4074
+ list.append(line);
4075
+ }
4076
+ palettebody.append(list);
4077
+ }
4078
+ }
4079
+ palettecard.append(palettebody);
4080
+ workfloweditorroot.append(palettecard);
4081
+ if (model !== void 0 && editor !== void 0) {
4082
+ const versioncard = document.createElement("details");
4083
+ versioncard.className = "sessiongroup";
4084
+ const versionopen = editorview.diff !== void 0;
4085
+ if (versionopen) versioncard.open = true;
4086
+ const versionsummary = document.createElement("summary");
4087
+ versionsummary.textContent = `Version timeline (${editor.versions.filter((entry) => entry.workflowid === editorview.workflowid).length} versions of this workflow)`;
4088
+ versioncard.append(versionsummary);
4089
+ for (const version of editor.versions.filter((entry) => entry.workflowid === editorview.workflowid)) {
4090
+ const line = document.createElement("p");
4091
+ line.className = "diffrow";
4092
+ line.textContent = `v${version.version} \xB7 ${new Date(version.createdat).toISOString()} \xB7 ${version.steps} steps \xB7 ${version.risk ?? "ungraded"}${version.rollback === true ? " \xB7 rollback" : ""} \xB7 ${version.note}`;
4093
+ line.append(" ", button("Roll back here", async () => {
4094
+ try {
4095
+ const rolled = await request({ kind: "rollbackversion", workflowid: editorview.workflowid, version: version.version });
4096
+ status(`Rolled back to v${version.version}; stored as v${rolled.version} and ${rolled.reviewstate} until the rollback review approves it.`);
4097
+ } catch (error) {
4098
+ status(error instanceof Error ? error.message : String(error), true);
4099
+ }
4100
+ await refresh();
4101
+ }));
4102
+ versioncard.append(line);
4103
+ }
4104
+ const diffrow = document.createElement("div");
4105
+ diffrow.className = "actions";
4106
+ const frominput = document.createElement("input");
4107
+ frominput.type = "number";
4108
+ frominput.min = "1";
4109
+ frominput.placeholder = "from";
4110
+ const toinput = document.createElement("input");
4111
+ toinput.type = "number";
4112
+ toinput.min = "1";
4113
+ toinput.placeholder = "to";
4114
+ diffrow.append(frominput, " ", toinput, " ", button("Diff versions", async () => {
4115
+ try {
4116
+ const diff = await request({ kind: "diffversions", workflowid: editorview.workflowid, from: Number(frominput.value), to: Number(toinput.value) });
4117
+ editorview.diff = diff;
4118
+ status(`Diffed v${diff.from} into v${diff.to}: ${diff.added.length} added, ${diff.removed.length} removed, ${diff.changed.length} changed.`);
4119
+ } catch (error) {
4120
+ status(error instanceof Error ? error.message : String(error), true);
4121
+ }
4122
+ await refresh();
4123
+ }));
4124
+ versioncard.append(diffrow);
4125
+ if (editorview.diff !== void 0) {
4126
+ const diff = editorview.diff;
4127
+ const card = document.createElement("div");
4128
+ card.className = "sessionrow";
4129
+ const headline = document.createElement("p");
4130
+ headline.textContent = `Version diff v${diff.from} \u2192 v${diff.to}`;
4131
+ card.append(headline);
4132
+ for (const added of diff.added) {
4133
+ const line = document.createElement("p");
4134
+ line.className = "diffrow";
4135
+ line.dataset.class = "added";
4136
+ line.textContent = `+ ${added.stepid} (${added.kind}) ${added.label}`;
4137
+ card.append(line);
4138
+ }
4139
+ for (const removed of diff.removed) {
4140
+ const line = document.createElement("p");
4141
+ line.className = "diffrow";
4142
+ line.dataset.class = "removed";
4143
+ line.textContent = `- ${removed.stepid} (${removed.kind}) ${removed.label}`;
4144
+ card.append(line);
4145
+ }
4146
+ for (const changed of diff.changed) {
4147
+ const line = document.createElement("p");
4148
+ line.className = "diffrow";
4149
+ line.dataset.class = "changed";
4150
+ line.textContent = `~ ${changed.stepid} (${changed.kind}) ${changed.label}: ${changed.changes.join(", ")}`;
4151
+ card.append(line);
4152
+ }
4153
+ card.append(button("Close diff", async () => {
4154
+ editorview.diff = void 0;
4155
+ await refresh();
4156
+ }));
4157
+ versioncard.append(card);
4158
+ }
4159
+ workfloweditorroot.append(versioncard);
4160
+ const backgroundrow = document.createElement("div");
4161
+ backgroundrow.className = "actions";
4162
+ const backgroundcheck = document.createElement("input");
4163
+ backgroundcheck.type = "checkbox";
4164
+ backgroundcheck.checked = editor.backgroundruns[editorview.workflowid] === true;
4165
+ const backgroundlabel = document.createElement("label");
4166
+ backgroundlabel.append(backgroundcheck, " keep runs of this workflow executing with the panel closed (checkpoints restore on every worker wake)");
4167
+ backgroundrow.append(backgroundlabel);
4168
+ backgroundrow.append(button("Apply background toggle", async () => {
4169
+ const result = await request({ kind: "setbackgroundrun", workflowid: editorview.workflowid, enabled: backgroundcheck.checked });
4170
+ status(result.enabled ? "Background runs stay alive with the panel closed; every step checkpoints." : "Background runs off; a closed panel pauses the next run at its last checkpoint.");
4171
+ await refresh();
4172
+ }));
4173
+ workfloweditorroot.append(backgroundrow);
4174
+ }
4175
+ if (context.workflow !== void 0) {
4176
+ const logcard = document.createElement("details");
4177
+ logcard.className = "sessiongroup";
4178
+ const logsummary = document.createElement("summary");
4179
+ logsummary.textContent = `Run log and variable inspector of the newest run (${context.workflow.log.length} entries)`;
4180
+ logcard.append(logsummary);
4181
+ const breakpoints = /* @__PURE__ */ new Set([...editor?.breakpoints ?? [], ...context.workflow.workflows.find((record) => record.id === editorview.workflowid)?.steps.flatMap((step) => step.breakpoint === true ? [step.id] : []) ?? []]);
4182
+ for (const entry of context.workflow.log) {
4183
+ const line = document.createElement("p");
4184
+ line.textContent = `${entry.state} \xB7 ${entry.label}${breakpoints.has(entry.stepid) ? " \xB7 breakpoint" : ""} \xB7 ${entry.duration} ms \xB7 ${entry.summary}`;
4185
+ line.dataset.class = entry.state === "failed" || entry.state === "refused" ? "changed" : "added";
4186
+ logcard.append(line);
4187
+ }
4188
+ if (context.workflow.log.length === 0) {
4189
+ const empty = document.createElement("p");
4190
+ empty.textContent = "No run log entry yet; run the workflow from the workflows view.";
4191
+ logcard.append(empty);
4192
+ }
4193
+ for (const scope of context.workflow.scopes) {
4194
+ const line = document.createElement("p");
4195
+ line.textContent = `Scope ${scope.name}: ${scope.variables.length === 0 ? "no variable" : scope.variables.map((variable) => `${variable.name} = ${Array.isArray(variable.value) ? `[${variable.value.join(", ")}]` : String(variable.value)} (${variable.kind})`).join(" \xB7 ")}`;
4196
+ logcard.append(line);
4197
+ }
4198
+ workfloweditorroot.append(logcard);
4199
+ }
4200
+ const historycard = document.createElement("details");
4201
+ historycard.className = "sessiongroup";
4202
+ if (editorview.history !== void 0) historycard.open = true;
4203
+ const historysummary = document.createElement("summary");
4204
+ historysummary.textContent = `Run history (${editorview.history?.length ?? editor?.history.length ?? 0} entries)`;
4205
+ historycard.append(historysummary);
4206
+ const historyfilters = document.createElement("div");
4207
+ historyfilters.className = "actions";
4208
+ const workflowselect = document.createElement("select");
4209
+ const anyoption = document.createElement("option");
4210
+ anyoption.value = "";
4211
+ anyoption.textContent = "every workflow";
4212
+ workflowselect.append(anyoption);
4213
+ for (const record of workflows) {
4214
+ const option = document.createElement("option");
4215
+ option.value = record.id;
4216
+ option.textContent = record.name;
4217
+ workflowselect.append(option);
4218
+ }
4219
+ workflowselect.value = editorview.historyfilter.workflowid;
4220
+ const outcomeinput = document.createElement("input");
4221
+ outcomeinput.type = "text";
4222
+ outcomeinput.placeholder = "outcome filter";
4223
+ outcomeinput.value = editorview.historyfilter.outcome;
4224
+ historyfilters.append(workflowselect, " ", outcomeinput, " ", button("Apply history filters", async () => {
4225
+ editorview.historyfilter = { workflowid: workflowselect.value, outcome: outcomeinput.value.trim() };
4226
+ try {
4227
+ const report = await request({ kind: "runhistory", ...workflowselect.value !== "" ? { workflowid: workflowselect.value } : {}, ...outcomeinput.value.trim() !== "" ? { outcome: outcomeinput.value.trim() } : {} });
4228
+ editorview.history = report.entries;
4229
+ status(`Run history: ${report.entries.length} entries match the filters.`);
4230
+ } catch (error) {
4231
+ status(error instanceof Error ? error.message : String(error), true);
4232
+ }
4233
+ await refresh();
4234
+ }));
4235
+ historyfilters.append(button("Keep every entry", async () => {
4236
+ await request({ kind: "setrunhistoryretention" });
4237
+ status("Run history keeps every entry; no code ceiling applies.");
4238
+ await refresh();
4239
+ }));
4240
+ historycard.append(historyfilters);
4241
+ const historyentries = editorview.history ?? editor?.history ?? [];
4242
+ for (const entry of historyentries.slice(0, 40)) {
4243
+ const line = document.createElement("div");
4244
+ line.className = "historyrow";
4245
+ line.dataset.outcome = entry.outcome;
4246
+ const detail = document.createElement("p");
4247
+ detail.textContent = `${entry.outcome} \xB7 ${entry.steps}/${entry.total} steps \xB7 ${entry.duration} ms \xB7 ${entry.cause}${entry.dryrun === true ? " \xB7 dry run" : ""} \xB7 ${new Date(entry.startedat).toISOString()}`;
4248
+ line.append(detail);
4249
+ historycard.append(line);
4250
+ }
4251
+ workfloweditorroot.append(historycard);
4252
+ const filecard = document.createElement("details");
4253
+ filecard.className = "sessiongroup";
4254
+ const filesummary = document.createElement("summary");
4255
+ filesummary.textContent = "Import, export and template sharing";
4256
+ filecard.append(filesummary);
4257
+ const formatselect = document.createElement("select");
4258
+ for (const format of ["json", "yaml"]) {
4259
+ const option = document.createElement("option");
4260
+ option.value = format;
4261
+ option.textContent = format;
4262
+ formatselect.append(option);
4263
+ }
4264
+ const contentsinput = document.createElement("textarea");
4265
+ contentsinput.rows = 4;
4266
+ contentsinput.placeholder = "paste a workflow file to import";
4267
+ const filenameinput = document.createElement("input");
4268
+ filenameinput.type = "text";
4269
+ filenameinput.placeholder = "source filename";
4270
+ const importactions = document.createElement("div");
4271
+ importactions.className = "actions";
4272
+ importactions.append(contentsinput, " ", filenameinput, " ", formatselect, " ", button("Import workflow file", async () => {
4273
+ if (contentsinput.value.trim() === "") {
4274
+ status("Paste the workflow file contents first.", true);
4275
+ return;
4276
+ }
4277
+ try {
4278
+ const imported = await request({ kind: "importworkflow", contents: contentsinput.value, format: formatselect.value, ...filenameinput.value.trim() !== "" ? { filename: filenameinput.value.trim() } : {} });
4279
+ const review = await request({ kind: "workflowreview", workflowid: imported.workflowid });
4280
+ editorview.importreview = { importid: imported.importid, workflowid: imported.workflowid, name: imported.name, version: imported.version, risk: imported.risk, steps: review.steps };
4281
+ status(`Imported ${imported.name} v${imported.version} with ${imported.steps} steps and ${imported.templates} templates; review before activation.`);
4282
+ } catch (error) {
4283
+ status(error instanceof Error ? error.message : String(error), true);
4284
+ }
4285
+ await refresh();
4286
+ }));
4287
+ filecard.append(importactions);
4288
+ if (editorview.importreview !== void 0) {
4289
+ const review = document.createElement("div");
4290
+ review.className = "sessionrow";
4291
+ const headline = document.createElement("p");
4292
+ headline.textContent = `Import review of ${editorview.importreview.name} v${editorview.importreview.version} (${editorview.importreview.risk} for review): every expanded step shows before activation and nothing runs until approval.`;
4293
+ review.append(headline);
4294
+ const list = document.createElement("ol");
4295
+ for (const step of editorview.importreview.steps) {
4296
+ const line = document.createElement("li");
4297
+ line.textContent = `${step.label} (${step.kind}${step.block !== void 0 ? ` \xB7 block ${step.block}` : ""}${step.target !== void 0 ? ` \xB7 ${step.target}` : ""})`;
4298
+ list.append(line);
4299
+ }
4300
+ review.append(list);
4301
+ review.append(button("Approve import", async () => {
4302
+ try {
4303
+ const approved = await request({ kind: "approveimport", importid: editorview.importreview?.importid });
4304
+ status(`Import approved: ${approved.reviewstate}; the workflow runs behind the same gates.`);
4305
+ editorview.importreview = void 0;
4306
+ } catch (error) {
4307
+ status(error instanceof Error ? error.message : String(error), true);
4308
+ }
4309
+ await refresh();
4310
+ }), " ", button("Reject import", async () => {
4311
+ try {
4312
+ await request({ kind: "rejectimport", importid: editorview.importreview?.importid });
4313
+ status("Import rejected; the pending record left the library.");
4314
+ editorview.importreview = void 0;
4315
+ } catch (error) {
4316
+ status(error instanceof Error ? error.message : String(error), true);
4317
+ }
4318
+ await refresh();
4319
+ }));
4320
+ filecard.append(review);
4321
+ }
4322
+ for (const pending of editor?.imports ?? []) {
4323
+ const line = document.createElement("p");
4324
+ line.textContent = `Pending import ${pending.name} v${pending.version} (${pending.steps} steps, ${pending.risk})${pending.filename !== void 0 ? ` from ${pending.filename}` : ""}`;
4325
+ line.append(" ", button("Review steps", async () => {
4326
+ try {
4327
+ const steps = await request({ kind: "workflowreview", workflowid: pending.workflowid });
4328
+ editorview.importreview = { importid: pending.id, workflowid: pending.workflowid, name: pending.name, version: pending.version, risk: pending.risk, steps: steps.steps };
4329
+ status(`Import review of ${pending.name}: ${steps.steps.length} expanded steps.`);
4330
+ } catch (error) {
4331
+ status(error instanceof Error ? error.message : String(error), true);
4332
+ }
4333
+ await refresh();
4334
+ }));
4335
+ filecard.append(line);
4336
+ }
4337
+ const exportrow = document.createElement("div");
4338
+ exportrow.className = "actions";
4339
+ const noteinput = document.createElement("input");
4340
+ noteinput.type = "text";
4341
+ noteinput.placeholder = "change note inside the file";
4342
+ exportrow.append(noteinput, " ", button("Export open workflow", async () => {
4343
+ if (editorview.workflowid === "") {
4344
+ status("Open a workflow on the canvas first.", true);
4345
+ return;
4346
+ }
4347
+ try {
4348
+ const exported = await request({ kind: "exportworkflow", workflowid: editorview.workflowid, format: formatselect.value, ...noteinput.value.trim() !== "" ? { note: noteinput.value.trim() } : {} });
4349
+ savefile(exported.filename, exported.contents);
4350
+ status(`Exported ${exported.filename} (${exported.contents.length} characters, ${exported.format}); the export review held every secret back.`);
4351
+ } catch (error) {
4352
+ status(error instanceof Error ? error.message : String(error), true);
4353
+ }
4354
+ }), " ", button("Share with templates", async () => {
4355
+ if (editorview.workflowid === "") {
4356
+ status("Open a workflow on the canvas first.", true);
4357
+ return;
4358
+ }
4359
+ try {
4360
+ const shared = await request({ kind: "shareworkflow", workflowid: editorview.workflowid, format: formatselect.value, ...noteinput.value.trim() !== "" ? { note: noteinput.value.trim() } : {} });
4361
+ savefile(shared.filename, shared.contents);
4362
+ status(`Packed the share bundle ${shared.filename}; templates travel with the workflow.`);
4363
+ } catch (error) {
4364
+ status(error instanceof Error ? error.message : String(error), true);
4365
+ }
4366
+ }));
4367
+ filecard.append(exportrow);
4368
+ workfloweditorroot.append(filecard);
4369
+ const watchdogcard = document.createElement("details");
4370
+ watchdogcard.className = "sessiongroup";
4371
+ const watchdogsummary = document.createElement("summary");
4372
+ const watchdogconfig = editor?.watchdog.config;
4373
+ watchdogsummary.textContent = `Watchdog status (${editor?.watchdog.events.length ?? 0} events)`;
4374
+ watchdogcard.append(watchdogsummary);
4375
+ const watchdoggrid = document.createElement("div");
4376
+ watchdoggrid.className = "editorgrid";
4377
+ const enabledcheck = document.createElement("input");
4378
+ enabledcheck.type = "checkbox";
4379
+ enabledcheck.checked = watchdogconfig?.enabled === true;
4380
+ const thresholdinput = document.createElement("input");
4381
+ thresholdinput.type = "number";
4382
+ thresholdinput.min = "1";
4383
+ thresholdinput.placeholder = "stall threshold ms";
4384
+ thresholdinput.value = watchdogconfig !== void 0 ? String(watchdogconfig.stallthreshold) : "";
4385
+ const actionselect = document.createElement("select");
4386
+ for (const action of ["retry", "pause", "cancel"]) {
4387
+ const option = document.createElement("option");
4388
+ option.value = action;
4389
+ option.textContent = action;
4390
+ actionselect.append(option);
4391
+ }
4392
+ actionselect.value = watchdogconfig?.action ?? "pause";
4393
+ const zombieinput = document.createElement("input");
4394
+ zombieinput.type = "number";
4395
+ zombieinput.min = "1";
4396
+ zombieinput.placeholder = "zombie window ms";
4397
+ zombieinput.value = watchdogconfig?.zombiewindow !== void 0 ? String(watchdogconfig.zombiewindow) : "";
4398
+ for (const [labeltext, control] of [["enabled", enabledcheck], ["stall threshold ms", thresholdinput], ["recovery action", actionselect], ["zombie window ms", zombieinput]]) {
4399
+ const fieldlabel = document.createElement("label");
4400
+ fieldlabel.textContent = labeltext;
4401
+ fieldlabel.append(control);
4402
+ watchdoggrid.append(fieldlabel);
4403
+ }
4404
+ watchdogcard.append(watchdoggrid);
4405
+ const watchdogactions = document.createElement("div");
4406
+ watchdogactions.className = "actions";
4407
+ watchdogactions.append(button("Save watchdog config", async () => {
4408
+ try {
4409
+ await request({ kind: "setwatchdog", config: { enabled: enabledcheck.checked, stallthreshold: Number(thresholdinput.value), action: actionselect.value, ...zombieinput.value.trim() !== "" ? { zombiewindow: Number(zombieinput.value) } : {} } });
4410
+ status("Watchdog saved; thresholds stay user values with no code ceiling.");
4411
+ } catch (error) {
4412
+ status(error instanceof Error ? error.message : String(error), true);
4413
+ }
4414
+ await refresh();
4415
+ }), " ", button("Scan now", async () => {
4416
+ try {
4417
+ const scan = await request({ kind: "watchdogscan" });
4418
+ status(`Watchdog scan: ${scan.events.length} stalled or zombie run${scan.events.length === 1 ? "" : "s"} recovered.`);
4419
+ } catch (error) {
4420
+ status(error instanceof Error ? error.message : String(error), true);
4421
+ }
4422
+ await refresh();
4423
+ }));
4424
+ watchdogcard.append(watchdogactions);
4425
+ for (const event of (editor?.watchdog.events ?? []).slice(0, 15)) {
4426
+ const line = document.createElement("p");
4427
+ line.textContent = `${event.verdict} \xB7 ${event.action} \xB7 ${new Date(event.at).toISOString()} \xB7 ${event.outcome}`;
4428
+ watchdogcard.append(line);
4429
+ }
4430
+ workfloweditorroot.append(watchdogcard);
4431
+ const overridecard = document.createElement("details");
4432
+ overridecard.className = "sessiongroup";
4433
+ const overridesummary = document.createElement("summary");
4434
+ overridesummary.textContent = `Per site policy overrides (${editor?.overrides.length ?? 0})`;
4435
+ overridecard.append(overridesummary);
4436
+ const overridegrid = document.createElement("div");
4437
+ overridegrid.className = "editorgrid";
4438
+ const patterninput = document.createElement("input");
4439
+ patterninput.type = "text";
4440
+ patterninput.placeholder = "https://origin or https://*.origin";
4441
+ const knobinputs = [];
4442
+ for (const knob of ["loopbound", "stepms", "runms", "waitms", "delaybase"]) {
4443
+ const input = document.createElement("input");
4444
+ input.type = "number";
4445
+ input.min = "1";
4446
+ input.placeholder = knob;
4447
+ knobinputs.push([knob, input]);
4448
+ const fieldlabel = document.createElement("label");
4449
+ fieldlabel.textContent = knob;
4450
+ fieldlabel.append(input);
4451
+ overridegrid.append(fieldlabel);
4452
+ }
4453
+ const patternlabel = document.createElement("label");
4454
+ patternlabel.textContent = "origin pattern";
4455
+ patternlabel.append(patterninput);
4456
+ overridegrid.prepend(patternlabel);
4457
+ overridecard.append(overridegrid, button("Attach override", async () => {
4458
+ if (editorview.workflowid === "") {
4459
+ status("Open a workflow on the canvas first.", true);
4460
+ return;
4461
+ }
4462
+ const deltas = {};
4463
+ for (const [knob, input] of knobinputs) if (input.value.trim() !== "" && Number.isFinite(Number(input.value)) && Number(input.value) > 0) deltas[knob] = Number(input.value);
4464
+ try {
4465
+ await request({ kind: "setsiteoverride", workflowid: editorview.workflowid, pattern: patterninput.value.trim(), deltas });
4466
+ status(`Attached the override ${patterninput.value.trim()} with ${Object.keys(deltas).length} knob delta${Object.keys(deltas).length === 1 ? "" : "s"}.`);
4467
+ } catch (error) {
4468
+ status(error instanceof Error ? error.message : String(error), true);
4469
+ }
4470
+ await refresh();
4471
+ }));
4472
+ for (const override of editor?.overrides ?? []) {
4473
+ const line = document.createElement("p");
4474
+ line.textContent = `${override.pattern} of ${override.workflowid}: ${Object.entries(override.deltas).map(([knob, delta]) => `${knob} ${delta}`).join(", ") || "no delta"}`;
4475
+ line.append(" ", button("Remove override", async () => {
4476
+ try {
4477
+ await request({ kind: "removesiteoverride", id: override.id });
4478
+ status(`Removed the override ${override.pattern}.`);
4479
+ } catch (error) {
4480
+ status(error instanceof Error ? error.message : String(error), true);
4481
+ }
4482
+ await refresh();
4483
+ }));
4484
+ overridecard.append(line);
4485
+ }
4486
+ workfloweditorroot.append(overridecard);
4487
+ }
3205
4488
  function rendertriggers(context) {
3206
4489
  if (!triggersroot) return;
3207
4490
  triggersroot.replaceChildren();