@ai-setting/roy-plugin-task-show 2.4.0 → 2.4.1

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.4.0",
3
+ "version": "2.4.1",
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": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "type": "tool-plugin",
5
- "description": "v2.4.0: Combines v2.3.0 (worktree-aware file-tree + unbounded Mermaid zoom, Task #2773) with the v2.3.0 mermaid placeholder expansion (Task #2771). Sidebar + endpoint scope git ls-files to session.context.worktree (fallback process.cwd()); Mermaid zoom-in is no longer capped at 3.0×; .mermaid placeholder is now min-height: 2400px and .mermaid-zoom-stage is min-width: 6400px / min-height: 2400px (5× base, via --mermaid-placeholder-multiplier CSS custom property) so dense diagrams get enough canvas to pan/zoom comfortably. v2.3.0: Worktree-aware file tree sidebar + unbounded Mermaid zoom-in. The file-tree sidebar (and /api/task/:id/file-tree endpoint) now scope git ls-files to session.context.worktree when the host (task:before.create / task:after.create payload) supplies a worktree path, so the sidebar shows the files the agent is actually editing instead of the plugin process cwd. New resolveWorktreePath helper in src/file-tree.ts returns session.context.worktree (trimmed, with empty/non-string guard) or falls back to process.cwd() for backward compatibility with pre-v2.3.0 hosts. TypeScript: TaskSession gains an optional context?: { worktree?: string } field. TaskShowServer exposes getCollector() so tests can seed sessions with context.worktree set. The Mermaid zoom toolbar (introduced in v2.0.8) no longer caps zoom-in at 3.0× — the ZOOM_MAX constant is REMOVED and clampZoom only enforces ZOOM_MIN=0.25, so dense diagrams remain readable on high-DPI monitors when the user wants to zoom way in. ZOOM_MIN stays in place so zoom-out still bottoms out before rendering a black screen. New tests: test/file-tree-worktree.test.ts (10 cases — resolveWorktreePath edges, SSR scope, endpoint scope, fallback), test/mermaid-zoom-unlimited.test.ts (8 cases — source guard, behaviour at 6×/10×/50×, lower-bound preserved), test/mermaid-placeholder-size.test.ts (7 cases — min-height/min-width 5× checks, transform-origin preserved, --mermaid-placeholder-multiplier hint). v2.3.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).",
5
+ "description": "v2.4.1: Mermaid zoom now supports click-and-drag pan (Task #2785). After zooming in, users can drag the diagram to view the surrounding canvas area. Implementation: per-container panState/dragState WeakMap (no global state leak); applyTransform(translate+scale) single-source-of-truth for the CSS transform; attachPanHandlers on pointerdown/move/up/cancel with setPointerCapture for off-element drags; pointer cursor switches to grab/grabbing with touch-action: none to prevent native scrolling; reset/zoomReset now also resets pan to (0,0). 14/14 new mermaid-pan tests pass, full suite 558/564 with 6 pre-existing failures unrelated to this commit. v2.4.0: Combines v2.3.0 Combines v2.3.0 (worktree-aware file-tree + unbounded Mermaid zoom, Task #2773) with the v2.3.0 mermaid placeholder expansion (Task #2771). Sidebar + endpoint scope git ls-files to session.context.worktree (fallback process.cwd()); Mermaid zoom-in is no longer capped at 3.0×; .mermaid placeholder is now min-height: 2400px and .mermaid-zoom-stage is min-width: 6400px / min-height: 2400px (5× base, via --mermaid-placeholder-multiplier CSS custom property) so dense diagrams get enough canvas to pan/zoom comfortably. v2.3.0: Worktree-aware file tree sidebar + unbounded Mermaid zoom-in. The file-tree sidebar (and /api/task/:id/file-tree endpoint) now scope git ls-files to session.context.worktree when the host (task:before.create / task:after.create payload) supplies a worktree path, so the sidebar shows the files the agent is actually editing instead of the plugin process cwd. New resolveWorktreePath helper in src/file-tree.ts returns session.context.worktree (trimmed, with empty/non-string guard) or falls back to process.cwd() for backward compatibility with pre-v2.3.0 hosts. TypeScript: TaskSession gains an optional context?: { worktree?: string } field. TaskShowServer exposes getCollector() so tests can seed sessions with context.worktree set. The Mermaid zoom toolbar (introduced in v2.0.8) no longer caps zoom-in at 3.0× — the ZOOM_MAX constant is REMOVED and clampZoom only enforces ZOOM_MIN=0.25, so dense diagrams remain readable on high-DPI monitors when the user wants to zoom way in. ZOOM_MIN stays in place so zoom-out still bottoms out before rendering a black screen. New tests: test/file-tree-worktree.test.ts (10 cases — resolveWorktreePath edges, SSR scope, endpoint scope, fallback), test/mermaid-zoom-unlimited.test.ts (8 cases — source guard, behaviour at 6×/10×/50×, lower-bound preserved), test/mermaid-placeholder-size.test.ts (7 cases — min-height/min-width 5× checks, transform-origin preserved, --mermaid-placeholder-multiplier hint). v2.3.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
  {
@@ -89,4 +89,4 @@
89
89
  },
