@ai-setting/roy-plugin-task-show 2.0.7 → 2.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-setting/roy-plugin-task-show",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "roy-agent plugin: visualize task solving process via tool call flow on a local web service",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-setting/roy-plugin-task-show",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "type": "tool-plugin",
5
5
  "description": "v2.0.0: Major visual overhaul of the per-task detail page \u2014 VS Code-style tool-call browser powered by Monaco Editor + LCS-based diff + project file tree with mermaid-driven navigation. The detail page now renders a 2-column layout: a sticky left sidebar with the project's git-tracked file tree (chevron-toggle directories, `is-affected` highlight for files touched by the current task), and a main column with the existing Mermaid + stats + toolcalls table + a new dedicated `<section id=\"tool-call-detail-panel\">` that hosts the LCS diff body + Monaco container for the currently-selected tool call. The legacy inline `<details class=\"diff-panel\">` (naive split-and-filter, prone to mis-tagging context lines) is replaced by `<ol class=\"diff-body\">` with proper added/removed/context markers driven by a real LCS dynamic-programming table (`computeDiff()` in `src/tool-call-detail.ts`). Tool calls with a file path get a Monaco Editor placeholder (`<div class=\"monaco-editor\" data-file-path=\"...\" data-language=\"...\">`) which the client-side `public/tool-call-detail.js` lazy-loads from `cdn.jsdelivr.net/npm/monaco-editor@0.45.0` the first time the user clicks a tool row. The Mermaid `__toolClick(toolId)` callback now drives THREE things: (a) the existing row scroll + highlight (preserved from v1.x), (b) the dedicated detail panel re-renders with the matching call, (c) the file-tree sidebar highlights the corresponding file (when `data-file-path` is present). New `GET /api/task/:id/file-tree` endpoint serves the git-tracked file list (`{ files: string[] }`) with a 30-second in-memory TTL + 8-entry FIFO bound. New modules: `src/file-tree.ts` (pure data layer: `buildFileTree / extractAffectedPaths / findNodeByPath / collectAllPaths / gitLsFiles / parseLsFiles`), `src/tool-call-detail.ts` (SSR + LCS diff + HTML escaping), `public/file-tree.js` (vanilla JS hydrator with chevron toggle + keyboard navigation + scrollIntoView), `public/tool-call-detail.js` (Monaco AMD loader + `__toolClick` wrapper + file-content fetch). New tests: `test/file-tree.test.ts` (21 cases \u2014 empty/single/nested/dedup/sort/depth/parseLsFiles/gitLsFiles), `test/tool-call-detail.test.ts` (22 cases \u2014 LCS diff edges, HTML escape, path aliases, summary stats), `test/tool-call-detail-server-integration.test.ts` (7 cases \u2014 SSR HTML contracts + endpoint), `test/tool-call-detail-jsdom.test.ts` (7 cases \u2014 client-side hydrators). v1.2.0: CSS context & packaged release hotfix. The plugin's public assets (notably `public/style.css`) and runtime adapters now correctly resolve relative to the installed package directory even when consumed via the published npm tarball. The session-scoped `TaskSessionStore` now preserves the full host session context (parent-child task links, plugin-handle id, env scope) across render cycles \u2014 previously the session was collapsed to its `sessionId` on first load and never refreshed, so the home page lost the \u300csession ancestors\u300d chain and external tasks from outside the current session silently disappeared from the tree. Adds `src/task-metadata.ts` as the single source of truth for the public `Task` shape exposed by `/api/tasks` + `/api/tasks/:id`, including the v1.0.0+ `processDescription` field, and re-exports it through the CLI adapters (`cli-tasks-adapter.ts` + `cli-tasks-tree-adapter.ts`) so the home page tree + the per-task page render against the same metadata contract. Bundles 244-line regression test (`test/context-and-packed-release.test.ts`) that boots the plugin from the **npm-pack** directory (not the repo working tree), spawns `roy-agent tasks get <id> --json`, and asserts (a) `public/style.css` is present and \u2265 64 lines, (b) the `/api/events` SSE endpoint survives a reload, and (c) `task.session` survives a render cycle. v1.1.0: Full Server-Sent Events realtime subscription across 3 event classes (task.created / operation.updated / tool.called) on both the home page and the per-task /task/<id> page. The per-task pipeline now subscribes to /api/events and patches the DOM in place on operation.updated \u2014 no more 5s-poll delay before the user sees a new milestone. The 'Task lifecycle pipeline' header badge is replaced by a 5-state SSE-aware badge (stale / connecting / live / reconnecting / error) so the user can tell at a glance whether real-time updates are flowing, the connection dropped, or 3+ consecutive errors triggered the polling fallback. Legacy boolean `stale` cache-TTL pill and `tool.recorded` event name are preserved for back-compat with v0.9.x / v1.0.0 clients. v1.0.0: First stable release. Replaces the v0.9.x fixture-based verify scripts (which built fake TaskOperationsEnvelope and never invoked the real `roy-agent` CLI, masking regressions in the public-schema `processDescription` field) with a real-CLI scenario test + verify (`test/process-description-real-scenario.test.ts` + `scripts/verify-v100-real-scenario.ts`) that spawns `roy-agent tasks get <id> --operations --json` via `defaultRunner` and asserts the API response carries `processDescription` end-to-end. The 0.9.9 processDescription fix is preserved verbatim \u2014 this release only swaps the verify surface. Visualize the tool call chain of a task on a local web service with real-time SSE updates. v0.9.9: Task lifecycle pipeline on /task/<id> now exposes BOTH the milestone badge AND the \u300c\u8fc7\u7a0b\u63cf\u8ff0\u300d column at a glance \u2014 the server-side `/api/tasks/:id/operations` endpoint exposes `processDescription` on every operation (no longer stripped from the public schema), the client-side `renderPipelineHtml` mirrors the server's `.op-desc-block` + `.op-proc-block` block layout so SSR \u2194 CSR stay in sync, and a long-standing CSS right-side text-truncation bug in the pipeline timeline (long CJK titles overflowing the panel edge) is fixed via `min-width: 0` on `.op-row1` + `overflow-wrap: anywhere` on `.op-title`. v0.9.0: Session-scoped home page (only show tasks created after plugin load + their external ancestors), with per-row \u300c\u663e\u793a\u5168\u90e8\u680f\u4f4d\u300d toggle and lazy-loaded operations timeline; per-task Mermaid labels now correctly render CJK / mixed-Latin / emoji text (encoded as \\uXXXX before emission, decoded by the browser); detail page layout reordered to lifecycle \u2192 pipeline \u2192 stats \u2192 toolcalls \u2192 rawjson. v0.5.0+: page refreshes stream over GET /api/events (Server-Sent Events). Subscribes to tool:before.execute, tool:after.execute, task:before.create, task:after.create, task:after.complete (preferred, 2026-07-10+), and task:after.update (legacy fallback). v0.6.11: Mermaid re-rendering is delegated to a self-contained controller (public/mermaid-renderer.js) that prevents the SVG\u2192raw-source regression on async updates and surfaces recoverable .mermaid-error states. v0.6.12: Task lifecycle pipeline (operations timeline) server now emits data-task-id on the pipeline section; client preserves it on swap, so the page actually fetches /api/tasks/<id>/operations and renders the 7-op timeline (previously silently bailed). v0.7.0: Home page redesigned as a hierarchical task tree (driven by `roy-agent tasks tree --json`); new /api/tasks/tree endpoint with status / priority / type / root-id filters, expand/collapse UI, search, and live 30s polling. v0.8.0: per-task page Mermaid area now renders the hierarchical 'Task lifecycle + tools' view \u2014 each operation record owns a subgraph that nests its tool calls, with click callbacks (`window.__toolClick`) that scroll-into-view + highlight + auto-expand the matching row in the tool-call table below. Operation record descriptions (`description` + `processDescription`) are now always rendered inline (no `<details>` collapse) so the user sees the lifecycle state at a glance; a fallback `<details>` kicks in only for descriptions longer than 600 chars. v0.8.1: hotfix for two pre-existing bugs in v0.8.0 (browser smoke test surfaced after merge). (a) Mermaid click directives were emitted as `click t1 __toolClick(1)` (missing `call` keyword) \u2014 Mermaid 10's parser rejects this with `got 'PS'`. Fixed to `click t1 call __toolClick(1)` (the v10 grammar requires `call` to invoke a callback with arguments). (b) `buildMermaidSource` lived inside the `attachTaskPageTimeline` IIFE but was also called from a listener in the `attachToolClickBridge` IIFE \u2014 sibling IIFEs cannot see each other's locals, so the listener threw `ReferenceError: buildMermaidSource is not defined` and the Mermaid diagram silently failed to re-render after `task-show:lifecycle-ops-loaded`. Fixed by hoisting the function (and its three helpers) to script top-level so both IIFEs can see it via the script-wide closure; the function is also exposed on `window.buildMermaidSource` for tests + tooling. v0.8.3: tree-display fix (Task #2426). The home page used to look like a flat list of root tasks because `autoExpandFirstLevels(..., 2)` only opened the first 2 levels \u2014 30/47 roots were leaf nodes and the remaining 17 collapsed to one level so grandchildren were never visible. Default expand depth is now 3 (root + child + grandchild + great-grandchild are visible on first paint), the summary line now shows per-depth count pills (root / child / grandchild / great-grandchild / level-N), each `tree-row` carries a `data-depth` attribute so CSS can paint coloured left rails per level, and the duplicated 'Live tool-call sessions (legacy view)' panel that made the page look like both a flat table AND a tree is now hidden behind `#legacy-sessions[hidden]` (kept for future debug-toggle restoration). v0.8.10: bug-fix release (Task #2537 + Task #2534). (a) Heap-bounded plugin caches: OperationsCache and TasksTreeCache now enforce a hard maxEntries cap (default 256 / 64). Oldest stale entries are evicted before inserting a new one, so long-lived roy-agent sessions (BackgroundTaskManager + MemorySessionStore) no longer leak Map entries through the plugin's per-task caches \u2014 see Task #2537 for the heap-unbounded-state RED\u2192GREEN repro. (b) Mermaid CJK font-family: server.ts renderTaskPage now configures mermaid.initialize({ themeVariables: { fontFamily: '\"PingFang SC\", \"Microsoft YaHei\", \"Noto Sans CJK SC\", \"Source Han Sans CN\", \"WenQuanYi Micro Hei\", sans-serif' } }) so Chinese node labels render correctly in browsers that have at least one of those fonts installed (see Task #2534).",
6
6
  "main": "dist/index.js",
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;
@@ -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