@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/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