90
90
  "repository": "github:ai-setting/roy-plugin-task-visualize",
91
91
  "bugs": "https://github.com/ai-setting/roy-plugin-task-visualize/issues"
92
- }
92
+ }
@@ -162,6 +162,16 @@
162
162
  // not reset the user's zoom level.
163
163
  const zoomState = new WeakMap(); // container -> number (>=0.25, no upper cap in v2.3.0+)
164
164
 
165
+ // v2.4.1 (feat/task-show-v241-mermaid-pan): per-container pan
166
+ // state, independent of zoom. Keyed by the same container
167
+ // reference so the user's drag offset survives re-renders.
168
+ const panState = new WeakMap(); // container -> { tx: number, ty: number }
169
+ // v2.4.1: per-container drag-in-progress state. Holds the
170
+ // pointer down coordinates so `pointermove` can compute the
171
+ // delta. We do NOT store the live tx/ty here — those live in
172
+ // `panState` so they survive pointer-cancel.
173
+ const dragState = new WeakMap(); // container -> { startX, startY, startTx, startTy, pointerId }
174
+
165
175
  function nextSeq(container) {
166
176
  const cur = seq.get(container) || 0;
167
177
  const next = cur + 1;
@@ -229,12 +239,79 @@
229
239
  return v == null ? ZOOM_DEFAULT : v;
230
240
  }
231
241
 
