@wenathlan/extension 1.1.51 → 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.
- package/README.md +7 -5
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1908 -200
- package/dist/index.js.map +4 -4
- package/dist/memory.d.ts +85 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +37 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +139 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/trigger.d.ts +151 -0
- package/dist/trigger.d.ts.map +1 -0
- package/dist/types.d.ts +289 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/workflow.d.ts +19 -3
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workfloweditor.d.ts +108 -0
- package/dist/workfloweditor.d.ts.map +1 -0
- package/extension/dist/background.js +2306 -20
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +1 -1
- package/extension/dist/pagebridge.js.map +2 -2
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +24 -1
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +1442 -1
- package/extension/dist/sidepanel.js.map +4 -4
- package/extension/dist/style.css +2 -0
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
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"]);
|
|
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,8 +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");
|
|
394
|
+
var triggersroot = document.querySelector("#triggers");
|
|
395
|
+
var triggerview = { manual: void 0, history: void 0 };
|
|
78
396
|
var sessionsview = { term: "", window: "all", diffselection: [], restorereview: void 0, importreview: void 0 };
|
|
79
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
|
+
};
|
|
80
413
|
var statusnode = document.querySelector("#status");
|
|
81
414
|
var progressnode = document.querySelector("#planprogress");
|
|
82
415
|
var capabilitiestext = document.querySelector("#capabilitiestext");
|
|
@@ -2539,6 +2872,8 @@ async function refresh() {
|
|
|
2539
2872
|
renderemulation(context);
|
|
2540
2873
|
rendersessions(context);
|
|
2541
2874
|
renderworkflows(context);
|
|
2875
|
+
renderworkfloweditor(context);
|
|
2876
|
+
rendertriggers(context);
|
|
2542
2877
|
renderconsolediff();
|
|
2543
2878
|
renderaudit(context.audit);
|
|
2544
2879
|
rendercapabilities(context.capabilities);
|
|
@@ -3199,4 +3534,1110 @@ function renderworkflows(context) {
|
|
|
3199
3534
|
}
|
|
3200
3535
|
}
|
|
3201
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
|
+
}
|
|
4488
|
+
function rendertriggers(context) {
|
|
4489
|
+
if (!triggersroot) return;
|
|
4490
|
+
triggersroot.replaceChildren();
|
|
4491
|
+
const rules = context.trigger?.rules ?? [];
|
|
4492
|
+
const queued = context.trigger?.queued ?? 0;
|
|
4493
|
+
const workflows = context.workflow?.workflows ?? [];
|
|
4494
|
+
const title = document.createElement("p");
|
|
4495
|
+
title.textContent = `${rules.length} armed rule${rules.length === 1 ? "" : "s"} across ${new Set(rules.map((rule) => rule.workflowid)).size} workflow${new Set(rules.map((rule) => rule.workflowid)).size === 1 ? "" : "s"} \xB7 ${queued} queued fire${queued === 1 ? "" : "s"}${context.session?.pausedat !== void 0 ? " held while the session is paused" : ""}.`;
|
|
4496
|
+
triggersroot.append(title);
|
|
4497
|
+
const byworkflow = /* @__PURE__ */ new Map();
|
|
4498
|
+
for (const rule of rules) {
|
|
4499
|
+
const group = byworkflow.get(rule.workflowid) ?? [];
|
|
4500
|
+
group.push(rule);
|
|
4501
|
+
byworkflow.set(rule.workflowid, group);
|
|
4502
|
+
}
|
|
4503
|
+
for (const [workflowid, group] of byworkflow) {
|
|
4504
|
+
const workflowname = group[0]?.workflowname ?? workflowid;
|
|
4505
|
+
const box = document.createElement("details");
|
|
4506
|
+
box.className = "sessiongroup";
|
|
4507
|
+
box.open = true;
|
|
4508
|
+
const summary = document.createElement("summary");
|
|
4509
|
+
summary.textContent = `${workflowname} \xB7 ${group.length} rule${group.length === 1 ? "" : "s"}`;
|
|
4510
|
+
box.append(summary);
|
|
4511
|
+
for (const rule of group) {
|
|
4512
|
+
const row = document.createElement("div");
|
|
4513
|
+
row.className = "sessionrow";
|
|
4514
|
+
const headline = document.createElement("p");
|
|
4515
|
+
const badge = document.createElement("span");
|
|
4516
|
+
badge.className = "sessionbadge";
|
|
4517
|
+
badge.dataset.restored = "false";
|
|
4518
|
+
badge.textContent = rule.enabled ? rule.paused === true ? "paused" : "enabled" : "disabled";
|
|
4519
|
+
const match = rule.summary.pattern !== void 0 ? String(rule.summary.pattern) : rule.summary.origins !== void 0 ? rule.summary.origins.join(", ") : rule.summary.cron !== void 0 ? `${String(rule.summary.cron)}${rule.summary.timezone !== void 0 ? ` (${String(rule.summary.timezone)})` : ""}` : rule.summary.period !== void 0 ? `every ${String(rule.summary.period)} ms${rule.summary.jitter !== void 0 ? ` \xB1 ${String(rule.summary.jitter)} ms` : ""}` : rule.summary.title !== void 0 ? String(rule.summary.title) : rule.summary.command !== void 0 ? String(rule.summary.command) : rule.summary.events !== void 0 ? rule.summary.events.join(", ") : rule.kind === "urllist" ? `${rule.summary.urls?.length ?? 0} urls` : rule.kind === "webhook" ? `webhook with ${String(rule.summary.fields ?? 0)} schema fields` : "toolbar button";
|
|
4520
|
+
headline.append(`${rule.label} \xB7 ${rule.kind} \xB7 ${match} \xB7 cooldown ${rule.cooldown} ms \xB7 ${rule.fires} fire${rule.fires === 1 ? "" : "s"}, ${rule.launches} launch${rule.launches === 1 ? "" : "es"}, ${rule.suppressions} suppressed${rule.nextfireat !== void 0 ? ` \xB7 next fire ${new Date(rule.nextfireat).toISOString()}` : ""}`, badge);
|
|
4521
|
+
row.append(headline);
|
|
4522
|
+
const actions = document.createElement("div");
|
|
4523
|
+
actions.className = "actions";
|
|
4524
|
+
actions.append(button(rule.enabled ? "Disable" : "Enable", async () => {
|
|
4525
|
+
await request({ kind: "toggletrigger", ruleid: rule.id, enabled: !rule.enabled });
|
|
4526
|
+
status(`The ${rule.kind} rule is now ${rule.enabled ? "disabled" : "enabled"}.`);
|
|
4527
|
+
await refresh();
|
|
4528
|
+
}));
|
|
4529
|
+
actions.append(" ", button("Fire history", async () => {
|
|
4530
|
+
const result = await request({ kind: "triggerhistory", ruleid: rule.id });
|
|
4531
|
+
triggerview.history = result.fires;
|
|
4532
|
+
status(`Fire history: ${result.fires.length} fire record${result.fires.length === 1 ? "" : "s"} of the ${rule.kind} rule.`);
|
|
4533
|
+
await refresh();
|
|
4534
|
+
}));
|
|
4535
|
+
actions.append(" ", button("Fire manually", async () => {
|
|
4536
|
+
const result = await request({ kind: "firetrigger", ruleid: rule.id });
|
|
4537
|
+
status(result.fired ? `The ${rule.kind} rule fired${result.queued === true ? " and queued for the busy run" : ""}.` : `The ${rule.kind} rule suppressed the fire: ${result.suppressed ?? "review gate"}.`);
|
|
4538
|
+
await refresh();
|
|
4539
|
+
}));
|
|
4540
|
+
if (rule.kind === "webhook") actions.append(" ", button("Rotate secret", async () => {
|
|
4541
|
+
const result = await request({ kind: "rotatetriggersecret", ruleid: rule.id });
|
|
4542
|
+
status(`The webhook secret rotated to ${result.secret}; it was shown once and never leaves the store.`);
|
|
4543
|
+
await refresh();
|
|
4544
|
+
}));
|
|
4545
|
+
if (workflows.length > 1) actions.append(" ", button("Duplicate to second workflow", async () => {
|
|
4546
|
+
const target = workflows.find((record) => record.id !== rule.workflowid);
|
|
4547
|
+
if (!target) {
|
|
4548
|
+
status("No second composed workflow exists to duplicate the rule to.", true);
|
|
4549
|
+
return;
|
|
4550
|
+
}
|
|
4551
|
+
await request({ kind: "duplicatetrigger", ruleid: rule.id, workflowid: target.id });
|
|
4552
|
+
status(`Duplicated the ${rule.kind} rule to ${target.name}.`);
|
|
4553
|
+
await refresh();
|
|
4554
|
+
}));
|
|
4555
|
+
row.append(actions);
|
|
4556
|
+
box.append(row);
|
|
4557
|
+
}
|
|
4558
|
+
const workflowactions = document.createElement("div");
|
|
4559
|
+
workflowactions.className = "actions";
|
|
4560
|
+
workflowactions.append(button("Create visit rule from current page", async () => {
|
|
4561
|
+
await request({ kind: "createvisitrule", workflowid });
|
|
4562
|
+
status(`Armed a visit rule of the current page origin for ${workflowname}.`);
|
|
4563
|
+
await refresh();
|
|
4564
|
+
}));
|
|
4565
|
+
workflowactions.append(" ", button("Manual run preview", async () => {
|
|
4566
|
+
const result = await request({ kind: "manualrun", workflowid });
|
|
4567
|
+
triggerview.manual = { ...result.manualrun, at: Date.now() };
|
|
4568
|
+
status(`Manual run preview: ${result.manualrun.preview.length} steps of ${workflowname}; nothing runs before the confirmation.`);
|
|
4569
|
+
await refresh();
|
|
4570
|
+
}));
|
|
4571
|
+
box.append(workflowactions);
|
|
4572
|
+
triggersroot.append(box);
|
|
4573
|
+
}
|
|
4574
|
+
if (rules.length === 0) {
|
|
4575
|
+
const empty = document.createElement("p");
|
|
4576
|
+
empty.textContent = "No armed trigger rule yet; arm a reviewed rule of any family or create a visit rule from the current page.";
|
|
4577
|
+
triggersroot.append(empty);
|
|
4578
|
+
}
|
|
4579
|
+
if (triggerview.history !== void 0) {
|
|
4580
|
+
const history = document.createElement("details");
|
|
4581
|
+
history.className = "sessiongroup";
|
|
4582
|
+
const summary = document.createElement("summary");
|
|
4583
|
+
summary.textContent = `Fire history (${triggerview.history.length} records)`;
|
|
4584
|
+
history.append(summary);
|
|
4585
|
+
for (const fire of triggerview.history.slice(0, 25)) {
|
|
4586
|
+
const line = document.createElement("p");
|
|
4587
|
+
line.textContent = `${new Date(fire.at).toISOString()} \xB7 ${fire.cause}${fire.url !== void 0 ? ` \xB7 ${fire.url}` : ""}${fire.title !== void 0 ? ` \xB7 ${fire.title}` : ""}`;
|
|
4588
|
+
history.append(line);
|
|
4589
|
+
}
|
|
4590
|
+
triggersroot.append(history);
|
|
4591
|
+
}
|
|
4592
|
+
if (triggerview.manual !== void 0) {
|
|
4593
|
+
const preview = document.createElement("div");
|
|
4594
|
+
preview.className = "sessionrow";
|
|
4595
|
+
const headline = document.createElement("p");
|
|
4596
|
+
headline.textContent = `Manual run step preview: ${triggerview.manual.preview.length} expanded step${triggerview.manual.preview.length === 1 ? "" : "s"}; approve or cancel before anything runs.`;
|
|
4597
|
+
preview.append(headline);
|
|
4598
|
+
for (const step of triggerview.manual.preview) {
|
|
4599
|
+
const line = document.createElement("p");
|
|
4600
|
+
line.textContent = `${step.stepid} \xB7 ${step.kind} \xB7 ${step.label}${step.block !== void 0 ? ` \xB7 block ${step.block}` : ""}${step.control !== void 0 ? ` \xB7 ${Object.entries(step.control).map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(", ") : String(value)}`).join(" \xB7 ")}` : ""}`;
|
|
4601
|
+
preview.append(line);
|
|
4602
|
+
}
|
|
4603
|
+
const actions = document.createElement("div");
|
|
4604
|
+
actions.className = "actions";
|
|
4605
|
+
actions.append(button("Approve manual run", async () => {
|
|
4606
|
+
const result = await request({ kind: "confirmmanualrun", previewid: triggerview.manual?.id, confirmed: true });
|
|
4607
|
+
status(`Manual run approved and launched${result.runid !== void 0 ? ` as run ${result.runid}` : ""}; the run ended ${result.state ?? "running"}.`);
|
|
4608
|
+
triggerview.manual = void 0;
|
|
4609
|
+
await refresh();
|
|
4610
|
+
}));
|
|
4611
|
+
actions.append(" ", button("Cancel manual run", async () => {
|
|
4612
|
+
await request({ kind: "confirmmanualrun", previewid: triggerview.manual?.id, confirmed: false });
|
|
4613
|
+
status("Manual run cancelled after the step preview; nothing ran.");
|
|
4614
|
+
triggerview.manual = void 0;
|
|
4615
|
+
await refresh();
|
|
4616
|
+
}));
|
|
4617
|
+
preview.append(actions);
|
|
4618
|
+
triggersroot.append(preview);
|
|
4619
|
+
}
|
|
4620
|
+
const settings = document.createElement("details");
|
|
4621
|
+
settings.className = "sessiongroup";
|
|
4622
|
+
const settingsummary = document.createElement("summary");
|
|
4623
|
+
settingsummary.textContent = "Trigger settings";
|
|
4624
|
+
settings.append(settingsummary);
|
|
4625
|
+
const retention = document.createElement("p");
|
|
4626
|
+
retention.textContent = `Fire record retention: ${context.triggerretention === void 0 ? "keep every fire record" : `${context.triggerretention} record${context.triggerretention === 1 ? "" : "s"}`}; the rule counters always survive and no code ceiling applies.`;
|
|
4627
|
+
settings.append(retention);
|
|
4628
|
+
const retentionactions = document.createElement("div");
|
|
4629
|
+
retentionactions.className = "actions";
|
|
4630
|
+
retentionactions.append(button("Keep every fire record", async () => {
|
|
4631
|
+
await request({ kind: "settriggerretention" });
|
|
4632
|
+
status("Trigger fire retention keeps every record.");
|
|
4633
|
+
await refresh();
|
|
4634
|
+
}));
|
|
4635
|
+
retentionactions.append(" ", button("Keep last 100 fire records", async () => {
|
|
4636
|
+
await request({ kind: "settriggerretention", retention: 100 });
|
|
4637
|
+
status("Trigger fire retention keeps the last 100 records.");
|
|
4638
|
+
await refresh();
|
|
4639
|
+
}));
|
|
4640
|
+
settings.append(retentionactions);
|
|
4641
|
+
triggersroot.append(settings);
|
|
4642
|
+
}
|
|
3202
4643
|
//# sourceMappingURL=sidepanel.js.map
|