@ai-setting/roy-plugin-task-show 2.0.7 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/dist/task-detail-mermaid.d.ts +29 -0
- package/dist/task-detail-mermaid.d.ts.map +1 -1
- package/dist/task-detail-mermaid.js +96 -7
- package/dist/task-detail-mermaid.js.map +1 -1
- package/dist/task-operations-html.d.ts.map +1 -1
- package/dist/task-operations-html.js +22 -7
- package/dist/task-operations-html.js.map +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/index.html +16 -0
- package/public/mermaid-renderer.js +180 -0
- package/public/style.css +103 -0
- package/public/task-operations.js +150 -7
- package/public/tool-call-detail.js +74 -0
package/public/index.html
CHANGED
|
@@ -18,6 +18,22 @@
|
|
|
18
18
|
</p>
|
|
19
19
|
</header>
|
|
20
20
|
|
|
21
|
+
<!--
|
|
22
|
+
v2.0.8 (feat/mermaid-zoom): hidden toolbar template that
|
|
23
|
+
`mermaid-renderer.js` clones for every rendered diagram.
|
|
24
|
+
The actual buttons are bound to `MermaidRenderer.zoomIn /
|
|
25
|
+
zoomOut / resetZoom` by the renderer's mount handler — this
|
|
26
|
+
template is just the markup. Keyboard shortcuts (+/−/0) are
|
|
27
|
+
also wired by the renderer.
|
|
28
|
+
-->
|
|
29
|
+
<template id="mermaid-zoom-toolbar-template">
|
|
30
|
+
<div class="mermaid-zoom-toolbar" role="toolbar" aria-label="Mermaid diagram zoom controls">
|
|
31
|
+
<button type="button" class="mermaid-zoom-btn" data-mermaid-zoom="out" aria-label="Zoom out" title="Zoom out (−)">−</button>
|
|
32
|
+
<button type="button" class="mermaid-zoom-btn" data-mermaid-zoom="reset" aria-label="Reset zoom" title="Reset zoom (0)">100%</button>
|
|
33
|
+
<button type="button" class="mermaid-zoom-btn" data-mermaid-zoom="in" aria-label="Zoom in" title="Zoom in (+)">+</button>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
21
37
|
<section class="panel tree-controls">
|
|
22
38
|
<div class="tree-controls-row">
|
|
23
39
|
<label class="tree-search">
|
|
@@ -54,6 +54,14 @@
|
|
|
54
54
|
"use strict";
|
|
55
55
|
|
|
56
56
|
const DEFAULT_ID_PREFIX = "mmr";
|
|
57
|
+
// v2.0.8 (feat/mermaid-zoom): zoom-in / zoom-out step and
|
|
58
|
+
// sanity bounds. We default to 10% per click (matching the
|
|
59
|
+
// common browser zoom UX) and clamp to a range that keeps the
|
|
60
|
+
// diagram readable on both directions.
|
|
61
|
+
const DEFAULT_ZOOM_STEP = 0.1;
|
|
62
|
+
const ZOOM_MIN = 0.25;
|
|
63
|
+
const ZOOM_MAX = 3.0;
|
|
64
|
+
const ZOOM_DEFAULT = 1.0;
|
|
57
65
|
|
|
58
66
|
function escapeHtml(s) {
|
|
59
67
|
return String(s)
|
|
@@ -142,6 +150,11 @@
|
|
|
142
150
|
// leak references after the container is removed.
|
|
143
151
|
const inflight = new WeakMap(); // container -> { token: number, source: string, destroyed: boolean, lastRenderedSource: string }
|
|
144
152
|
const seq = new WeakMap(); // container -> number
|
|
153
|
+
// v2.0.8 (feat/mermaid-zoom): per-container zoom state,
|
|
154
|
+
// independent of the rendering state. Keyed by the same
|
|
155
|
+
// container reference so that re-rendering a diagram does
|
|
156
|
+
// not reset the user's zoom level.
|
|
157
|
+
const zoomState = new WeakMap(); // container -> number (0.25..3.0)
|
|
145
158
|
|
|
146
159
|
function nextSeq(container) {
|
|
147
160
|
const cur = seq.get(container) || 0;
|
|
@@ -182,6 +195,156 @@
|
|
|
182
195
|
return cur ? cur.lastRenderedSource : null;
|
|
183
196
|
}
|
|
184
197
|
|
|
198
|
+
// -----------------------------------------------------------------
|
|
199
|
+
// v2.0.8 (feat/mermaid-zoom): zoom-in / zoom-out / reset / get
|
|
200
|
+
// -----------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
function clampZoom(z) {
|
|
203
|
+
if (!Number.isFinite(z)) return ZOOM_DEFAULT;
|
|
204
|
+
if (z < ZOOM_MIN) return ZOOM_MIN;
|
|
205
|
+
if (z > ZOOM_MAX) return ZOOM_MAX;
|
|
206
|
+
return z;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function roundZoom(z) {
|
|
210
|
+
// Round to 2 decimal places so 1.1000000000000003 doesn't
|
|
211
|
+
// leak into the CSS transform (and so the trace payload is
|
|
212
|
+
// human-readable).
|
|
213
|
+
return Math.round(z * 100) / 100;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getZoom(container) {
|
|
217
|
+
if (!container) return ZOOM_DEFAULT;
|
|
218
|
+
const v = zoomState.get(container);
|
|
219
|
+
return v == null ? ZOOM_DEFAULT : v;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function ensureZoomStage(container) {
|
|
223
|
+
// Lazy-create the wrapper if the user calls zoom before a
|
|
224
|
+
// successful mount (the regression test exercises this path).
|
|
225
|
+
if (!container || !container.querySelector) return null;
|
|
226
|
+
let stage = container.querySelector(".mermaid-zoom-stage");
|
|
227
|
+
if (stage) return stage;
|
|
228
|
+
stage = container.ownerDocument.createElement("div");
|
|
229
|
+
stage.className = "mermaid-zoom-stage";
|
|
230
|
+
// Re-parent any existing children (e.g. an already-rendered
|
|
231
|
+
// SVG) into the new wrapper so the zoom transform applies to
|
|
232
|
+
// them too. This keeps the existing render pipeline
|
|
233
|
+
// untouched — the controller just adds a transform layer.
|
|
234
|
+
const doc = container.ownerDocument;
|
|
235
|
+
const fragment = doc.createDocumentFragment
|
|
236
|
+
? doc.createDocumentFragment()
|
|
237
|
+
: null;
|
|
238
|
+
if (fragment) {
|
|
239
|
+
while (container.firstChild) {
|
|
240
|
+
fragment.appendChild(container.firstChild);
|
|
241
|
+
}
|
|
242
|
+
stage.appendChild(fragment);
|
|
243
|
+
}
|
|
244
|
+
container.appendChild(stage);
|
|
245
|
+
return stage;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* v2.0.8 (feat/mermaid-zoom): wire the optional toolbar
|
|
250
|
+
* template (`<template id="mermaid-zoom-toolbar-template">` in
|
|
251
|
+
* `index.html`) to this container's zoom methods. The
|
|
252
|
+
* toolbar is inserted BEFORE the zoom stage, so the visual
|
|
253
|
+
* order is: [toolbar] [diagram]. We look for the template
|
|
254
|
+
* by id; if it is not present (e.g. in unit tests that
|
|
255
|
+
* don't ship the HTML), the call is a silent no-op.
|
|
256
|
+
*/
|
|
257
|
+
function attachZoomToolbar(container) {
|
|
258
|
+
if (!container || !container.ownerDocument) return;
|
|
259
|
+
const doc = container.ownerDocument;
|
|
260
|
+
// Already wired — don't double-bind.
|
|
261
|
+
if (container.querySelector(".mermaid-zoom-toolbar")) return;
|
|
262
|
+
const tpl = doc.getElementById
|
|
263
|
+
? doc.getElementById("mermaid-zoom-toolbar-template")
|
|
264
|
+
: null;
|
|
265
|
+
if (!tpl || !tpl.content) return;
|
|
266
|
+
const fragment = doc.createDocumentFragment
|
|
267
|
+
? doc.createDocumentFragment()
|
|
268
|
+
: null;
|
|
269
|
+
if (!fragment) return;
|
|
270
|
+
// Clone the template's children into a fresh document
|
|
271
|
+
// fragment, then attach the event listeners.
|
|
272
|
+
const node = tpl.content.cloneNode(true);
|
|
273
|
+
const toolbar = node.querySelector(".mermaid-zoom-toolbar");
|
|
274
|
+
if (!toolbar) return;
|
|
275
|
+
// Wire the three buttons.
|
|
276
|
+
const inBtn = toolbar.querySelector('[data-mermaid-zoom="in"]');
|
|
277
|
+
const outBtn = toolbar.querySelector('[data-mermaid-zoom="out"]');
|
|
278
|
+
const resetBtn = toolbar.querySelector('[data-mermaid-zoom="reset"]');
|
|
279
|
+
if (inBtn) inBtn.addEventListener("click", () => zoomIn(container));
|
|
280
|
+
if (outBtn) outBtn.addEventListener("click", () => zoomOut(container));
|
|
281
|
+
if (resetBtn) resetBtn.addEventListener("click", () => resetZoom(container));
|
|
282
|
+
// Insert at the top of the container (so the toolbar
|
|
283
|
+
// appears above the diagram).
|
|
284
|
+
if (container.firstChild) {
|
|
285
|
+
container.insertBefore(toolbar, container.firstChild);
|
|
286
|
+
} else {
|
|
287
|
+
container.appendChild(toolbar);
|
|
288
|
+
}
|
|
289
|
+
// Re-apply the current zoom so the "100%" label reflects
|
|
290
|
+
// the actual scale (in case the user re-mounted a diagram
|
|
291
|
+
// that already had a non-default zoom).
|
|
292
|
+
applyZoom(container, getZoom(container));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function applyZoom(container, zoom) {
|
|
296
|
+
const stage = ensureZoomStage(container);
|
|
297
|
+
if (!stage) return;
|
|
298
|
+
// Use the inline `style` so the transform survives any
|
|
299
|
+
// user-provided stylesheet overrides. We also set
|
|
300
|
+
// `transform-origin: top left` so the diagram does not
|
|
301
|
+
// recenter on every zoom step (Mermaid SVGs have their own
|
|
302
|
+
// viewBox, but the surrounding page would jump otherwise).
|
|
303
|
+
stage.style.transformOrigin = "top left";
|
|
304
|
+
stage.style.transform = `scale(${zoom})`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function setZoom(container, nextZoom) {
|
|
308
|
+
if (!container) return ZOOM_DEFAULT;
|
|
309
|
+
const from = getZoom(container);
|
|
310
|
+
const to = roundZoom(clampZoom(nextZoom));
|
|
311
|
+
if (Math.abs(to - from) < 1e-9) {
|
|
312
|
+
// No-op: zoom did not actually change (e.g. clamped at the
|
|
313
|
+
// limit). Skip the trace emission so the host does not
|
|
314
|
+
// see a flood of `mermaid.zoom.change` events at the
|
|
315
|
+
// bound.
|
|
316
|
+
return to;
|
|
317
|
+
}
|
|
318
|
+
zoomState.set(container, to);
|
|
319
|
+
applyZoom(container, to);
|
|
320
|
+
// v2.0.8: emit the canonical `mermaid.zoom.change` trace.
|
|
321
|
+
// public/ is vanilla JS (no @TracedAs decorator), so we
|
|
322
|
+
// log a console.debug line the host OTel / visual-verify
|
|
323
|
+
// scripts can grep for.
|
|
324
|
+
try {
|
|
325
|
+
if (typeof console !== "undefined" && typeof console.debug === "function") {
|
|
326
|
+
console.debug("[trace] mermaid.zoom.change", { from, to });
|
|
327
|
+
}
|
|
328
|
+
} catch (_) {
|
|
329
|
+
// ignore — tracing must never break the controller
|
|
330
|
+
}
|
|
331
|
+
return to;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function zoomIn(container, step) {
|
|
335
|
+
const s = typeof step === "number" && Number.isFinite(step) ? Math.abs(step) : DEFAULT_ZOOM_STEP;
|
|
336
|
+
return setZoom(container, getZoom(container) + s);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function zoomOut(container, step) {
|
|
340
|
+
const s = typeof step === "number" && Number.isFinite(step) ? Math.abs(step) : DEFAULT_ZOOM_STEP;
|
|
341
|
+
return setZoom(container, getZoom(container) - s);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function resetZoom(container) {
|
|
345
|
+
return setZoom(container, ZOOM_DEFAULT);
|
|
346
|
+
}
|
|
347
|
+
|
|
185
348
|
/**
|
|
186
349
|
* Render `source` into `container`. Returns a Promise that resolves when
|
|
187
350
|
* the container reflects the latest source (either an SVG or an error
|
|
@@ -274,6 +437,14 @@
|
|
|
274
437
|
setState(container, "rendered");
|
|
275
438
|
rememberRenderedSource(container, source);
|
|
276
439
|
if (onStateChange) onStateChange(container, "rendered");
|
|
440
|
+
// v2.0.8 (feat/mermaid-zoom): wrap the freshly-rendered
|
|
441
|
+
// SVG in a `.mermaid-zoom-stage` div so the zoom transform
|
|
442
|
+
// applies to it, then (optionally) attach the toolbar.
|
|
443
|
+
// The toolbar lookup is silent if the template is missing
|
|
444
|
+
// (e.g. in unit tests), so the controller still works in
|
|
445
|
+
// every environment.
|
|
446
|
+
ensureZoomStage(container);
|
|
447
|
+
attachZoomToolbar(container);
|
|
277
448
|
// Bind functions if mermaid provided any (e.g., for click handlers).
|
|
278
449
|
if (typeof result.bindFunctions === "function") {
|
|
279
450
|
try {
|
|
@@ -300,6 +471,15 @@
|
|
|
300
471
|
if (onStateChange) onStateChange(container, "error");
|
|
301
472
|
},
|
|
302
473
|
getState,
|
|
474
|
+
// v2.0.8 (feat/mermaid-zoom): zoom controls. Each method
|
|
475
|
+
// takes a `container` reference so the controller can be
|
|
476
|
+
// re-used across multiple diagrams on the same page. The
|
|
477
|
+
// zoom is applied to a lazy-created `.mermaid-zoom-stage`
|
|
478
|
+
// wrapper inside the container.
|
|
479
|
+
getZoom,
|
|
480
|
+
zoomIn,
|
|
481
|
+
zoomOut,
|
|
482
|
+
resetZoom,
|
|
303
483
|
destroy() {
|
|
304
484
|
controllerDestroyed = true;
|
|
305
485
|
// Mark every known container as destroyed so any in-flight render
|
package/public/style.css
CHANGED
|
@@ -384,6 +384,64 @@ table.toolcalls tr.tool-row.highlight:hover {
|
|
|
384
384
|
white-space: pre-wrap;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
/* v2.0.8 (feat/mermaid-zoom): zoom toolbar + stage styles.
|
|
388
|
+
The toolbar is a small inline control set the user clicks to
|
|
389
|
+
scale the rendered diagram up or down. The .mermaid-zoom-stage
|
|
390
|
+
wrapper is what carries the CSS transform — the underlying
|
|
391
|
+
SVG keeps its own viewBox so the diagram does not reflow on
|
|
392
|
+
every zoom step. */
|
|
393
|
+
.mermaid-zoom-toolbar {
|
|
394
|
+
display: inline-flex;
|
|
395
|
+
align-items: center;
|
|
396
|
+
gap: 4px;
|
|
397
|
+
margin: 4px 0;
|
|
398
|
+
padding: 2px 6px;
|
|
399
|
+
background: var(--panel, #f8fafc);
|
|
400
|
+
border: 1px solid var(--border, #cbd5e1);
|
|
401
|
+
border-radius: 4px;
|
|
402
|
+
font-size: 12px;
|
|
403
|
+
user-select: none;
|
|
404
|
+
}
|
|
405
|
+
.mermaid-zoom-btn {
|
|
406
|
+
display: inline-flex;
|
|
407
|
+
align-items: center;
|
|
408
|
+
justify-content: center;
|
|
409
|
+
min-width: 28px;
|
|
410
|
+
height: 24px;
|
|
411
|
+
padding: 0 6px;
|
|
412
|
+
background: #fff;
|
|
413
|
+
border: 1px solid var(--border, #cbd5e1);
|
|
414
|
+
border-radius: 3px;
|
|
415
|
+
font-family: inherit;
|
|
416
|
+
font-size: 12px;
|
|
417
|
+
color: #0f172a;
|
|
418
|
+
cursor: pointer;
|
|
419
|
+
transition: background 0.1s ease-in-out;
|
|
420
|
+
}
|
|
421
|
+
.mermaid-zoom-btn:hover {
|
|
422
|
+
background: #e2e8f0;
|
|
423
|
+
}
|
|
424
|
+
.mermaid-zoom-btn:focus-visible {
|
|
425
|
+
outline: 2px solid #2563eb;
|
|
426
|
+
outline-offset: 1px;
|
|
427
|
+
}
|
|
428
|
+
.mermaid-zoom-btn:active {
|
|
429
|
+
background: #cbd5e1;
|
|
430
|
+
}
|
|
431
|
+
.mermaid-zoom-stage {
|
|
432
|
+
/* The wrapper that carries the transform. `transform-origin:
|
|
433
|
+
top left` ensures the diagram grows down-right rather than
|
|
434
|
+
re-centering (which would look jumpy). The transform itself
|
|
435
|
+
is set inline by `MermaidRenderer.applyZoom` so the JS
|
|
436
|
+
state is the single source of truth. */
|
|
437
|
+
display: inline-block;
|
|
438
|
+
transform-origin: top left;
|
|
439
|
+
transition: transform 0.12s ease-out;
|
|
440
|
+
/* Allow the zoomed content to overflow horizontally; the
|
|
441
|
+
surrounding container will get its own scroll bar if needed. */
|
|
442
|
+
max-width: 100%;
|
|
443
|
+
}
|
|
444
|
+
|
|
387
445
|
.hint {
|
|
388
446
|
margin-top: 8px;
|
|
389
447
|
font-size: 12px;
|
|
@@ -437,6 +495,51 @@ details summary {
|
|
|
437
495
|
font-weight: normal;
|
|
438
496
|
}
|
|
439
497
|
|
|
498
|
+
/* ------------------------------------------------------------------------- */
|
|
499
|
+
/* v2.0.9: collapse/expand toggle for pipeline + lifecycle sections */
|
|
500
|
+
/* ------------------------------------------------------------------------- */
|
|
501
|
+
|
|
502
|
+
.collapse-toggle {
|
|
503
|
+
display: inline-flex;
|
|
504
|
+
align-items: center;
|
|
505
|
+
justify-content: center;
|
|
506
|
+
width: 24px;
|
|
507
|
+
height: 24px;
|
|
508
|
+
margin-left: 8px;
|
|
509
|
+
padding: 0;
|
|
510
|
+
border: 1px solid var(--border);
|
|
511
|
+
border-radius: 4px;
|
|
512
|
+
background: var(--panel-2);
|
|
513
|
+
color: var(--text);
|
|
514
|
+
font-size: 12px;
|
|
515
|
+
line-height: 1;
|
|
516
|
+
cursor: pointer;
|
|
517
|
+
vertical-align: middle;
|
|
518
|
+
transition: background 0.15s ease, border-color 0.15s ease;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.collapse-toggle:hover {
|
|
522
|
+
background: var(--bg);
|
|
523
|
+
border-color: var(--muted);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.collapse-toggle:focus-visible {
|
|
527
|
+
outline: 2px solid var(--accent, #4a9);
|
|
528
|
+
outline-offset: 1px;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.toggle-icon {
|
|
532
|
+
display: inline-block;
|
|
533
|
+
font-size: 10px;
|
|
534
|
+
line-height: 1;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/* When a section has the `collapsed` class, hide its body container. */
|
|
538
|
+
[data-pipeline-root].collapsed [data-pipeline-body],
|
|
539
|
+
[data-lifecycle-root].collapsed [data-lifecycle-body] {
|
|
540
|
+
display: none;
|
|
541
|
+
}
|
|
542
|
+
|
|
440
543
|
.op-timeline {
|
|
441
544
|
list-style: none;
|
|
442
545
|
margin: 0;
|
|
@@ -79,6 +79,15 @@
|
|
|
79
79
|
* length of description + processDescription exceeds 600 chars
|
|
80
80
|
* (long descriptions should not blow the panel up vertically).
|
|
81
81
|
*/
|
|
82
|
+
/**
|
|
83
|
+
* v2.0.9: render the collapse/expand toggle button. Matches the
|
|
84
|
+
* server-side `renderPipelineToggle()` output so swapIn re-renders
|
|
85
|
+
* stay consistent.
|
|
86
|
+
*/
|
|
87
|
+
function renderPipelineToggle() {
|
|
88
|
+
return ' <button type="button" class="collapse-toggle" data-pipeline-toggle aria-expanded="true" aria-label="Collapse pipeline"><span class="toggle-icon">▼</span></button>';
|
|
89
|
+
}
|
|
90
|
+
|
|
82
91
|
function renderPipelineHtml(ops, stale, taskId) {
|
|
83
92
|
var nodes = (ops || []).map(function (op) {
|
|
84
93
|
var label = statusLabel(op.milestoneType);
|
|
@@ -123,19 +132,20 @@
|
|
|
123
132
|
}).join("");
|
|
124
133
|
|
|
125
134
|
var badge = renderSseBadge(stale, currentSseState);
|
|
135
|
+
var toggle = renderPipelineToggle();
|
|
126
136
|
var idAttr = taskId ? ' data-task-id="' + esc(taskId) + '"' : "";
|
|
127
137
|
if (!ops || ops.length === 0) {
|
|
128
138
|
return (
|
|
129
139
|
'<section class="panel panel-pipeline"' + idAttr + ' data-pipeline-root>' +
|
|
130
|
-
'<h2>Task lifecycle pipeline' + badge + '</h2>' +
|
|
131
|
-
'<p class="empty">No operations recorded yet.</p>' +
|
|
140
|
+
'<h2>Task lifecycle pipeline' + badge + toggle + '</h2>' +
|
|
141
|
+
'<div data-pipeline-body><p class="empty">No operations recorded yet.</p></div>' +
|
|
132
142
|
'</section>'
|
|
133
143
|
);
|
|
134
144
|
}
|
|
135
145
|
return (
|
|
136
146
|
'<section class="panel panel-pipeline"' + idAttr + ' data-pipeline-root>' +
|
|
137
|
-
'<h2>Task lifecycle pipeline' + badge + '</h2>' +
|
|
138
|
-
'<ol class="op-timeline" role="list">' + nodes + '</ol>' +
|
|
147
|
+
'<h2>Task lifecycle pipeline' + badge + toggle + '</h2>' +
|
|
148
|
+
'<div data-pipeline-body><ol class="op-timeline" role="list">' + nodes + '</ol></div>' +
|
|
139
149
|
'</section>'
|
|
140
150
|
);
|
|
141
151
|
}
|
|
@@ -168,10 +178,11 @@
|
|
|
168
178
|
|
|
169
179
|
function renderErrorHtml(code, taskId) {
|
|
170
180
|
var idAttr = taskId ? ' data-task-id="' + esc(taskId) + '"' : "";
|
|
181
|
+
var toggle = renderPipelineToggle();
|
|
171
182
|
return (
|
|
172
183
|
'<section class="panel panel-pipeline"' + idAttr + ' data-pipeline-error="' + esc(code) + '" data-pipeline-root>' +
|
|
173
|
-
'<h2>Task lifecycle pipeline</h2>' +
|
|
174
|
-
'<div class="pipeline-error">' +
|
|
184
|
+
'<h2>Task lifecycle pipeline' + toggle + '</h2>' +
|
|
185
|
+
'<div class="pipeline-error" data-pipeline-body>' +
|
|
175
186
|
'<p>Could not load operations: <code>' + esc(code) + '</code></p>' +
|
|
176
187
|
'<button type="button" class="btn btn-retry" data-pipeline-retry>Retry</button>' +
|
|
177
188
|
'</div>' +
|
|
@@ -184,16 +195,31 @@
|
|
|
184
195
|
return document.querySelector('[data-pipeline-root]');
|
|
185
196
|
}
|
|
186
197
|
|
|
187
|
-
/** Replace the placeholder with new HTML, preserving the scroll position.
|
|
198
|
+
/** Replace the placeholder with new HTML, preserving the scroll position.
|
|
199
|
+
*
|
|
200
|
+
* v2.0.9: also restores the collapsed state from localStorage so
|
|
201
|
+
* SSE re-renders don't lose the user's collapse/expand preference.
|
|
202
|
+
*/
|
|
188
203
|
function swapIn(newHtml) {
|
|
189
204
|
var root = findRoot();
|
|
190
205
|
if (!root) return;
|
|
206
|
+
var wasCollapsed = root.classList.contains("collapsed");
|
|
191
207
|
var scrollY = window.scrollY;
|
|
192
208
|
var tmp = document.createElement("div");
|
|
193
209
|
tmp.innerHTML = newHtml;
|
|
194
210
|
var newEl = tmp.firstElementChild;
|
|
195
211
|
if (!newEl) return;
|
|
196
212
|
root.parentNode.replaceChild(newEl, root);
|
|
213
|
+
// Restore collapsed state after swap-in
|
|
214
|
+
if (wasCollapsed) {
|
|
215
|
+
newEl.classList.add("collapsed");
|
|
216
|
+
var btn = newEl.querySelector("[data-pipeline-toggle]");
|
|
217
|
+
if (btn) {
|
|
218
|
+
btn.setAttribute("aria-expanded", "false");
|
|
219
|
+
var icon = btn.querySelector(".toggle-icon");
|
|
220
|
+
if (icon) icon.textContent = "▶";
|
|
221
|
+
}
|
|
222
|
+
}
|
|
197
223
|
// Don't restore scrollY unless the user has actually scrolled
|
|
198
224
|
if (scrollY > 0) window.scrollTo(0, scrollY);
|
|
199
225
|
}
|
|
@@ -431,6 +457,118 @@
|
|
|
431
457
|
if (this.timer) clearTimeout(this.timer);
|
|
432
458
|
};
|
|
433
459
|
|
|
460
|
+
// ─── v2.0.9: collapse/expand toggle ──────────────────────────────
|
|
461
|
+
|
|
462
|
+
/** localStorage keys for persisting collapse state. */
|
|
463
|
+
var PIPELINE_COLLAPSE_KEY = "roy-task-show:pipeline-collapsed";
|
|
464
|
+
var LIFECYCLE_COLLAPSE_KEY = "roy-task-show:lifecycle-collapsed";
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* v2.0.9: Apply the collapsed/expanded state to a section element.
|
|
468
|
+
* Toggles the `collapsed` CSS class, updates `aria-expanded` on the
|
|
469
|
+
* toggle button, and swaps the ▼/▶ icon.
|
|
470
|
+
*/
|
|
471
|
+
function applyCollapseState(section, toggleBtn, collapsed) {
|
|
472
|
+
if (!section || !toggleBtn) return;
|
|
473
|
+
var icon = toggleBtn.querySelector(".toggle-icon");
|
|
474
|
+
if (collapsed) {
|
|
475
|
+
section.classList.add("collapsed");
|
|
476
|
+
toggleBtn.setAttribute("aria-expanded", "false");
|
|
477
|
+
if (icon) icon.textContent = "▶";
|
|
478
|
+
} else {
|
|
479
|
+
section.classList.remove("collapsed");
|
|
480
|
+
toggleBtn.setAttribute("aria-expanded", "true");
|
|
481
|
+
if (icon) icon.textContent = "▼";
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* v2.0.9: Wire up the collapse/expand toggle for a section.
|
|
487
|
+
*
|
|
488
|
+
* @param {string} sectionSelector — e.g. '[data-pipeline-root]'
|
|
489
|
+
* @param {string} toggleSelector — e.g. '[data-pipeline-toggle]'
|
|
490
|
+
* @param {string} storageKey — localStorage key for persistence
|
|
491
|
+
*/
|
|
492
|
+
function wireCollapseToggle(sectionSelector, toggleSelector, storageKey) {
|
|
493
|
+
var sections = document.querySelectorAll(sectionSelector);
|
|
494
|
+
for (var i = 0; i < sections.length; i++) {
|
|
495
|
+
(function (section) {
|
|
496
|
+
var toggleBtn = section.querySelector(toggleSelector);
|
|
497
|
+
if (!toggleBtn) return;
|
|
498
|
+
// Restore saved preference on load
|
|
499
|
+
var saved = null;
|
|
500
|
+
try {
|
|
501
|
+
saved = localStorage.getItem(storageKey);
|
|
502
|
+
} catch (_) { /* localStorage may be unavailable */ }
|
|
503
|
+
if (saved === "true") {
|
|
504
|
+
applyCollapseState(section, toggleBtn, true);
|
|
505
|
+
}
|
|
506
|
+
// Click handler
|
|
507
|
+
toggleBtn.addEventListener("click", function (e) {
|
|
508
|
+
e.preventDefault();
|
|
509
|
+
e.stopPropagation();
|
|
510
|
+
var collapsed = !section.classList.contains("collapsed");
|
|
511
|
+
applyCollapseState(section, toggleBtn, collapsed);
|
|
512
|
+
try {
|
|
513
|
+
localStorage.setItem(storageKey, String(collapsed));
|
|
514
|
+
} catch (_) { /* ignore quota / privacy errors */ }
|
|
515
|
+
});
|
|
516
|
+
})(sections[i]);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* v2.0.9: Initialize collapse toggles. Pipeline uses event delegation
|
|
522
|
+
* (initPipelineToggleDelegation) because swapIn replaces it on every
|
|
523
|
+
* SSE update. Lifecycle section is never re-rendered by swapIn, so
|
|
524
|
+
* direct binding is fine. This function also restores saved
|
|
525
|
+
* preferences for BOTH sections on initial load.
|
|
526
|
+
*/
|
|
527
|
+
function initCollapseToggles() {
|
|
528
|
+
// Restore pipeline saved state (the delegation handler manages
|
|
529
|
+
// clicks, but initial state restoration must happen here).
|
|
530
|
+
var pipelineRoot = document.querySelector("[data-pipeline-root]");
|
|
531
|
+
if (pipelineRoot) {
|
|
532
|
+
var pipelineToggle = pipelineRoot.querySelector("[data-pipeline-toggle]");
|
|
533
|
+
if (pipelineToggle) {
|
|
534
|
+
var pipelineSaved = null;
|
|
535
|
+
try { pipelineSaved = localStorage.getItem(PIPELINE_COLLAPSE_KEY); } catch (_) {}
|
|
536
|
+
if (pipelineSaved === "true") {
|
|
537
|
+
applyCollapseState(pipelineRoot, pipelineToggle, true);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// Lifecycle section: direct binding + restore
|
|
542
|
+
wireCollapseToggle("[data-lifecycle-root]", "[data-lifecycle-toggle]", LIFECYCLE_COLLAPSE_KEY);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* v2.0.9: Document-level event delegation for pipeline toggle clicks.
|
|
547
|
+
* Since swapIn replaces the pipeline section on every SSE update,
|
|
548
|
+
* per-element listeners are lost. This delegated handler catches
|
|
549
|
+
* clicks on any current or future [data-pipeline-toggle] button.
|
|
550
|
+
*/
|
|
551
|
+
function initPipelineToggleDelegation() {
|
|
552
|
+
document.addEventListener("click", function (e) {
|
|
553
|
+
var t = e.target;
|
|
554
|
+
// Walk up to find the toggle button (click may hit the icon span)
|
|
555
|
+
while (t && t !== document) {
|
|
556
|
+
if (t.matches && t.matches("[data-pipeline-toggle]")) break;
|
|
557
|
+
t = t.parentNode;
|
|
558
|
+
}
|
|
559
|
+
if (!t || !t.matches || !t.matches("[data-pipeline-toggle]")) return;
|
|
560
|
+
e.preventDefault();
|
|
561
|
+
e.stopPropagation();
|
|
562
|
+
var section = t.closest("[data-pipeline-root]");
|
|
563
|
+
if (!section) return;
|
|
564
|
+
var collapsed = !section.classList.contains("collapsed");
|
|
565
|
+
applyCollapseState(section, t, collapsed);
|
|
566
|
+
try {
|
|
567
|
+
localStorage.setItem(PIPELINE_COLLAPSE_KEY, String(collapsed));
|
|
568
|
+
} catch (_) { /* ignore */ }
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
434
572
|
function boot() {
|
|
435
573
|
var root = findRoot();
|
|
436
574
|
if (!root) return;
|
|
@@ -448,6 +586,11 @@
|
|
|
448
586
|
// tool.called events patch the pipeline DOM in place without
|
|
449
587
|
// waiting for the next polling tick.
|
|
450
588
|
window.__pipelineController.attachSse();
|
|
589
|
+
// v2.0.9: wire collapse/expand toggles for pipeline + lifecycle.
|
|
590
|
+
// Pipeline uses event delegation (survives swapIn re-renders);
|
|
591
|
+
// lifecycle uses direct binding (never re-rendered by swapIn).
|
|
592
|
+
initPipelineToggleDelegation();
|
|
593
|
+
initCollapseToggles();
|
|
451
594
|
}
|
|
452
595
|
|
|
453
596
|
// v0.9.0: expose renderPipelineHtml to the global scope so other
|
|
@@ -159,6 +159,64 @@
|
|
|
159
159
|
// Render
|
|
160
160
|
// -----------------------------------------------------------------------
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* v2.0.8 (fix/tool-call-loading-skeleton): remove the
|
|
164
|
+
* `.monaco-skeleton` element (and its `.monaco-skeleton-hint`
|
|
165
|
+
* child) from `container` so the animated loading block
|
|
166
|
+
* does NOT remain as a sibling of the mounted editor or
|
|
167
|
+
* fallback. Without this, the success branch
|
|
168
|
+
* (`monaco.editor.create(container, ...)`) leaves the
|
|
169
|
+
* skeleton in place because Monaco ADDS the editor widget
|
|
170
|
+
* to the container rather than replacing its contents. The
|
|
171
|
+
* fallback and error branches happen to clear the container
|
|
172
|
+
* via `innerHTML = "..."`, but calling this helper from
|
|
173
|
+
* every branch is the single source of truth — the
|
|
174
|
+
* `innerHTML = "..."` calls become redundant (and we drop
|
|
175
|
+
* them) so there is exactly one place that owns the
|
|
176
|
+
* "skeleton is gone now" invariant.
|
|
177
|
+
*
|
|
178
|
+
* Side effect: emits
|
|
179
|
+
* console.debug("[trace] tool-call.loading.hide", { filePath, branch })
|
|
180
|
+
* so the host OTel pipeline and the visual-verify Playwright
|
|
181
|
+
* script can observe the trace path. `branch` is one of
|
|
182
|
+
* "success" — Monaco editor is now mounted
|
|
183
|
+
* "fallback" — Monaco timed out, `<pre>` fallback rendered
|
|
184
|
+
* "error" — fetch / Monaco both failed
|
|
185
|
+
*
|
|
186
|
+
* The function is intentionally idempotent and side-effect-
|
|
187
|
+
* free aside from the trace log: calling it twice on the
|
|
188
|
+
* same container is a no-op the second time.
|
|
189
|
+
*
|
|
190
|
+
* @param {HTMLElement|null} container
|
|
191
|
+
* @param {"success"|"fallback"|"error"} branch
|
|
192
|
+
*/
|
|
193
|
+
function hideLoadingSkeleton(container, branch) {
|
|
194
|
+
if (!container || !container.querySelector) return;
|
|
195
|
+
const skeleton = container.querySelector(".monaco-skeleton");
|
|
196
|
+
if (skeleton && skeleton.parentNode) {
|
|
197
|
+
skeleton.parentNode.removeChild(skeleton);
|
|
198
|
+
}
|
|
199
|
+
// v2.0.3 also injected a `.monaco-skeleton-hint` text node —
|
|
200
|
+
// in the success branch this is the *only* visible leftover
|
|
201
|
+
// (the skeleton div itself is removed as part of the editor
|
|
202
|
+
// mount, but the hint text node is a separate element). Belt-
|
|
203
|
+
// and-braces: drop the hint too if it's still around.
|
|
204
|
+
const hint = container.querySelector(".monaco-skeleton-hint");
|
|
205
|
+
if (hint && hint.parentNode) {
|
|
206
|
+
hint.parentNode.removeChild(hint);
|
|
207
|
+
}
|
|
208
|
+
const filePath =
|
|
209
|
+
(container.getAttribute && container.getAttribute("data-file-path")) ||
|
|
210
|
+
null;
|
|
211
|
+
try {
|
|
212
|
+
if (typeof console !== "undefined" && typeof console.debug === "function") {
|
|
213
|
+
console.debug("[trace] tool-call.loading.hide", { filePath, branch });
|
|
214
|
+
}
|
|
215
|
+
} catch (_) {
|
|
216
|
+
// ignore — tracing must never break the controller
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
162
220
|
/**
|
|
163
221
|
* v2.0.4 (Task #2690): hydrate a single monaco container with the
|
|
164
222
|
* skeleton + load + fallback lifecycle. Used by BOTH the dedicated
|
|
@@ -206,16 +264,31 @@
|
|
|
206
264
|
'<div class="monaco-placeholder monaco-error">Monaco failed to load and the file is not accessible: <code>' +
|
|
207
265
|
escapeHtml(filePath) + '</code></div>';
|
|
208
266
|
}
|
|
267
|
+
// v2.0.8: explicitly drop the skeleton before the innerHTML
|
|
268
|
+
// swap completes — guarantees the trace fires even if
|
|
269
|
+
// someone later refactors the innerHTML to a different
|
|
270
|
+
// string. The redundant removal is cheap (no DOM match
|
|
271
|
+
// in the success branch → no-op).
|
|
272
|
+
hideLoadingSkeleton(monacoContainer, file ? "fallback" : "error");
|
|
209
273
|
return;
|
|
210
274
|
}
|
|
211
275
|
if (!file) {
|
|
212
276
|
monacoContainer.innerHTML = `<div class="monaco-placeholder monaco-error">File not accessible (sandboxed or missing): <code>${escapeHtml(filePath)}</code></div>`;
|
|
277
|
+
hideLoadingSkeleton(monacoContainer, "error");
|
|
213
278
|
return;
|
|
214
279
|
}
|
|
215
280
|
const language = monacoContainer.getAttribute("data-language") || "plaintext";
|
|
216
281
|
// v2.0.3: use the current theme (vs-dark for dark, vs for light).
|
|
217
282
|
// Bug E fix — the default 'vs' is white-on-grey and unreadable on
|
|
218
283
|
// dark pages.
|
|
284
|
+
// v2.0.8 (fix/tool-call-loading-skeleton): drop the
|
|
285
|
+
// skeleton BEFORE `monaco.editor.create` so the editor
|
|
286
|
+
// mounts into a clean container. Monaco's `create()` does
|
|
287
|
+
// NOT clear the container's children, so without this
|
|
288
|
+
// step the animated skeleton would stay as a sibling of
|
|
289
|
+
// the editor widget and the user would see "Loading
|
|
290
|
+
// /path/…" stuck on screen.
|
|
291
|
+
hideLoadingSkeleton(monacoContainer, "success");
|
|
219
292
|
const editor = monaco.editor.create(monacoContainer, {
|
|
220
293
|
value: file.content,
|
|
221
294
|
language,
|
|
@@ -245,6 +318,7 @@
|
|
|
245
318
|
}
|
|
246
319
|
} catch (err) {
|
|
247
320
|
monacoContainer.innerHTML = `<div class="monaco-placeholder monaco-error">Monaco failed to load: ${escapeHtml(String((err && err.message) || err))}</div>`;
|
|
321
|
+
hideLoadingSkeleton(monacoContainer, "error");
|
|
248
322
|
}
|
|
249
323
|
}
|
|
250
324
|
|