242
+ // -----------------------------------------------------------------
243
+ // v2.4.1 (feat/task-show-v241-mermaid-pan): pan state + helpers
244
+ // -----------------------------------------------------------------
245
+
246
+ // Default pan offsets: no offset. panState defaults to
247
+ // { tx: 0, ty: 0 } if the container has no entry yet.
248
+ function getPan(container) {
249
+ if (!container) return { tx: 0, ty: 0 };
250
+ const v = panState.get(container);
251
+ return v == null ? { tx: 0, ty: 0 } : { tx: v.tx, ty: v.ty };
252
+ }
253
+
254
+ function setPan(container, tx, ty) {
255
+ // Sanitize: NaN -> 0 (defensive; tests assert finite).
256
+ const nextTx = Number.isFinite(tx) ? tx : 0;
257
+ const nextTy = Number.isFinite(ty) ? ty : 0;
258
+ panState.set(container, { tx: nextTx, ty: nextTy });
259
+ return { tx: nextTx, ty: nextTy };
260
+ }
261
+
262
+ function resetPan(container) {
263
+ return setPan(container, 0, 0);
264
+ }
265
+
266
+ // Clamp pan offsets so the diagram cannot be dragged beyond
267
+ // its visible bounds. The lower bound is `-diagramSize + containerSize`
268
+ // (so the right/bottom edge stays visible), the upper bound
269
+ // is 0 (so the top/left edge stays visible). When the
270
+ // diagram is smaller than the container in either axis, that
271
+ // axis is pinned to 0.
272
+ //
273
+ // When the stage has no measured size yet (pre-mount, jsdom
274
+ // test, or a freshly-created wrapper), the bounds collapse
275
+ // to (0, 0) — i.e. we still return the requested values
276
+ // (within NaN sanitization) without clamping, because we have
277
+ // no information to clamp against.
278
+ function clampPan(container, tx, ty) {
279
+ const safeTx = Number.isFinite(tx) ? tx : 0;
280
+ const safeTy = Number.isFinite(ty) ? ty : 0;
281
+ if (!container || !container.querySelector) {
282
+ return { tx: safeTx, ty: safeTy };
283
+ }
284
+ const stage = container.querySelector(".mermaid-zoom-stage");
285
+ if (!stage) return { tx: safeTx, ty: safeTy };
286
+ const cw = container.clientWidth || 0;
287
+ const ch = container.clientHeight || 0;
288
+ const sw = stage.scrollWidth || 0;
289
+ const sh = stage.scrollHeight || 0;
290
+ // No dimensions known yet — don't clamp, the caller may
291
+ // be setting an explicit value (e.g. tests, programmatic
292
+ // panning before the SVG is measured).
293
+ if (cw === 0 && ch === 0 && sw === 0 && sh === 0) {
294
+ return { tx: safeTx, ty: safeTy };
295
+ }
296
+ const minTx = sw > cw ? -(sw - cw) : 0;
297
+ const minTy = sh > ch ? -(sh - ch) : 0;
298
+ const nextTx = Math.max(minTx, Math.min(0, safeTx));
299
+ const nextTy = Math.max(minTy, Math.min(0, safeTy));
300
+ return { tx: nextTx, ty: nextTy };
301
+ }
302
+
232
303
  function ensureZoomStage(container) {
233
304
  // Lazy-create the wrapper if the user calls zoom before a
234
305
  // successful mount (the regression test exercises this path).
235
306
  if (!container || !container.querySelector) return null;
236
307
  let stage = container.querySelector(".mermaid-zoom-stage");
237
- if (stage) return stage;
308
+ if (stage) {
309
+ // v2.4.1: ensure pan handlers are wired even when the
310
+ // stage was created by a previous version of the
311
+ // controller (covers the upgrade path).
312
+ attachPanHandlers(container);
313
+ return stage;
314
+ }
238
315
  stage = container.ownerDocument.createElement("div");
239
316
  stage.className = "mermaid-zoom-stage";
240
317
  // Re-parent any existing children (e.g. an already-rendered
@@ -252,6 +329,9 @@
252
329
  stage.appendChild(fragment);
253
330
  }
254
331
  container.appendChild(stage);
332
+ // v2.4.1: wire pointer-event handlers on the freshly
333
+ // created stage so the user can click-drag to pan.
334
+ attachPanHandlers(container);
255
335
  return stage;
256
336
  }
257
337
 
@@ -302,16 +382,33 @@
302
382
  applyZoom(container, getZoom(container));
303
383
  }
304
384
 
