@ai-setting/roy-plugin-task-show 1.2.0 → 2.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-setting/roy-plugin-task-show",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
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,8 +1,8 @@
1
1
  {
2
2
  "name": "@ai-setting/roy-plugin-task-show",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "type": "tool-plugin",
5
- "description": "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 — previously the session was collapsed to its `sessionId` on first load and never refreshed, so the home page lost the 「session ancestors」 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 ≥ 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 — 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 — 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 「过程描述」 column at a glance — 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 ↔ 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 「显示全部栏位」 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 → pipeline → stats → toolcalls → 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→raw-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 — 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) — 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 — 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 — 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 — see Task #2537 for the heap-unbounded-state RED→GREEN 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).",
5
+ "description": "v2.0.0: Major visual overhaul of the per-task detail page — 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 — empty/single/nested/dedup/sort/depth/parseLsFiles/gitLsFiles), `test/tool-call-detail.test.ts` (22 cases — LCS diff edges, HTML escape, path aliases, summary stats), `test/tool-call-detail-server-integration.test.ts` (7 cases — SSR HTML contracts + endpoint), `test/tool-call-detail-jsdom.test.ts` (7 cases — 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 — previously the session was collapsed to its `sessionId` on first load and never refreshed, so the home page lost the 「session ancestors」 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 ≥ 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 — 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 — 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 「过程描述」 column at a glance — 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 ↔ 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 「显示全部栏位」 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 → pipeline → stats → toolcalls → 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→raw-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 — 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) — 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 — 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 — 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 — see Task #2537 for the heap-unbounded-state RED→GREEN 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",
7
7
  "hooks": [
8
8
  {
@@ -0,0 +1,210 @@
1
+ /* v2.0.0: file-tree sidebar hydrator.
2
+ *
3
+ * Loads the project file list from the data-files JSON attribute on the
4
+ * SSR-rendered `<ul id="file-tree">` and wires up:
5
+ * - chevron toggle (▶ / ▼) for directories
6
+ * - "expand all / collapse all" controls
7
+ * - click-to-highlight a file node (calls window.setActiveToolCallFile)
8
+ * - scrolling a node into view when window.__toolClick is fired
9
+ *
10
+ * Designed to be loaded AFTER app.js (so it can hook __toolClick) but
11
+ * BEFORE tool-call-detail.js (so that hydrator can call our
12
+ * setActiveToolCallFile when a Mermaid node is clicked).
13
+ *
14
+ * No dependencies — pure vanilla JS, no module loader, no JSX.
15
+ */
16
+
17
+ (function attachFileTreeHydrator() {
18
+ const SIDEBAR_SELECTOR = "#file-tree";
19
+
20
+ // -----------------------------------------------------------------------
21
+ // State
22
+ // -----------------------------------------------------------------------
23
+ /** @type {HTMLElement|null} */
24
+ let root = null;
25
+ /** @type {Set<string>} */
26
+ let affectedSet = new Set();
27
+ /** @type {string|null} */
28
+ let activePath = null;
29
+
30
+ // -----------------------------------------------------------------------
31
+ // Helpers
32
+ // -----------------------------------------------------------------------
33
+
34
+ /**
35
+ * Toggle visibility of a directory's children.
36
+ * @param {HTMLElement} li
37
+ */
38
+ function toggleDir(li) {
39
+ const isOpen = li.getAttribute("aria-expanded") === "true";
40
+ const next = !isOpen;
41
+ li.setAttribute("aria-expanded", String(next));
42
+ const chev = li.querySelector(".file-tree-chevron");
43
+ if (chev) chev.textContent = next ? "▼" : "▶";
44
+ // Update descendant visibility by toggling the `data-collapsed` flag on
45
+ // every sibling immediately under this directory.
46
+ const depth = Number(li.getAttribute("data-depth") || "0");
47
+ let sibling = li.nextElementSibling;
48
+ while (sibling) {
49
+ const sDepth = Number(sibling.getAttribute("data-depth") || "0");
50
+ if (sDepth <= depth) break;
51
+ if (!next) {
52
+ sibling.setAttribute("data-collapsed", "true");
53
+ } else {
54
+ sibling.removeAttribute("data-collapsed");
55
+ }
56
+ sibling = sibling.nextElementSibling;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Set the active (highlighted) file path. Called by the tool-call
62
+ * detail hydrator when the user clicks a Mermaid tool node.
63
+ * @param {string|null} path
64
+ */
65
+ function setActive(path) {
66
+ if (!root) return;
67
+ if (activePath) {
68
+ const prev = root.querySelector(`[data-path="${cssEscape(activePath)}"]`);
69
+ if (prev) prev.classList.remove("is-active");
70
+ }
71
+ activePath = path;
72
+ if (path) {
73
+ const next = root.querySelector(`[data-path="${cssEscape(path)}"]`);
74
+ if (next) {
75
+ next.classList.add("is-active");
76
+ // Expand parent directories so the active node is visible.
77
+ expandAncestors(next);
78
+ // Scroll into view (block: 'center') — but only if it's not
79
+ // already on-screen so we don't fight the user mid-scroll.
80
+ try {
81
+ next.scrollIntoView({ behavior: "smooth", block: "center" });
82
+ } catch {
83
+ try {
84
+ next.scrollIntoView();
85
+ } catch (_) {
86
+ /* noop */
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Walk up the DOM expanding every ancestor directory so the given
95
+ * node becomes visible. Cheap because the tree is at most ~10 levels
96
+ * deep in practice.
97
+ * @param {HTMLElement} leaf
98
+ */
99
+ function expandAncestors(leaf) {
100
+ let cur = leaf.previousElementSibling;
101
+ let targetDepth = Number(leaf.getAttribute("data-depth") || "0");
102
+ while (cur) {
103
+ const d = Number(cur.getAttribute("data-depth") || "0");
104
+ if (d < targetDepth) {
105
+ if (cur.getAttribute("aria-expanded") === "false") {
106
+ toggleDir(cur);
107
+ }
108
+ targetDepth = d; // keep walking
109
+ }
110
+ cur = cur.previousElementSibling;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * CSS.escape polyfill — older browsers / jsdom may lack it.
116
+ * @param {string} s
117
+ */
118
+ function cssEscape(s) {
119
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
120
+ return CSS.escape(s);
121
+ }
122
+ return String(s).replace(/([^a-zA-Z0-9_-])/g, "\\$1");
123
+ }
124
+
125
+ // -----------------------------------------------------------------------
126
+ // Wiring
127
+ // -----------------------------------------------------------------------
128
+
129
+ /**
130
+ * Build the affected set from `[data-affected="true"]` nodes the SSR
131
+ * already flagged. Re-runs on each hydration so SSE-inserted nodes
132
+ * stay highlighted.
133
+ */
134
+ function rebuildAffectedSet() {
135
+ if (!root) return;
136
+ affectedSet = new Set();
137
+ root.querySelectorAll('[data-affected="true"]').forEach((el) => {
138
+ const p = el.getAttribute("data-path");
139
+ if (p) affectedSet.add(p);
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Initial hydration: bind click handlers + collect the affected set.
145
+ * Idempotent — safe to call multiple times.
146
+ */
147
+ function hydrate() {
148
+ root = document.querySelector(SIDEBAR_SELECTOR);
149
+ if (!root) return;
150
+ rebuildAffectedSet();
151
+ root.addEventListener("click", (ev) => {
152
+ const target = ev.target;
153
+ // jsdom's eval'd scripts don't have `Element` in scope; use a
154
+ // duck-type check instead of `instanceof Element`.
155
+ if (!target || typeof target.getAttribute !== "function") return;
156
+ const li = target.closest && target.closest(".file-tree-node");
157
+ if (!li || typeof li.getAttribute !== "function") return;
158
+ const path = li.getAttribute("data-path") || null;
159
+ const type = li.getAttribute("data-type");
160
+ if (type === "directory") {
161
+ toggleDir(li);
162
+ // Suppress the subsequent active-path switch — directories
163
+ // aren't files, just containers.
164
+ return;
165
+ }
166
+ if (path) setActive(path);
167
+ });
168
+ // Auto-hydrate on DOMContentLoaded if the DOM is already ready.
169
+ if (document.readyState === "loading") {
170
+ document.addEventListener("DOMContentLoaded", hydrate, { once: true });
171
+ }
172
+ }
173
+ hydrate();
174
+
175
+ /**
176
+ * Expand every directory in the tree (used by the "expand all" button).
177
+ */
178
+ function expandAll() {
179
+ if (!root) return;
180
+ root.querySelectorAll('.file-tree-node[data-type="directory"]').forEach((li) => {
181
+ if (li.getAttribute("aria-expanded") === "false") toggleDir(li);
182
+ });
183
+ }
184
+
185
+ /**
186
+ * Collapse every directory (used by "collapse all").
187
+ */
188
+ function collapseAll() {
189
+ if (!root) return;
190
+ root.querySelectorAll('.file-tree-node[data-type="directory"]').forEach((li) => {
191
+ if (li.getAttribute("aria-expanded") === "true") toggleDir(li);
192
+ });
193
+ }
194
+
195
+ // Expose a tiny global API so other scripts (app.js, tool-call-detail.js)
196
+ // can drive the sidebar.
197
+ /** @type {any} */
198
+ const w = window;
199
+ w.__fileTree = {
200
+ setActive,
201
+ expandAll,
202
+ collapseAll,
203
+ /** Refresh affected-set from the current DOM (called by SSE handlers). */
204
+ refreshAffected: rebuildAffectedSet,
205
+ /** Return the currently-highlighted file path, or null. */
206
+ getActive() {
207
+ return activePath;
208
+ },
209
+ };
210
+ })();
package/public/style.css CHANGED
@@ -1154,3 +1154,505 @@ details summary {
1154
1154
  margin-bottom: 8px;
1155
1155
  }
1156
1156
  }
1157
+
1158
+ /* ============================================================================
1159
+ * v2.0.0: Monaco Editor + File Tree + Diff + Mermaid jump
1160
+ *
1161
+ * Sections (search for the header to navigate):
1162
+ * - .task-v2-layout — sidebar + main grid
1163
+ * - .file-tree-panel — left sidebar
1164
+ * - .file-tree — collapsible nested list
1165
+ * - .file-tree-node — individual rows
1166
+ * - .file-tree-chevron — directory expand/collapse
1167
+ * - .tool-call-detail — right-hand detail panel
1168
+ * - .monaco-editor — Monaco container
1169
+ * - .diff-body / .diff-line — LCS diff rendering
1170
+ * - .v2-hint — small inline hints
1171
+ * ========================================================================== */
1172
+
1173
+ /* ---------- v2.0.0 layout (sidebar + main) ---------- */
1174
+ .task-v2-layout {
1175
+ display: grid;
1176
+ grid-template-columns: minmax(220px, 280px) 1fr;
1177
+ gap: 16px;
1178
+ margin: 0 auto;
1179
+ max-width: 1400px;
1180
+ padding: 0 16px;
1181
+ }
1182
+ .task-v2-layout > .task-v2-main {
1183
+ min-width: 0; /* allow grid item to shrink */
1184
+ }
1185
+ .file-tree-panel {
1186
+ position: sticky;
1187
+ top: 12px;
1188
+ align-self: start;
1189
+ max-height: calc(100vh - 24px);
1190
+ overflow: auto;
1191
+ border: 1px solid var(--border, #e5e7eb);
1192
+ border-radius: 6px;
1193
+ padding: 12px 14px;
1194
+ background: var(--bg-panel, #fafafa);
1195
+ }
1196
+ .file-tree-panel h2 {
1197
+ font-size: 14px;
1198
+ margin: 0 0 6px;
1199
+ letter-spacing: 0.02em;
1200
+ }
1201
+ .file-tree-summary {
1202
+ font-size: 11px;
1203
+ color: var(--fg-muted, #6b7280);
1204
+ margin: 0 0 8px;
1205
+ }
1206
+ @media (max-width: 900px) {
1207
+ .task-v2-layout {
1208
+ grid-template-columns: 1fr;
1209
+ }
1210
+ .file-tree-panel {
1211
+ position: static;
1212
+ max-height: none;
1213
+ }
1214
+ }
1215
+
1216
+ /* ---------- file tree sidebar ---------- */
1217
+ .file-tree {
1218
+ list-style: none;
1219
+ margin: 0;
1220
+ padding: 0;
1221
+ font-family: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, monospace;
1222
+ font-size: 12px;
1223
+ line-height: 1.5;
1224
+ }
1225
+ .file-tree-node {
1226
+ display: flex;
1227
+ align-items: center;
1228
+ gap: 4px;
1229
+ padding: 1px 4px 1px calc(4px + var(--depth, 0) * 12px);
1230
+ border-radius: 3px;
1231
+ cursor: pointer;
1232
+ user-select: none;
1233
+ }
1234
+ .file-tree-node[data-collapsed="true"] {
1235
+ display: none;
1236
+ }
1237
+ .file-tree-node:hover {
1238
+ background: rgba(99, 102, 241, 0.08);
1239
+ }
1240
+ .file-tree-node.is-active {
1241
+ background: rgba(99, 102, 241, 0.18);
1242
+ outline: 1px solid rgba(99, 102, 241, 0.45);
1243
+ font-weight: 600;
1244
+ }
1245
+ .file-tree-node.is-affected .file-tree-name::after {
1246
+ content: " ●";
1247
+ color: rgba(220, 38, 38, 0.8);
1248
+ font-size: 10px;
1249
+ }
1250
+ .file-tree-chevron {
1251
+ display: inline-block;
1252
+ width: 12px;
1253
+ text-align: center;
1254
+ font-size: 9px;
1255
+ color: var(--fg-muted, #6b7280);
1256
+ flex: 0 0 12px;
1257
+ }
1258
+ .file-tree-spacer {
1259
+ display: inline-block;
1260
+ width: 12px;
1261
+ flex: 0 0 12px;
1262
+ }
1263
+ .file-tree-name {
1264
+ flex: 1 1 auto;
1265
+ white-space: nowrap;
1266
+ overflow: hidden;
1267
+ text-overflow: ellipsis;
1268
+ }
1269
+ .file-tree-empty {
1270
+ list-style: none;
1271
+ color: var(--fg-muted, #6b7280);
1272
+ font-style: italic;
1273
+ padding: 4px;
1274
+ }
1275
+
1276
+ /* ---------- tool-call detail panel ---------- */
1277
+ .tool-call-detail-panel {
1278
+ border-left: 4px solid rgba(99, 102, 241, 0.55);
1279
+ padding-left: 14px;
1280
+ margin-top: 18px;
1281
+ }
1282
+ .tool-call-detail-panel .v2-hint {
1283
+ font-size: 12px;
1284
+ font-weight: 400;
1285
+ color: var(--fg-muted, #6b7280);
1286
+ margin-left: 8px;
1287
+ }
1288
+ .tool-call-detail {
1289
+ border: 1px solid var(--border, #e5e7eb);
1290
+ border-radius: 6px;
1291
+ padding: 10px 12px;
1292
+ margin-top: 8px;
1293
+ background: #ffffff;
1294
+ }
1295
+ .tool-call-header {
1296
+ display: flex;
1297
+ align-items: center;
1298
+ gap: 10px;
1299
+ font-size: 13px;
1300
+ margin-bottom: 6px;
1301
+ }
1302
+ .tool-call-header .tool-name code {
1303
+ background: rgba(99, 102, 241, 0.08);
1304
+ padding: 1px 6px;
1305
+ border-radius: 3px;
1306
+ }
1307
+ .tool-call-header .tool-path {
1308
+ color: var(--fg-muted, #6b7280);
1309
+ font-family: ui-monospace, Menlo, monospace;
1310
+ font-size: 11px;
1311
+ margin-left: auto;
1312
+ overflow: hidden;
1313
+ text-overflow: ellipsis;
1314
+ white-space: nowrap;
1315
+ flex: 1 1 auto;
1316
+ }
1317
+ .tool-call-detail-empty {
1318
+ color: var(--fg-muted, #6b7280);
1319
+ font-style: italic;
1320
+ padding: 8px 0;
1321
+ }
1322
+
1323
+ /* ---------- Monaco container ---------- */
1324
+ .monaco-editor {
1325
+ width: 100%;
1326
+ height: 280px;
1327
+ border: 1px solid var(--border, #d1d5db);
1328
+ border-radius: 4px;
1329
+ margin: 6px 0 0;
1330
+ position: relative;
1331
+ background: #fafafa;
1332
+ overflow: hidden;
1333
+ }
1334
+ .monaco-editor .monaco-placeholder {
1335
+ display: flex;
1336
+ align-items: center;
1337
+ justify-content: center;
1338
+ height: 100%;
1339
+ color: var(--fg-muted, #6b7280);
1340
+ font-family: ui-monospace, Menlo, monospace;
1341
+ font-size: 12px;
1342
+ }
1343
+ .monaco-editor .monaco-error {
1344
+ color: rgba(220, 38, 38, 0.9);
1345
+ background: rgba(254, 226, 226, 0.5);
1346
+ }
1347
+ .monaco-editor .monaco-truncation-note {
1348
+ position: absolute;
1349
+ bottom: 4px;
1350
+ right: 8px;
1351
+ font-size: 10px;
1352
+ color: var(--fg-muted, #6b7280);
1353
+ background: rgba(255, 255, 255, 0.85);
1354
+ padding: 1px 6px;
1355
+ border-radius: 3px;
1356
+ }
1357
+
1358
+ /* ---------- LCS diff body ---------- */
1359
+ .diff-body {
1360
+ list-style: none;
1361
+ margin: 6px 0 0;
1362
+ padding: 6px 8px;
1363
+ border: 1px solid var(--border, #e5e7eb);
1364
+ border-radius: 4px;
1365
+ background: #fafafa;
1366
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
1367
+ font-size: 12px;
1368
+ line-height: 1.5;
1369
+ max-height: 320px;
1370
+ overflow: auto;
1371
+ }
1372
+ .diff-line {
1373
+ display: flex;
1374
+ gap: 8px;
1375
+ padding: 0 4px;
1376
+ white-space: pre;
1377
+ }
1378
+ .diff-line .diff-gutter {
1379
+ flex: 0 0 14px;
1380
+ text-align: center;
1381
+ font-weight: 600;
1382
+ }
1383
+ .diff-line.diff-line-added {
1384
+ background: rgba(220, 252, 231, 0.6);
1385
+ }
1386
+ .diff-line.diff-line-added .diff-gutter {
1387
+ color: #15803d;
1388
+ }
1389
+ .diff-line.diff-line-removed {
1390
+ background: rgba(254, 226, 226, 0.6);
1391
+ }
1392
+ .diff-line.diff-line-removed .diff-gutter {
1393
+ color: #b91c1c;
1394
+ }
1395
+ .diff-line.diff-line-context .diff-gutter {
1396
+ color: var(--fg-muted, #9ca3af);
1397
+ }
1398
+ .diff-line .diff-text {
1399
+ flex: 1 1 auto;
1400
+ white-space: pre;
1401
+ }
1402
+
1403
+ /* ---------- v2 small inline hint ---------- */
1404
+ .v2-hint {
1405
+ font-size: 11px;
1406
+ color: var(--fg-muted, #6b7280);
1407
+ }
1408
+
1409
+ /* ============================================================================
1410
+ * v2.0.0: Monaco Editor + File Tree + Diff + Mermaid jump
1411
+ *
1412
+ * Sections (search for the header to navigate):
1413
+ * - .task-v2-layout — sidebar + main grid
1414
+ * - .file-tree-panel — left sidebar
1415
+ * - .file-tree — collapsible nested list
1416
+ * - .file-tree-node — individual rows
1417
+ * - .file-tree-chevron — directory expand/collapse
1418
+ * - .tool-call-detail — right-hand detail panel
1419
+ * - .monaco-editor — Monaco container
1420
+ * - .diff-body / .diff-line — LCS diff rendering
1421
+ * - .v2-hint — small inline hints
1422
+ * ========================================================================== */
1423
+
1424
+ /* ---------- v2.0.0 layout (sidebar + main) ---------- */
1425
+ .task-v2-layout {
1426
+ display: grid;
1427
+ grid-template-columns: minmax(220px, 280px) 1fr;
1428
+ gap: 16px;
1429
+ margin: 0 auto;
1430
+ max-width: 1400px;
1431
+ padding: 0 16px;
1432
+ }
1433
+ .task-v2-layout > .task-v2-main {
1434
+ min-width: 0; /* allow grid item to shrink */
1435
+ }
1436
+ .file-tree-panel {
1437
+ position: sticky;
1438
+ top: 12px;
1439
+ align-self: start;
1440
+ max-height: calc(100vh - 24px);
1441
+ overflow: auto;
1442
+ border: 1px solid var(--border, #e5e7eb);
1443
+ border-radius: 6px;
1444
+ padding: 12px 14px;
1445
+ background: var(--bg-panel, #fafafa);
1446
+ }
1447
+ .file-tree-panel h2 {
1448
+ font-size: 14px;
1449
+ margin: 0 0 6px;
1450
+ letter-spacing: 0.02em;
1451
+ }
1452
+ .file-tree-summary {
1453
+ font-size: 11px;
1454
+ color: var(--fg-muted, #6b7280);
1455
+ margin: 0 0 8px;
1456
+ }
1457
+ @media (max-width: 900px) {
1458
+ .task-v2-layout {
1459
+ grid-template-columns: 1fr;
1460
+ }
1461
+ .file-tree-panel {
1462
+ position: static;
1463
+ max-height: none;
1464
+ }
1465
+ }
1466
+
1467
+ /* ---------- file tree sidebar ---------- */
1468
+ .file-tree {
1469
+ list-style: none;
1470
+ margin: 0;
1471
+ padding: 0;
1472
+ font-family: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, monospace;
1473
+ font-size: 12px;
1474
+ line-height: 1.5;
1475
+ }
1476
+ .file-tree-node {
1477
+ display: flex;
1478
+ align-items: center;
1479
+ gap: 4px;
1480
+ padding: 1px 4px 1px calc(4px + var(--depth, 0) * 12px);
1481
+ border-radius: 3px;
1482
+ cursor: pointer;
1483
+ user-select: none;
1484
+ }
1485
+ .file-tree-node[data-collapsed="true"] {
1486
+ display: none;
1487
+ }
1488
+ .file-tree-node:hover {
1489
+ background: rgba(99, 102, 241, 0.08);
1490
+ }
1491
+ .file-tree-node.is-active {
1492
+ background: rgba(99, 102, 241, 0.18);
1493
+ outline: 1px solid rgba(99, 102, 241, 0.45);
1494
+ font-weight: 600;
1495
+ }
1496
+ .file-tree-node.is-affected .file-tree-name::after {
1497
+ content: " ●";
1498
+ color: rgba(220, 38, 38, 0.8);
1499
+ font-size: 10px;
1500
+ }
1501
+ .file-tree-chevron {
1502
+ display: inline-block;
1503
+ width: 12px;
1504
+ text-align: center;
1505
+ font-size: 9px;
1506
+ color: var(--fg-muted, #6b7280);
1507
+ flex: 0 0 12px;
1508
+ }
1509
+ .file-tree-spacer {
1510
+ display: inline-block;
1511
+ width: 12px;
1512
+ flex: 0 0 12px;
1513
+ }
1514
+ .file-tree-name {
1515
+ flex: 1 1 auto;
1516
+ white-space: nowrap;
1517
+ overflow: hidden;
1518
+ text-overflow: ellipsis;
1519
+ }
1520
+ .file-tree-empty {
1521
+ list-style: none;
1522
+ color: var(--fg-muted, #6b7280);
1523
+ font-style: italic;
1524
+ padding: 4px;
1525
+ }
1526
+
1527
+ /* ---------- tool-call detail panel ---------- */
1528
+ .tool-call-detail-panel {
1529
+ border-left: 4px solid rgba(99, 102, 241, 0.55);
1530
+ padding-left: 14px;
1531
+ margin-top: 18px;
1532
+ }
1533
+ .tool-call-detail-panel .v2-hint {
1534
+ font-size: 12px;
1535
+ font-weight: 400;
1536
+ color: var(--fg-muted, #6b7280);
1537
+ margin-left: 8px;
1538
+ }
1539
+ .tool-call-detail {
1540
+ border: 1px solid var(--border, #e5e7eb);
1541
+ border-radius: 6px;
1542
+ padding: 10px 12px;
1543
+ margin-top: 8px;
1544
+ background: #ffffff;
1545
+ }
1546
+ .tool-call-header {
1547
+ display: flex;
1548
+ align-items: center;
1549
+ gap: 10px;
1550
+ font-size: 13px;
1551
+ margin-bottom: 6px;
1552
+ }
1553
+ .tool-call-header .tool-name code {
1554
+ background: rgba(99, 102, 241, 0.08);
1555
+ padding: 1px 6px;
1556
+ border-radius: 3px;
1557
+ }
1558
+ .tool-call-header .tool-path {
1559
+ color: var(--fg-muted, #6b7280);
1560
+ font-family: ui-monospace, Menlo, monospace;
1561
+ font-size: 11px;
1562
+ margin-left: auto;
1563
+ overflow: hidden;
1564
+ text-overflow: ellipsis;
1565
+ white-space: nowrap;
1566
+ flex: 1 1 auto;
1567
+ }
1568
+ .tool-call-detail-empty {
1569
+ color: var(--fg-muted, #6b7280);
1570
+ font-style: italic;
1571
+ padding: 8px 0;
1572
+ }
1573
+
1574
+ /* ---------- Monaco container ---------- */
1575
+ .monaco-editor {
1576
+ width: 100%;
1577
+ height: 280px;
1578
+ border: 1px solid var(--border, #d1d5db);
1579
+ border-radius: 4px;
1580
+ margin: 6px 0 0;
1581
+ position: relative;
1582
+ background: #fafafa;
1583
+ overflow: hidden;
1584
+ }
1585
+ .monaco-editor .monaco-placeholder {
1586
+ display: flex;
1587
+ align-items: center;
1588
+ justify-content: center;
1589
+ height: 100%;
1590
+ color: var(--fg-muted, #6b7280);
1591
+ font-family: ui-monospace, Menlo, monospace;
1592
+ font-size: 12px;
1593
+ }
1594
+ .monaco-editor .monaco-error {
1595
+ color: rgba(220, 38, 38, 0.9);
1596
+ background: rgba(254, 226, 226, 0.5);
1597
+ }
1598
+ .monaco-editor .monaco-truncation-note {
1599
+ position: absolute;
1600
+ bottom: 4px;
1601
+ right: 8px;
1602
+ font-size: 10px;
1603
+ color: var(--fg-muted, #6b7280);
1604
+ background: rgba(255, 255, 255, 0.85);
1605
+ padding: 1px 6px;
1606
+ border-radius: 3px;
1607
+ }
1608
+
1609
+ /* ---------- LCS diff body ---------- */
1610
+ .diff-body {
1611
+ list-style: none;
1612
+ margin: 6px 0 0;
1613
+ padding: 6px 8px;
1614
+ border: 1px solid var(--border, #e5e7eb);
1615
+ border-radius: 4px;
1616
+ background: #fafafa;
1617
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
1618
+ font-size: 12px;
1619
+ line-height: 1.5;
1620
+ max-height: 320px;
1621
+ overflow: auto;
1622
+ }
1623
+ .diff-line {
1624
+ display: flex;
1625
+ gap: 8px;
1626
+ padding: 0 4px;
1627
+ white-space: pre;
1628
+ }
1629
+ .diff-line .diff-gutter {
1630
+ flex: 0 0 14px;
1631
+ text-align: center;
1632
+ font-weight: 600;
1633
+ }
1634
+ .diff-line.diff-line-added {
1635
+ background: rgba(220, 252, 231, 0.6);
1636
+ }
1637
+ .diff-line.diff-line-added .diff-gutter {
1638
+ color: #15803d;
1639
+ }
1640
+ .diff-line.diff-line-removed {
1641
+ background: rgba(254, 226, 226, 0.6);
1642
+ }
1643
+ .diff-line.diff-line-removed .diff-gutter {
1644
+ color: #b91c1c;
1645
+ }
1646
+ .diff-line.diff-line-context .diff-gutter {
1647
+ color: var(--fg-muted, #9ca3af);
1648
+ }
1649
+ .diff-line .diff-text {
1650
+ flex: 1 1 auto;
1651
+ white-space: pre;
1652
+ }
1653
+
1654
+ /* ---------- v2 small inline hint ---------- */
1655
+ .v2-hint {
1656
+ font-size: 11px;
1657
+ color: var(--fg-muted, #6b7280);
1658
+ }
@@ -0,0 +1,311 @@
1
+ /* v2.0.0: tool-call detail panel hydrator.
2
+ *
3
+ * Wires up the dedicated `<section id="tool-call-detail-panel">`:
4
+ *
5
+ * 1. Hooks `window.__toolClick(toolId)` (set by mermaid-renderer.js
6
+ * + app.js) so a Mermaid click scrolls the panel into view AND
7
+ * re-renders its content with the clicked call's Monaco + diff.
8
+ *
9
+ * 2. Lazy-loads Monaco Editor from a CDN the first time the user
10
+ * clicks any tool row. Subsequent clicks reuse the same Monaco
11
+ * model — no extra network round-trips.
12
+ *
13
+ * 3. Fetches the file content for `data-file-path` via
14
+ * `/api/file-content` and feeds it into Monaco. If the path is
15
+ * outside the server's sandbox (403), shows a graceful
16
+ * "file not accessible" placeholder rather than a broken editor.
17
+ *
18
+ * 4. Re-renders the inline `<details class="tool-detail">` block for
19
+ * the active tool row so the diff/Monaco surfaces both in the
20
+ * detail panel AND in the original table position.
21
+ *
22
+ * Loaded AFTER app.js + file-tree.js.
23
+ *
24
+ * The Monaco loader is loaded from jsDelivr (per design doc §2.1)
25
+ * and exposes the AMD `require` to set language workers.
26
+ */
27
+
28
+ (function attachToolCallDetailHydrator() {
29
+ const PANEL_SELECTOR = "#tool-call-detail-panel";
30
+ /** @type {HTMLElement|null} */
31
+ let panel = null;
32
+ /** @type {any} */
33
+ let monaco = null;
34
+ /** @type {boolean} */
35
+ let monacoLoading = false;
36
+ /** @type {Array<() => void>} */
37
+ const monacoWaiters = [];
38
+ /** @type {string|null} */
39
+ let currentToolId = null;
40
+ /** @type {string|null} */
41
+ let currentFilePath = null;
42
+
43
+ // -----------------------------------------------------------------------
44
+ // Monaco loader (CDN, AMD)
45
+ // -----------------------------------------------------------------------
46
+ const MONACO_VERSION = "0.45.0";
47
+ const MONACO_BASE = `https://cdn.jsdelivr.net/npm/monaco-editor@${MONACO_VERSION}/min/vs`;
48
+
49
+ /**
50
+ * Inject the AMD loader script and resolve once `require` is ready.
51
+ * The script is idempotent — calling twice is a no-op the second time.
52
+ */
53
+ function loadMonaco() {
54
+ if (monaco) return Promise.resolve(monaco);
55
+ if (monacoLoading) return new Promise((r) => monacoWaiters.push(() => r(monaco)));
56
+ monacoLoading = true;
57
+ return new Promise((resolve, reject) => {
58
+ /** @type {any} */
59
+ const w = window;
60
+ const onReady = () => {
61
+ try {
62
+ // eslint-disable-next-line no-undef
63
+ require.config({ paths: { vs: MONACO_BASE } });
64
+ // eslint-disable-next-line no-undef
65
+ require(["vs/editor/editor.main"], () => {
66
+ monaco = w.monaco;
67
+ monacoLoading = false;
68
+ for (const waiter of monacoWaiters) waiter();
69
+ monacoWaiters.length = 0;
70
+ resolve(monaco);
71
+ });
72
+ } catch (err) {
73
+ monacoLoading = false;
74
+ reject(err);
75
+ }
76
+ };
77
+ if (typeof w.require === "function" && w.require.config) {
78
+ onReady();
79
+ return;
80
+ }
81
+ const s = document.createElement("script");
82
+ s.src = `${MONACO_BASE}/loader.js`;
83
+ s.onload = () => onReady();
84
+ s.onerror = () => {
85
+ monacoLoading = false;
86
+ reject(new Error("Failed to load Monaco loader script"));
87
+ };
88
+ document.head.appendChild(s);
89
+ });
90
+ }
91
+
92
+ // -----------------------------------------------------------------------
93
+ // File fetch
94
+ // -----------------------------------------------------------------------
95
+
96
+ /**
97
+ * Fetch the file content via the server's sandboxed endpoint. Returns
98
+ * `null` when the file isn't accessible (404/403/network). We surface
99
+ * a graceful "not accessible" placeholder rather than blowing up the
100
+ * page.
101
+ * @param {string} path
102
+ * @returns {Promise<{content: string, truncated: boolean, byteSize: number} | null>}
103
+ */
104
+ async function fetchFileContent(path) {
105
+ try {
106
+ const res = await fetch(
107
+ `/api/file-content?path=${encodeURIComponent(path)}&max=${64 * 1024}`,
108
+ );
109
+ if (!res.ok) return null;
110
+ const data = await res.json();
111
+ if (typeof data.content !== "string") return null;
112
+ return {
113
+ content: data.content,
114
+ truncated: Boolean(data.truncated),
115
+ byteSize: Number(data.byteSize) || data.content.length,
116
+ };
117
+ } catch (_) {
118
+ return null;
119
+ }
120
+ }
121
+
122
+ // -----------------------------------------------------------------------
123
+ // Render
124
+ // -----------------------------------------------------------------------
125
+
126
+ /**
127
+ * Replace the panel contents with the detail for a given tool call.
128
+ *
129
+ * We can read the existing `<section class="tool-call-detail">` from
130
+ * the SSR-rendered table row (the same `data-tool-id`) and reuse it
131
+ * verbatim — the LCS diff is already computed server-side. The
132
+ * Monaco container is the only thing that needs lazy-loading.
133
+ *
134
+ * @param {string} toolId
135
+ */
136
+ async function renderDetailForTool(toolId) {
137
+ if (!panel) return;
138
+ currentToolId = toolId;
139
+ // Find the SSR-rendered detail (from the toolcalls table). There
140
+ // may be several `<section class="tool-call-detail">` blocks — one
141
+ // per row. Pick the one matching toolId.
142
+ const source = document.querySelector(
143
+ `section.tool-call-detail[data-tool-id="${cssEscape(toolId)}"]`,
144
+ );
145
+ if (!source) {
146
+ // Fallback: empty placeholder
147
+ panel.innerHTML = `<div class="tool-call-detail-empty">No detail available for tool call #${escapeHtml(toolId)}.</div>`;
148
+ return;
149
+ }
150
+ // Move the SSR block into the panel (clone so we don't tear it
151
+ // out of the row — the row's "show args" <details> still references
152
+ // it for diff display).
153
+ const cloned = source.cloneNode(true);
154
+ panel.innerHTML = "";
155
+ panel.appendChild(cloned);
156
+ panel.setAttribute("data-current-call", toolId);
157
+
158
+ const monacoContainer = cloned.querySelector(".monaco-editor");
159
+ if (!monacoContainer) return;
160
+ const filePath = monacoContainer.getAttribute("data-file-path");
161
+ if (!filePath) return;
162
+ currentFilePath = filePath;
163
+ monacoContainer.innerHTML = `<div class="monaco-placeholder">Loading ${escapeHtml(filePath)}…</div>`;
164
+ try {
165
+ const [file] = await Promise.all([fetchFileContent(filePath), loadMonaco()]);
166
+ if (!monaco) return; // loadMonaco() rejected; placeholder stays
167
+ if (!file) {
168
+ monacoContainer.innerHTML = `<div class="monaco-placeholder monaco-error">File not accessible (sandboxed or missing): <code>${escapeHtml(filePath)}</code></div>`;
169
+ return;
170
+ }
171
+ const language = monacoContainer.getAttribute("data-language") || "plaintext";
172
+ // eslint-disable-next-line no-undef
173
+ const editor = monaco.editor.create(monacoContainer, {
174
+ value: file.content,
175
+ language,
176
+ readOnly: true,
177
+ minimap: { enabled: false },
178
+ fontSize: 13,
179
+ lineNumbers: "on",
180
+ scrollBeyondLastLine: false,
181
+ automaticLayout: true,
182
+ theme: "vs",
183
+ });
184
+ // Dispose when the panel is cleared (next render or page unload).
185
+ const dispose = () => {
186
+ try {
187
+ editor.dispose();
188
+ } catch (_) {
189
+ /* noop */
190
+ }
191
+ };
192
+ monacoContainer.addEventListener("monaco-dispose", dispose, { once: true });
193
+ // Annotate with truncation notice if needed.
194
+ if (file.truncated) {
195
+ const note = document.createElement("div");
196
+ note.className = "monaco-truncation-note";
197
+ note.textContent = `(truncated — first ${file.content.length} bytes of ${file.byteSize})`;
198
+ monacoContainer.appendChild(note);
199
+ }
200
+ } catch (err) {
201
+ monacoContainer.innerHTML = `<div class="monaco-placeholder monaco-error">Monaco failed to load: ${escapeHtml(String((err && err.message) || err))}</div>`;
202
+ }
203
+ }
204
+
205
+ // -----------------------------------------------------------------------
206
+ // Hooks
207
+ // -----------------------------------------------------------------------
208
+
209
+ /**
210
+ * Replace the existing `window.__toolClick` (set by app.js) with our
211
+ * wrapper that ALSO updates the panel. We preserve the original
212
+ * scroll-into-view + highlight behaviour so the table row still
213
+ * flashes — we just add: panel render + file-tree highlight.
214
+ */
215
+ function attachToolClickHook() {
216
+ /** @type {any} */
217
+ const w = window;
218
+ const existing = w.__toolClick;
219
+ w.__toolClick = function (toolId) {
220
+ try {
221
+ if (typeof existing === "function") existing(toolId);
222
+ } catch (err) {
223
+ // eslint-disable-next-line no-console
224
+ console.warn("[task-show] prior __toolClick failed:", err);
225
+ }
226
+ try {
227
+ renderDetailForTool(String(toolId));
228
+ // Highlight the corresponding file-tree node (when the call
229
+ // has a file_path).
230
+ const detail = document.querySelector(
231
+ `section.tool-call-detail[data-tool-id="${cssEscape(String(toolId))}"]`,
232
+ );
233
+ const filePath = detail && detail.querySelector(".monaco-editor")
234
+ ? detail.querySelector(".monaco-editor").getAttribute("data-file-path")
235
+ : null;
236
+ if (filePath && w.__fileTree && typeof w.__fileTree.setActive === "function") {
237
+ w.__fileTree.setActive(filePath);
238
+ }
239
+ // Scroll the panel into view (block: 'start' so the heading
240
+ // is at the top of the viewport).
241
+ if (panel) {
242
+ try {
243
+ panel.scrollIntoView({ behavior: "smooth", block: "start" });
244
+ } catch (_) {
245
+ try {
246
+ panel.scrollIntoView();
247
+ } catch (_) {
248
+ /* noop */
249
+ }
250
+ }
251
+ }
252
+ } catch (err) {
253
+ // eslint-disable-next-line no-console
254
+ console.warn("[task-show] tool-call-detail render failed:", err);
255
+ }
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Initial hydration: find the panel + install the __toolClick hook.
261
+ * Idempotent.
262
+ */
263
+ function hydrate() {
264
+ panel = document.querySelector(PANEL_SELECTOR);
265
+ if (!panel) return;
266
+ attachToolClickHook();
267
+ // If the SSR rendered an initial detail (first tool call), pre-fill
268
+ // Monaco for it so the user sees the editor immediately on page load.
269
+ const initialMonaco = panel.querySelector(".monaco-editor");
270
+ const initialFilePath = initialMonaco && initialMonaco.getAttribute("data-file-path");
271
+ if (initialMonaco && initialFilePath) {
272
+ // Defer to next microtask so we don't block the first paint.
273
+ Promise.resolve().then(() => {
274
+ renderDetailForTool(currentToolId || panel.getAttribute("data-current-call") || "");
275
+ });
276
+ }
277
+ }
278
+
279
+ /**
280
+ * CSS.escape polyfill for older browsers + jsdom.
281
+ * @param {string} s
282
+ */
283
+ function cssEscape(s) {
284
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
285
+ return CSS.escape(s);
286
+ }
287
+ return String(s).replace(/([^a-zA-Z0-9_-])/g, "\\$1");
288
+ }
289
+
290
+ /**
291
+ * Tiny HTML escape for the placeholders we inject via innerHTML.
292
+ * @param {unknown} s
293
+ */
294
+ function escapeHtml(s) {
295
+ return String(s ?? "")
296
+ .replace(/&/g, "&amp;")
297
+ .replace(/</g, "&lt;")
298
+ .replace(/>/g, "&gt;")
299
+ .replace(/"/g, "&quot;")
300
+ .replace(/'/g, "&#39;");
301
+ }
302
+
303
+ // -----------------------------------------------------------------------
304
+ // Boot
305
+ // -----------------------------------------------------------------------
306
+ if (document.readyState === "loading") {
307
+ document.addEventListener("DOMContentLoaded", hydrate, { once: true });
308
+ } else {
309
+ hydrate();
310
+ }
311
+ })();