dsh-workflow-map 0.0.0-stage → 0.3.4
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/CHANGELOG.md +184 -0
- package/LICENSE +21 -0
- package/README.en.md +458 -0
- package/README.md +436 -2
- package/client.js +9685 -0
- package/cordis.patch.yml +11 -0
- package/docs/OPEN-SOURCE-GUIDE.md +256 -0
- package/docs/host-compatibility.md +320 -0
- package/docs/screenshot-detail.png +0 -0
- package/docs/screenshot.png +0 -0
- package/icon.svg +6 -0
- package/index.js +12 -0
- package/lib/index.js +974 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +81 -4
- package/src/client/Branches.js +241 -0
- package/src/client/FlowColumn.js +572 -0
- package/src/client/FlowEffects.js +677 -0
- package/src/client/HotInject.js +628 -0
- package/src/client/MainFlow.js +359 -0
- package/src/client/NodeCard.js +545 -0
- package/src/client/NodeDetail.js +569 -0
- package/src/client/ProgressUnder.js +216 -0
- package/src/client/RightPage.js +285 -0
- package/src/client/Settings.js +212 -0
- package/src/client/StepColumn.js +479 -0
- package/src/client/StepDetail.js +250 -0
- package/src/client/export.js +280 -0
- package/src/client/panel.js +1911 -0
- package/src/client/pulse.js +126 -0
- package/src/client/store.js +1090 -0
- package/src/client/theme.js +1176 -0
- package/src/fold.mjs +2763 -0
- package/src/orchestrator.mjs +444 -0
- package/src/route.js +621 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NodeDetail.js - "what did this node actually do", inline under the selected node card.
|
|
3
|
+
*
|
|
4
|
+
* The card (NodeCard.js) answers what / how / how did it in one line; a click expands
|
|
5
|
+
* this block, which is the only place the panel shows:
|
|
6
|
+
* head tool name · category chip (file / command / skill / subagent / CAD / other) · status
|
|
7
|
+
* args the host's argsText digest (≤ ARGS_CAP, monospace, pre-wrap) — a single-line
|
|
8
|
+
* preview plus a <details> that opens the whole stored text
|
|
9
|
+
* files grouped by action: read / edited / written / queried; each row copies the path
|
|
10
|
+
* result the first line; on an error the stored error text is shown in full
|
|
11
|
+
* meta start clock · duration ms · event seq
|
|
12
|
+
*
|
|
13
|
+
* PRIVACY ( §3): the panel never shows file CONTENT. That is structural, not a
|
|
14
|
+
* promise: the host's fold drops `content` / `old_str` / `new_str` when it builds argsText
|
|
15
|
+
* (src/fold.mjs argsDigest), and store.js copies node fields through an explicit whitelist —
|
|
16
|
+
* so a body can neither reach this renderer nor be printed by it. tools/selftest-detail.mjs
|
|
17
|
+
* feeds a node that still carries `content` and asserts the marker never reaches the tree.
|
|
18
|
+
*
|
|
19
|
+
* The copy helpers live here too, because every copy affordance on this panel copies either
|
|
20
|
+
* a path (this file) or a step artifact (StepDetail.js): one implementation, one fallback.
|
|
21
|
+
*
|
|
22
|
+
* Pure: `h` and `say` are injected; no React import, no DOM at module scope. The three DOM
|
|
23
|
+
* functions at the bottom (copy / Escape / layout nudge) take their document and window as
|
|
24
|
+
* parameters so a Node self-test can drive them without a browser.
|
|
25
|
+
*
|
|
26
|
+
* Inlined into client.js by tools/build-client.mjs (single-file browser half).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Host-side bound: src/fold.mjs ARGS_LIMIT. The detail can never show more than this. */
|
|
30
|
+
var WF_DETAIL_ARGS_CAP = 200;
|
|
31
|
+
|
|
32
|
+
/** How much of argsText the collapsed preview keeps (the rest is one click away). */
|
|
33
|
+
var WF_DETAIL_PREVIEW = 110;
|
|
34
|
+
|
|
35
|
+
/** Category → chip label key. `cad` is a detail-only split: the CAD tools are tools too. */
|
|
36
|
+
var WF_DETAIL_CATEGORY_KEY = {
|
|
37
|
+
file: "panel.detail.category.file",
|
|
38
|
+
command: "panel.detail.category.command",
|
|
39
|
+
skill: "panel.detail.category.skill",
|
|
40
|
+
subagent: "panel.detail.category.subagent",
|
|
41
|
+
cad: "panel.detail.category.cad",
|
|
42
|
+
tool: "panel.detail.category.other",
|
|
43
|
+
message: "panel.kind.message",
|
|
44
|
+
report: "panel.kind.report",
|
|
45
|
+
question: "panel.kind.question",
|
|
46
|
+
approval: "panel.kind.approval",
|
|
47
|
+
other: "panel.detail.category.other",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Category → glyph. Same text-glyph family as NodeCard (no emoji in the head). */
|
|
51
|
+
var WF_DETAIL_GLYPH = {
|
|
52
|
+
file: "▤",
|
|
53
|
+
command: "⌘",
|
|
54
|
+
skill: "✦",
|
|
55
|
+
subagent: "⧉",
|
|
56
|
+
cad: "◈",
|
|
57
|
+
tool: "⚙",
|
|
58
|
+
message: "≡",
|
|
59
|
+
report: "◍",
|
|
60
|
+
question: "?",
|
|
61
|
+
approval: "⚑",
|
|
62
|
+
other: "·",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** File-action groups, in the order the step asked for, with their own glyph + label. */
|
|
66
|
+
var WF_DETAIL_ACTIONS = [
|
|
67
|
+
{ action: "read", glyph: "📖", key: "panel.detail.action.read" },
|
|
68
|
+
{ action: "edit", glyph: "✏️", key: "panel.detail.action.edit" },
|
|
69
|
+
{ action: "write", glyph: "💾", key: "panel.detail.action.write" },
|
|
70
|
+
{ action: "query", glyph: "🔎", key: "panel.detail.action.query" },
|
|
71
|
+
{ action: "other", glyph: "▤", key: "panel.detail.action.other" },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The category used by THIS block. It starts from NodeCard's split (so the two views
|
|
76
|
+
* cannot disagree) and only adds the one bucket the card does not have: CAD tools.
|
|
77
|
+
*/
|
|
78
|
+
function wfDetailCategory(node) {
|
|
79
|
+
var tool = node && typeof node.tool === "string" ? node.tool.toLowerCase() : "";
|
|
80
|
+
if (tool === "cad" || tool.indexOf("cad_") === 0 || tool.indexOf("cadflow_") === 0 || tool.indexOf("dsh_cad_") === 0) {
|
|
81
|
+
return "cad";
|
|
82
|
+
}
|
|
83
|
+
return wfNodeCategory(node);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Normalise a host action label to the five groups above. Unknown → "other". */
|
|
87
|
+
function wfDetailAction(action) {
|
|
88
|
+
var label = typeof action === "string" ? action.trim().toLowerCase() : "";
|
|
89
|
+
if (label === "读" || label === "read") return "read";
|
|
90
|
+
if (label === "改" || label === "edit" || label === "修改") return "edit";
|
|
91
|
+
if (label === "写" || label === "write" || label === "写入") return "write";
|
|
92
|
+
if (label === "查" || label === "glob" || label === "grep" || label === "list" || label === "query") return "query";
|
|
93
|
+
return "other";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** "D:\\proj\\src\\client\\NodeDetail.js" → {base:"NodeDetail.js", dir:"D:/proj/src/client"}. */
|
|
97
|
+
function wfDetailPath(path) {
|
|
98
|
+
var raw = typeof path === "string" ? path : "";
|
|
99
|
+
var flat = raw.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
100
|
+
var at = flat.lastIndexOf("/");
|
|
101
|
+
return {
|
|
102
|
+
path: raw,
|
|
103
|
+
base: at === -1 ? flat : flat.slice(at + 1),
|
|
104
|
+
dir: at === -1 ? "" : flat.slice(0, at),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* File rows grouped by action, first occurrence of a path winning within its group.
|
|
110
|
+
* A node with no files yields [] - the renderer prints "no files touched" itself.
|
|
111
|
+
*/
|
|
112
|
+
function wfDetailFileGroups(node) {
|
|
113
|
+
var list = node && Array.isArray(node.fileList) ? node.fileList : [];
|
|
114
|
+
var groups = [];
|
|
115
|
+
var byAction = Object.create(null);
|
|
116
|
+
for (var i = 0; i < list.length; i += 1) {
|
|
117
|
+
var entry = list[i];
|
|
118
|
+
if (!entry || typeof entry.path !== "string" || entry.path === "") continue;
|
|
119
|
+
var action = wfDetailAction(entry.action);
|
|
120
|
+
var group = byAction[action];
|
|
121
|
+
if (!group) {
|
|
122
|
+
group = { action: action, files: [], seen: Object.create(null) };
|
|
123
|
+
byAction[action] = group;
|
|
124
|
+
groups.push(group);
|
|
125
|
+
}
|
|
126
|
+
if (group.seen[entry.path] === true) continue;
|
|
127
|
+
group.seen[entry.path] = true;
|
|
128
|
+
var split = wfDetailPath(entry.path);
|
|
129
|
+
group.files.push({ path: split.path, base: split.base, dir: split.dir });
|
|
130
|
+
}
|
|
131
|
+
var out = [];
|
|
132
|
+
for (var a = 0; a < WF_DETAIL_ACTIONS.length; a += 1) {
|
|
133
|
+
var found = byAction[WF_DETAIL_ACTIONS[a].action];
|
|
134
|
+
if (found) out.push({ action: found.action, glyph: WF_DETAIL_ACTIONS[a].glyph, key: WF_DETAIL_ACTIONS[a].key, files: found.files });
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The node's own status for the head chip: the host's error flag wins over everything. */
|
|
140
|
+
function wfDetailStatus(node) {
|
|
141
|
+
if (node && node.error === true) return "error";
|
|
142
|
+
if (node && node.result === true) return "done";
|
|
143
|
+
return "pending";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The args text this detail may show: the stored digest, bounded again at the host cap. */
|
|
147
|
+
function wfDetailArgsText(node) {
|
|
148
|
+
var raw = node && typeof node.argsText === "string" ? node.argsText : "";
|
|
149
|
+
return raw.length > WF_DETAIL_ARGS_CAP ? raw.slice(0, WF_DETAIL_ARGS_CAP) : raw;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* One copy control. PURE: the click calls `actions.onCopy(text, element)`, and the element
|
|
154
|
+
* is what the panel's feedback routine repaints for 1.5 s. `data-wf-copy` carries the exact
|
|
155
|
+
* text, so the tree itself can be asserted without a clipboard.
|
|
156
|
+
*/
|
|
157
|
+
function wfRenderCopyButton(h, text, say, actions, className, labelKey, titleKey) {
|
|
158
|
+
var fire = actions || {};
|
|
159
|
+
var label = say(labelKey);
|
|
160
|
+
return h(
|
|
161
|
+
"button",
|
|
162
|
+
{
|
|
163
|
+
type: "button",
|
|
164
|
+
className: className,
|
|
165
|
+
key: "copy-" + (titleKey || labelKey),
|
|
166
|
+
"data-wf-copy": text,
|
|
167
|
+
"data-wf-copy-label": label,
|
|
168
|
+
"data-copy-state": "idle",
|
|
169
|
+
title: say(titleKey || labelKey) + " — " + text,
|
|
170
|
+
"aria-label": say(titleKey || labelKey),
|
|
171
|
+
onClick: function (event) {
|
|
172
|
+
if (typeof fire.onCopy === "function") {
|
|
173
|
+
var element = event && event.currentTarget ? event.currentTarget : null;
|
|
174
|
+
fire.onCopy(text, element);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
// Two spans, never a bare text node: the 1.5 s "Copied" repaints the status span only,
|
|
179
|
+
// so a file row keeps its basename + directory (a textContent write would erase them and
|
|
180
|
+
// React would not restore children it thinks are unchanged).
|
|
181
|
+
h("span", { className: "wf-copy-label", key: "label" }, label),
|
|
182
|
+
h("span", { className: "wf-copy-status", key: "status", "aria-live": "polite" }, "")
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function wfRenderFileRows(h, group, say, actions) {
|
|
187
|
+
return h(
|
|
188
|
+
"div",
|
|
189
|
+
{ className: "wf-detail-group", key: "group-" + group.action, "data-action": group.action },
|
|
190
|
+
h(
|
|
191
|
+
"span",
|
|
192
|
+
{ className: "wf-detail-sublabel", key: "label" },
|
|
193
|
+
h("span", { className: "wf-detail-glyph", key: "glyph", "aria-hidden": "true" }, group.glyph),
|
|
194
|
+
say(group.key) + " · " + group.files.length
|
|
195
|
+
),
|
|
196
|
+
h(
|
|
197
|
+
"div",
|
|
198
|
+
{ className: "wf-detail-rows", key: "rows" },
|
|
199
|
+
group.files.map(function (file, position) {
|
|
200
|
+
// The whole row is the copy button: basename in the foreground, directory grey.
|
|
201
|
+
return h(
|
|
202
|
+
"button",
|
|
203
|
+
{
|
|
204
|
+
type: "button",
|
|
205
|
+
className: "wf-detail-file",
|
|
206
|
+
key: "file-" + group.action + "-" + position,
|
|
207
|
+
"data-wf-copy": file.path,
|
|
208
|
+
"data-wf-copy-label": file.base,
|
|
209
|
+
"data-copy-state": "idle",
|
|
210
|
+
title: say("panel.detail.copypath") + " — " + file.path,
|
|
211
|
+
"aria-label": say("panel.detail.copypath") + " " + file.path,
|
|
212
|
+
onClick: function (event) {
|
|
213
|
+
var fire = actions || {};
|
|
214
|
+
if (typeof fire.onCopy === "function") {
|
|
215
|
+
fire.onCopy(file.path, event && event.currentTarget ? event.currentTarget : null);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
h("span", { className: "wf-detail-base", key: "base" }, file.base),
|
|
220
|
+
file.dir === "" ? null : h("span", { className: "wf-detail-dir", key: "dir", title: file.dir }, file.dir),
|
|
221
|
+
h("span", { className: "wf-copy-status", key: "status", "aria-live": "polite" }, "")
|
|
222
|
+
);
|
|
223
|
+
})
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The block itself. `node` is a store.js flow node (argsText / fileList / errorText / seq
|
|
230
|
+
* are the fields this renderer needs). Returns null for a missing node so a caller can
|
|
231
|
+
* write `selected ? wfRenderNodeDetail(...) : null` without a type check.
|
|
232
|
+
*/
|
|
233
|
+
function wfRenderNodeDetail(h, node, say, actions, ctx) {
|
|
234
|
+
if (!node || typeof node.id !== "string") return null;
|
|
235
|
+
var context = ctx || {};
|
|
236
|
+
var category = wfDetailCategory(node);
|
|
237
|
+
var status = wfDetailStatus(node);
|
|
238
|
+
var argsText = wfDetailArgsText(node);
|
|
239
|
+
var capped = typeof node.argsText === "string" && node.argsText.length >= WF_DETAIL_ARGS_CAP;
|
|
240
|
+
var groups = wfDetailFileGroups(node);
|
|
241
|
+
var resultLine = typeof node.resultLine === "string" && node.resultLine !== "" ? node.resultLine : "";
|
|
242
|
+
var errorText = typeof node.errorText === "string" && node.errorText !== "" ? node.errorText : "";
|
|
243
|
+
var seq = wfFinite(node.seq);
|
|
244
|
+
var clock = wfFormatClock(node.at);
|
|
245
|
+
var duration = wfFinite(node.durationMs);
|
|
246
|
+
var argsTitleKey = category === "command" ? "panel.detail.showcommand" : "panel.detail.showargs";
|
|
247
|
+
|
|
248
|
+
var head = h(
|
|
249
|
+
"div",
|
|
250
|
+
{ className: "wf-detail-head", key: "head" },
|
|
251
|
+
h(
|
|
252
|
+
"span",
|
|
253
|
+
{ className: "wf-detail-icon", key: "icon", "aria-hidden": "true" },
|
|
254
|
+
WF_DETAIL_GLYPH[category] || WF_DETAIL_GLYPH.other
|
|
255
|
+
),
|
|
256
|
+
h("span", { className: "wf-detail-name", key: "name" }, node.tool || say("panel.detail.unknowntool")),
|
|
257
|
+
h(
|
|
258
|
+
"span",
|
|
259
|
+
{ className: "wf-detail-chip", key: "category", "data-category": category },
|
|
260
|
+
say(WF_DETAIL_CATEGORY_KEY[category] || WF_DETAIL_CATEGORY_KEY.other)
|
|
261
|
+
),
|
|
262
|
+
h(
|
|
263
|
+
"span",
|
|
264
|
+
{ className: "wf-detail-chip wf-detail-state", key: "status", "data-state": status },
|
|
265
|
+
say(
|
|
266
|
+
status === "error"
|
|
267
|
+
? "panel.status.failed"
|
|
268
|
+
: status === "done"
|
|
269
|
+
? "panel.status.done"
|
|
270
|
+
: "panel.node.pending"
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// The expansion is a native <details>: keyboard-operable, no React state, and the opened
|
|
276
|
+
// text is the WHOLE stored argsText (the host's 200-char digest, not the file body).
|
|
277
|
+
var argsChildren = [
|
|
278
|
+
h(
|
|
279
|
+
"div",
|
|
280
|
+
{ className: "wf-detail-block-head", key: "head" },
|
|
281
|
+
h("span", { className: "wf-detail-label", key: "label" }, say("panel.detail.args")),
|
|
282
|
+
argsText === "" ? null : wfRenderCopyButton(h, argsText, say, actions, "wf-detail-copy", "panel.detail.copy", "panel.detail.copyargs")
|
|
283
|
+
),
|
|
284
|
+
argsText === ""
|
|
285
|
+
? h("div", { className: "wf-detail-empty", key: "empty" }, say("panel.detail.noargs"))
|
|
286
|
+
: h(
|
|
287
|
+
"pre",
|
|
288
|
+
{ className: "wf-detail-preview", key: "preview", "data-collapsed": "true" },
|
|
289
|
+
wfTrimText(argsText, WF_DETAIL_PREVIEW) || argsText.slice(0, WF_DETAIL_PREVIEW)
|
|
290
|
+
),
|
|
291
|
+
];
|
|
292
|
+
if (argsText !== "") {
|
|
293
|
+
argsChildren.push(
|
|
294
|
+
h(
|
|
295
|
+
"details",
|
|
296
|
+
{ className: "wf-detail-more", key: "more", "data-wf-more": category === "command" ? "command" : "args" },
|
|
297
|
+
h("summary", { className: "wf-detail-summary", key: "summary" }, say(argsTitleKey)),
|
|
298
|
+
h("pre", { className: "wf-detail-args", key: "body" }, argsText)
|
|
299
|
+
)
|
|
300
|
+
);
|
|
301
|
+
if (capped) {
|
|
302
|
+
argsChildren.push(h("div", { className: "wf-detail-note", key: "cap" }, say("panel.detail.argscap")));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
var args = h("div", { className: "wf-detail-block", key: "args", "data-block": "args" }, argsChildren);
|
|
306
|
+
|
|
307
|
+
var files = h(
|
|
308
|
+
"div",
|
|
309
|
+
{ className: "wf-detail-block", key: "files", "data-block": "files" },
|
|
310
|
+
h(
|
|
311
|
+
"div",
|
|
312
|
+
{ className: "wf-detail-block-head", key: "head" },
|
|
313
|
+
h("span", { className: "wf-detail-label", key: "label" }, say("panel.detail.files"))
|
|
314
|
+
),
|
|
315
|
+
groups.length === 0
|
|
316
|
+
? h("div", { className: "wf-detail-empty", key: "empty" }, say("panel.detail.nofiles"))
|
|
317
|
+
: h(
|
|
318
|
+
"div",
|
|
319
|
+
{ className: "wf-detail-groups", key: "groups" },
|
|
320
|
+
groups.map(function (group) {
|
|
321
|
+
return wfRenderFileRows(h, group, say, actions);
|
|
322
|
+
})
|
|
323
|
+
)
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
var result = h(
|
|
327
|
+
"div",
|
|
328
|
+
{ className: "wf-detail-block", key: "result", "data-block": "result" },
|
|
329
|
+
h(
|
|
330
|
+
"div",
|
|
331
|
+
{ className: "wf-detail-block-head", key: "head" },
|
|
332
|
+
h(
|
|
333
|
+
"span",
|
|
334
|
+
{ className: "wf-detail-label", key: "label" },
|
|
335
|
+
say(status === "error" ? "panel.detail.error" : "panel.detail.result")
|
|
336
|
+
)
|
|
337
|
+
),
|
|
338
|
+
status === "error"
|
|
339
|
+
? h(
|
|
340
|
+
"div",
|
|
341
|
+
{ className: "wf-detail-errors", key: "error" },
|
|
342
|
+
h("pre", { className: "wf-detail-error", key: "text" }, errorText || say("panel.detail.noerror")),
|
|
343
|
+
h("div", { className: "wf-detail-note", key: "cap" }, say("panel.detail.errorcap"))
|
|
344
|
+
)
|
|
345
|
+
: h(
|
|
346
|
+
"div",
|
|
347
|
+
{ className: "wf-detail-result", key: "ok" },
|
|
348
|
+
resultLine || say("panel.detail.noresult")
|
|
349
|
+
)
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
var meta = h(
|
|
353
|
+
"div",
|
|
354
|
+
{ className: "wf-detail-meta", key: "meta" },
|
|
355
|
+
h("span", { key: "start" }, say("panel.detail.started") + " " + (clock || "—")),
|
|
356
|
+
h("span", { key: "duration" }, say("panel.detail.duration") + " " + (duration === null || duration <= 0 ? "—" : Math.round(duration) + "ms")),
|
|
357
|
+
h("span", { key: "seq" }, "seq " + (seq === null ? "—" : String(Math.round(seq))))
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
return h(
|
|
361
|
+
"div",
|
|
362
|
+
{
|
|
363
|
+
className: "wf-node-detail",
|
|
364
|
+
key: "detail-" + node.id,
|
|
365
|
+
role: "region",
|
|
366
|
+
"data-dsh-workflow-map": "node-detail",
|
|
367
|
+
"data-node-id": node.id,
|
|
368
|
+
"data-category": category,
|
|
369
|
+
"data-status": status,
|
|
370
|
+
"aria-label": say("panel.detail.aria", { tool: node.tool || "—" }),
|
|
371
|
+
},
|
|
372
|
+
head,
|
|
373
|
+
args,
|
|
374
|
+
files,
|
|
375
|
+
result,
|
|
376
|
+
meta
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* ------------------------------------------------------------------------- */
|
|
381
|
+
/* clipboard */
|
|
382
|
+
/* ------------------------------------------------------------------------- */
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Copy `text`. The async Clipboard API is used when the environment has one; anything
|
|
386
|
+
* else (file:// page, denied permission, older browser) falls back to the hidden
|
|
387
|
+
* textarea + execCommand('copy') path. Returns a Promise<boolean> — never throws.
|
|
388
|
+
*/
|
|
389
|
+
function wfCopyText(text, env) {
|
|
390
|
+
var value = typeof text === "string" ? text : String(text === undefined || text === null ? "" : text);
|
|
391
|
+
var scope = env || {};
|
|
392
|
+
var clipboard = scope.clipboard || (typeof navigator !== "undefined" && navigator ? navigator.clipboard : null);
|
|
393
|
+
if (clipboard && typeof clipboard.writeText === "function") {
|
|
394
|
+
try {
|
|
395
|
+
return Promise.resolve(clipboard.writeText(value)).then(
|
|
396
|
+
function () {
|
|
397
|
+
return true;
|
|
398
|
+
},
|
|
399
|
+
function () {
|
|
400
|
+
return wfCopyFallback(value, scope);
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
return Promise.resolve(wfCopyFallback(value, scope));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return Promise.resolve(wfCopyFallback(value, scope));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** The pre-Clipboard-API path. True only when execCommand reports success. */
|
|
411
|
+
function wfCopyFallback(text, env) {
|
|
412
|
+
var doc = (env && env.document) || (typeof document !== "undefined" ? document : null);
|
|
413
|
+
if (!doc || typeof doc.createElement !== "function" || !doc.body) return false;
|
|
414
|
+
var area = doc.createElement("textarea");
|
|
415
|
+
if (!area || typeof area !== "object") return false;
|
|
416
|
+
area.value = text;
|
|
417
|
+
area.setAttribute("readonly", "");
|
|
418
|
+
area.setAttribute("aria-hidden", "true");
|
|
419
|
+
if (area.style) {
|
|
420
|
+
area.style.position = "fixed";
|
|
421
|
+
area.style.top = "-1000px";
|
|
422
|
+
area.style.opacity = "0";
|
|
423
|
+
}
|
|
424
|
+
doc.body.appendChild(area);
|
|
425
|
+
var copied = false;
|
|
426
|
+
try {
|
|
427
|
+
if (typeof area.focus === "function") area.focus();
|
|
428
|
+
if (typeof area.select === "function") area.select();
|
|
429
|
+
if (typeof area.setSelectionRange === "function") area.setSelectionRange(0, text.length);
|
|
430
|
+
copied = typeof doc.execCommand === "function" ? doc.execCommand("copy") === true : false;
|
|
431
|
+
} catch (error) {
|
|
432
|
+
copied = false;
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
if (area.parentNode) area.parentNode.removeChild(area);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
/* an unremovable node is still harmless: it is offscreen and empty */
|
|
438
|
+
}
|
|
439
|
+
return copied;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Repaint ONE copy control for its 1.5 s acknowledgement. Pure DOM mutation, so the
|
|
444
|
+
* renderers stay state-free and a React re-render cannot strand a "Copied" label.
|
|
445
|
+
*/
|
|
446
|
+
function wfSetCopyState(element, state, say) {
|
|
447
|
+
if (!element || typeof element !== "object") return false;
|
|
448
|
+
var label =
|
|
449
|
+
state === "copied"
|
|
450
|
+
? say("panel.detail.copied")
|
|
451
|
+
: state === "failed"
|
|
452
|
+
? say("panel.detail.copyfailed")
|
|
453
|
+
: null;
|
|
454
|
+
if (typeof element.setAttribute === "function") element.setAttribute("data-copy-state", state);
|
|
455
|
+
// Preferred: the dedicated status span. The button's label (and a file row's basename +
|
|
456
|
+
// directory) stay in place; CSS hides the label while the status is showing.
|
|
457
|
+
var status = typeof element.querySelector === "function" ? element.querySelector(".wf-copy-status") : null;
|
|
458
|
+
if (status && typeof status.textContent === "string") {
|
|
459
|
+
status.textContent = label === null ? "" : label;
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
// Fallback for a plain element (a test double, or a future copy control without the span).
|
|
463
|
+
if (typeof element.textContent === "string") {
|
|
464
|
+
if (label !== null) element.textContent = label;
|
|
465
|
+
else if (typeof element.getAttribute === "function") {
|
|
466
|
+
var original = element.getAttribute("data-wf-copy-label");
|
|
467
|
+
if (typeof original === "string") element.textContent = original;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Copy + acknowledge. `env` may carry {clipboard, document} and `env.setTimeout` so the
|
|
475
|
+
* 1.5 s restore can be exercised in Node without waiting.
|
|
476
|
+
*/
|
|
477
|
+
function wfCopyWithFeedback(text, element, say, env) {
|
|
478
|
+
var scope = env || {};
|
|
479
|
+
var timer = typeof scope.setTimeout === "function" ? scope.setTimeout : typeof setTimeout === "function" ? setTimeout : null;
|
|
480
|
+
var fire = typeof say === "function" ? say : function (key) {
|
|
481
|
+
return key;
|
|
482
|
+
};
|
|
483
|
+
wfSetCopyState(element, "pending", fire);
|
|
484
|
+
return wfCopyText(text, scope).then(function (ok) {
|
|
485
|
+
wfSetCopyState(element, ok === true ? "copied" : "failed", fire);
|
|
486
|
+
if (timer) {
|
|
487
|
+
timer(function () {
|
|
488
|
+
wfSetCopyState(element, "idle", fire);
|
|
489
|
+
}, WF_COPY_FEEDBACK_MS);
|
|
490
|
+
}
|
|
491
|
+
return ok === true;
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** How long "Copied" stays on the button. */
|
|
496
|
+
var WF_COPY_FEEDBACK_MS = 1500;
|
|
497
|
+
|
|
498
|
+
/* ------------------------------------------------------------------------- */
|
|
499
|
+
/* keyboard + layout wiring */
|
|
500
|
+
/* ------------------------------------------------------------------------- */
|
|
501
|
+
|
|
502
|
+
/** Escape, and not while the user is composing (IME) or holding a modifier. */
|
|
503
|
+
function wfEscapeIntent(event) {
|
|
504
|
+
if (!event || event.key !== "Escape") return false;
|
|
505
|
+
if (event.isComposing === true || event.altKey === true || event.ctrlKey === true || event.metaKey === true) return false;
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Document-level Escape: collapse the details, not the panel. `actions.onEscapeDetail`
|
|
511
|
+
* is fired once per Escape; returns a handle with destroy so the hook can unmount it.
|
|
512
|
+
*/
|
|
513
|
+
function wfBindEscape(doc, actions) {
|
|
514
|
+
if (!doc || typeof doc.addEventListener !== "function") return { destroy: function () {} };
|
|
515
|
+
var fire = actions || {};
|
|
516
|
+
function onKeyDown(event) {
|
|
517
|
+
if (!wfEscapeIntent(event)) return;
|
|
518
|
+
if (typeof fire.onEscapeDetail === "function") fire.onEscapeDetail();
|
|
519
|
+
}
|
|
520
|
+
doc.addEventListener("keydown", onKeyDown, true);
|
|
521
|
+
return {
|
|
522
|
+
destroy: function () {
|
|
523
|
+
if (typeof doc.removeEventListener === "function") doc.removeEventListener("keydown", onKeyDown, true);
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Details change the height of the scrolling columns, and the curve measures the
|
|
530
|
+
* two cards' rects. A <details> toggle does not resize any observed box, so the curve
|
|
531
|
+
* would keep the old endpoints: nudge the layer through its existing resize listener
|
|
532
|
+
* once the browser has laid the new height out.
|
|
533
|
+
*/
|
|
534
|
+
function wfBindDetailNudge(doc, win) {
|
|
535
|
+
if (!doc || typeof doc.addEventListener !== "function") return { destroy: function () {} };
|
|
536
|
+
var scope = win || {};
|
|
537
|
+
var timer = typeof scope.setTimeout === "function" ? scope.setTimeout : null;
|
|
538
|
+
function nudge() {
|
|
539
|
+
if (!timer) return;
|
|
540
|
+
// One task later: the <details> content exists and the column has scrolled if it had to.
|
|
541
|
+
timer(function () {
|
|
542
|
+
try {
|
|
543
|
+
if (typeof scope.dispatchEvent === "function" && typeof scope.Event === "function") {
|
|
544
|
+
scope.dispatchEvent(new scope.Event("resize"));
|
|
545
|
+
}
|
|
546
|
+
} catch (error) {
|
|
547
|
+
/* a closed window has nothing to re-measure */
|
|
548
|
+
}
|
|
549
|
+
}, 40);
|
|
550
|
+
}
|
|
551
|
+
function onToggle() {
|
|
552
|
+
nudge();
|
|
553
|
+
}
|
|
554
|
+
function onClick(event) {
|
|
555
|
+
var target = event && event.target;
|
|
556
|
+
if (!target || typeof target.closest !== "function") return;
|
|
557
|
+
if (target.closest('[data-dsh-workflow-map="step-info"]') || target.closest(".wf-detail-more")) nudge();
|
|
558
|
+
}
|
|
559
|
+
doc.addEventListener("toggle", onToggle, true);
|
|
560
|
+
doc.addEventListener("click", onClick, true);
|
|
561
|
+
return {
|
|
562
|
+
destroy: function () {
|
|
563
|
+
if (typeof doc.removeEventListener === "function") {
|
|
564
|
+
doc.removeEventListener("toggle", onToggle, true);
|
|
565
|
+
doc.removeEventListener("click", onClick, true);
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
}
|