305
- function applyZoom(container, zoom) {
385
+ function applyZoom(container, _zoom) {
386
+ // v2.4.1: applyZoom now writes the COMPOSED transform
387
+ // (`translate(tx, ty) scale(z)`) so pan and zoom share a
388
+ // single CSS property. This keeps the order stable (CSS
389
+ // applies right-to-left: scale first, then translate) and
390
+ // avoids the brief flicker a separate `transform-origin`
391
+ // change would cause. The legacy `_zoom` argument is
392
+ // ignored — `applyTransform` reads both pan + zoom from
393
+ // their WeakMaps so the caller's value stays authoritative.
394
+ applyTransform(container);
395
+ }
396
+
397
+ // v2.4.1: single source of truth for the stage's transform.
398
+ // Reads current pan + zoom from their WeakMaps and writes
399
+ // `translate(tx, ty) scale(z)` to the inline style.
400
+ function applyTransform(container) {
306
401
  const stage = ensureZoomStage(container);
307
402
  if (!stage) return;
403
+ const z = getZoom(container);
404
+ const p = getPan(container);
308
405
  // Use the inline `style` so the transform survives any
309
406
  // user-provided stylesheet overrides. We also set
310
407
  // `transform-origin: top left` so the diagram does not
311
408
  // recenter on every zoom step (Mermaid SVGs have their own
312
409
  // viewBox, but the surrounding page would jump otherwise).
313
410
  stage.style.transformOrigin = "top left";
314
- stage.style.transform = `scale(${zoom})`;
411
+ stage.style.transform = `translate(${p.tx}px, ${p.ty}px) scale(${z})`;
315
412
  }
316
413
 
317
414
  function setZoom(container, nextZoom) {
@@ -352,7 +449,143 @@
352
449
  }
353
450
 
354
451
  function resetZoom(container) {
355
- return setZoom(container, ZOOM_DEFAULT);
452
+ // v2.4.1: resetZoom now also resets the pan offset, so
453
+ // "100%" returns the user to the top-left corner of the
454
+ // diagram regardless of where they had dragged it. We
455
+ // always re-apply the transform even if the zoom value
456
+ // didn't change — `setZoom` short-circuits when from ===
457
+ // to (e.g. the user was already at 100%), which would
458
+ // otherwise leave a stale `translate(tx, ty)` on the
459
+ // stage even after pan was reset.
460
+ resetPan(container);
461
+ const z = setZoom(container, ZOOM_DEFAULT);
462
+ applyTransform(container);
463
+ return z;
464
+ }
465
+
466
+ // v2.4.1: pan API. Mutates the per-container panState and
467
+ // re-applies the composed transform. Pan is clamped so the
468
+ // diagram cannot be dragged past the viewport (see clampPan).
469
+ function panTo(container, tx, ty) {
470
+ if (!container) return { tx: 0, ty: 0 };
471
+ // Lazy-create the stage so callers can pan before mount
472
+ // (matches the behaviour of `setZoom` / `applyZoom`).
473
+ ensureZoomStage(container);
474
+ const clamped = clampPan(container, Number(tx) || 0, Number(ty) || 0);
475
+ setPan(container, clamped.tx, clamped.ty);
476
+ applyTransform(container);
477
+ return { tx: clamped.tx, ty: clamped.ty };
478
+ }
479
+
480
+ // v2.4.1: pointer-driven drag handlers. We wire them on the
481
+ // stage (not the container) so they don't interfere with
482
+ // text selection inside the toolbar.
483
+ function attachPanHandlers(container) {
484
+ if (!container || !container.ownerDocument) return;
485
+ // Already wired — don't double-bind.
486
+ if (container.__panHandlersAttached) return;
487
+ const stage = container.querySelector(".mermaid-zoom-stage");
488
+ if (!stage) return;
489
+ const doc = container.ownerDocument;
490
+
491
+ // pointerdown: capture the starting client coordinates and
492
+ // the current pan offsets so subsequent pointermove events
493
+ // can compute the delta.
494
+ function onPointerDown(ev) {
495
+ // Only respond to the primary pointer button (left click
496
+ // or primary touch). Ignore right-click and middle-click.
497
+ if (ev && typeof ev.button === "number" && ev.button !== 0) return;
498
+ // Allow the user to drag using touch / pen too.
499
+ const pid = ev && typeof ev.pointerId === "number" ? ev.pointerId : 1;
500
+ const cur = getPan(container);
501
+ dragState.set(container, {
502
+ startX: ev.clientX,
503
+ startY: ev.clientY,
504
+ startTx: cur.tx,
505
+ startTy: cur.ty,
506
+ pointerId: pid,
507
+ });
508
+ stage.classList.add("is-dragging");
509
+ try {
510
+ stage.setPointerCapture && stage.setPointerCapture(pid);
511
+ } catch (_) {
512
+ // ignore — setPointerCapture can throw in jsdom
513
+ }
514
+ if (ev && typeof ev.preventDefault === "function") {
515
+ try { ev.preventDefault(); } catch (_) { /* ignore */ }
516
+ }
517
+ }
518
+
519
+ function onPointerMove(ev) {
520
+ const drag = dragState.get(container);
521
+ if (!drag) return;
522
+ // Filter to the captured pointer only — touchscreens may
523
+ // send moves for secondary fingers.
524
+ if (ev && typeof ev.pointerId === "number" && drag.pointerId !== undefined && ev.pointerId !== drag.pointerId) {
525
+ return;
526
+ }
527
+ const dx = ev.clientX - drag.startX;
528
+ const dy = ev.clientY - drag.startY;
529
+ panTo(container, drag.startTx + dx, drag.startTy + dy);
530
+ }
531
+
532
+ function endDrag(ev) {
533
+ const drag = dragState.get(container);
534
+ if (!drag) return;
535
+ if (ev && typeof ev.pointerId === "number" && drag.pointerId !== undefined && ev.pointerId !== drag.pointerId) {
536
+ return;
537
+ }
538
+ dragState.delete(container);
539
+ stage.classList.remove("is-dragging");
540
+ try {
541
+ stage.releasePointerCapture && stage.releasePointerCapture(drag.pointerId);
542
+ } catch (_) {
543
+ // ignore — release can throw if capture was lost
544
+ }
545
+ }
546
+
547
+ // pointermove + pointerup + pointercancel all attach to the
548
+ // stage itself so the drag survives the pointer leaving
549
+ // the stage element. setPointerCapture keeps the events
550
+ // routed to the stage even if the cursor drifts off the
551
+ // diagram. Attaching to the stage (rather than window)
552
+ // also keeps the implementation deterministic in test
553
+ // environments where `window` and `doc.defaultView` can
554
+ // be distinct objects.
555
+ stage.addEventListener("pointerdown", onPointerDown);
556
+ stage.addEventListener("pointermove", onPointerMove);
557
+ stage.addEventListener("pointerup", endDrag);
558
+ stage.addEventListener("pointercancel", endDrag);
559
+ // Also attach to document/window as a safety net — some
560
+ // browsers route pointercancel through the document even
561
+ // when capture is set. We swallow duplicates in endDrag.
562
+ if (doc && doc.addEventListener) {
563
+ doc.addEventListener("pointerup", endDrag);
564
+ doc.addEventListener("pointercancel", endDrag);
565
+ }
566
+ // We need a way to remove these listeners on destroy();
567
+ // stash a teardown closure on the container.
568
+ container.__panHandlers = { onPointerDown, onPointerMove, endDrag, doc, stage };
569
+ container.__panHandlersAttached = true;
570
+ }
571
+
572
+ function detachPanHandlers(container) {
573
+ if (!container || !container.__panHandlers) return;
574
+ const h = container.__panHandlers;
575
+ try {
576
+ h.stage && h.stage.removeEventListener && h.stage.removeEventListener("pointerdown", h.onPointerDown);
577
+ h.stage && h.stage.removeEventListener && h.stage.removeEventListener("pointermove", h.onPointerMove);
578
+ h.stage && h.stage.removeEventListener && h.stage.removeEventListener("pointerup", h.endDrag);
579
+ h.stage && h.stage.removeEventListener && h.stage.removeEventListener("pointercancel", h.endDrag);
580
+ if (h.doc && h.doc.removeEventListener) {
581
+ h.doc.removeEventListener("pointerup", h.endDrag);
582
+ h.doc.removeEventListener("pointercancel", h.endDrag);
583
+ }
584
+ } catch (_) {
585
+ // ignore
586
+ }
587
+ container.__panHandlers = null;
588
+ container.__panHandlersAttached = false;
356
589
  }
357
590
 
358
591
  /**
@@ -490,18 +723,31 @@
490
723
  zoomIn,
491
724
  zoomOut,
492
725
  resetZoom,
726
+ // v2.4.1 (feat/task-show-v241-mermaid-pan): pan controls.
727
+ // The pan offset is tracked per-container in `panState`
728
+ // and composes with zoom via the same CSS transform
729
+ // (`translate(tx, ty) scale(z)`). Pointer-driven drag is
730
+ // wired automatically when the stage is created.
731
+ getPan,
732
+ panTo,
493
733
  destroy() {
494
734
  controllerDestroyed = true;
495
735
  // Mark every known container as destroyed so any in-flight render
496
736
  // for them is dropped. We can't iterate WeakMaps, but we *can*
497
737
  // iterate the Set we maintain.
498
738
  const list = Array.from(containers);
499
- for (const c of list) markDestroyed(c);
739
+ for (const c of list) {
740
+ markDestroyed(c);
741
+ // v2.4.1: also tear down the pan event listeners so
742
+ // detached controllers don't leak handlers.
743
+ detachPanHandlers(c);
744
+ }
500
745
  containers.clear();
501
746
  },
502
747
  /** Destroy state for a single container. */
503
748
  destroyContainer(container) {
504
749
  markDestroyed(container);
750
+ detachPanHandlers(container);
505
751
  containers.delete(container);
506
752
  },
507
753
  };
package/public/style.css CHANGED
@@ -447,7 +447,7 @@ table.toolcalls tr.tool-row.highlight:hover {
447
447
  /* The wrapper that carries the transform. `transform-origin:
448
448
  top left` ensures the diagram grows down-right rather than
449
449
  re-centering (which would look jumpy). The transform itself
450
- is set inline by `MermaidRenderer.applyZoom` so the JS
450
+ is set inline by `MermaidRenderer.applyTransform` so the JS
451
451
  state is the single source of truth. */
452
452
  display: inline-block;
453
453
  transform-origin: top left;
@@ -466,6 +466,29 @@ table.toolcalls tr.tool-row.highlight:hover {
466
466
  min-height: calc(
467
467
  var(--mermaid-base-height) * var(--mermaid-placeholder-multiplier)
468
468
  );
469
+ /* v2.4.1 (feat/task-show-v241-mermaid-pan): the stage is
470
+ grabbable so the user can drag to pan around a zoomed-in
471
+ diagram. `cursor: grab` is the canonical CSS hint for
472
+ "this thing is draggable" — combined with `cursor:
473
+ grabbing` set while a drag is in flight (see
474
+ `.mermaid-zoom-stage.is-dragging`). `touch-action: none`
475
+ disables the browser's default pan/zoom gestures on
476
+ touchscreens so our pointer handlers stay in charge. */
477
+ cursor: grab;
478
+ touch-action: none;
479
+ user-select: none;
480
+ -webkit-user-select: none;
481
+ }
482
+
483
+ /* v2.4.1 (feat/task-show-v241-mermaid-pan): the dragging
484
+ variant is set on the stage while a pointer drag is in
485
+ flight. `cursor: grabbing` is the conventional "currently
486
+ dragging" cursor, and we drop the transition so the
487
+ transform tracks the pointer 1:1 (otherwise the diagram
488
+ lags behind the cursor). */
489
+ .mermaid-zoom-stage.is-dragging {
490
+ cursor: grabbing;
491
+ transition: none;
469
492
  }
470
493
 
471
494
  .hint {