Files
fusion/plugins/fusion-plugin-dependency-graph/README.md
Fusion 917ffa5b74 feat(FN-4129): add wheel pan and modifier-key zoom to dependency graph
Adds wheel-based panning to the dependency graph plugin, separating pan behavior from zoom when the wheel event is horizontal or un修饰, with new tests covering both interactions and updated graph interaction documentation in the plugin README.

Fusion-Task-Id: FN-4129
2026-05-12 17:04:32 -07:00

74 lines
7.2 KiB
Markdown

# fusion-plugin-dependency-graph
Plugin-provided top-level **Graph** dashboard view for Fusion.
## Host registration and bundled loading
The dependency graph view is registered as a **bundled plugin view** in the dashboard. This means:
- `registerBundledPluginViews()` (called at dashboard startup) registers the view component in the static plugin view registry under key `plugin:fusion-plugin-dependency-graph:graph`.
- The view component is resolved via a **literal-specifier lazy import** (`import("@fusion-plugin-examples/dependency-graph/dashboard-view")`) — this is critical for Vite/esbuild to emit a production chunk.
- In Vite dev/build and Vitest, `@fusion-plugin-examples/dependency-graph/dashboard-view` is aliased to `plugins/fusion-plugin-dependency-graph/src/dashboard-view.tsx` (source, not `dist/`). This avoids stale dist artifacts breaking the dashboard view.
- The App.tsx graph route resolves to the bundled view via `isPluginViewRegistered` fallback, so the graph view works even when the plugin is not installed/loaded through the PluginLoader API (e.g. fresh DB).
- The canonical route ID is `plugin:fusion-plugin-dependency-graph:graph`.
## Rendering approach
- **Filtering**: includes `triage`, `todo`, `in-progress`, `in-review`; excludes `done`, `archived`
- **Graph build**: edges are resolved only from `task.dependencies` as `source=dependent`, `target=dependency`
- **Orphan dependency handling**: if a visible task depends on an excluded/missing dependency (for example `done`/`archived` after filtering), the missing edge is silently dropped and graph rendering continues without broken connectors
- **Auto-layout**: Sugiyama-style layered layout (`computeAutoLayout`) groups nodes by dependency depth and spaces layers consistently
- **Edge drawing**: SVG bezier curves from source bottom-center to target top-center, with arrowheads showing dependent → dependency direction
- **Interaction**: drag-to-pan canvas background, scroll/two-finger pan, Ctrl/Cmd-wheel zoom, pinch-to-zoom with stationary midpoint, drag-to-reposition nodes, keyboard shortcuts, zoom toolbar, reset, and fit-to-graph
- **Zoomed navigation reachability**: pan clamping now scales with the full rendered graph bounds (min/max node extents) at the active zoom level, so zooming in no longer traps off-screen content behind fixed viewport-only limits
- **Fit-to-graph**: computes node bounding box from both minimum and maximum node coordinates (including negative auto-layout origins) with layout node dimensions and applies zoom/pan so the graph fits in viewport with padding
- **Initial auto-fit**: when no saved scoped positions exist, the first non-empty render auto-fits once; subsequent updates preserve user navigation state
- **Position persistence**: dragged node positions are stored per project in browser localStorage and restored on reload
- **Animated transitions**: fit/reset operations animate `transform` (`var(--transition-normal)`), while continuous drag/wheel/pinch stays transition-free for responsiveness
- **Node rendering**: each graph node renders the real dashboard `TaskCard` via `GraphTaskNode` (no duplicated card markup)
- **Task detail integration**: a primary non-drag click on a graph node surface opens the native dashboard task detail modal exactly once through host context (`openTaskDetail`), matching board/list behavior
- **In-progress behavior**: steps are visible by default and active-task glow (`agent-active`) is preserved because node cards reuse TaskCard directly
- **Active-state indicator bar**: active nodes render a compact top bar (`.graph-task-active-indicator`) with the current execution status label (for example `Executing`, `Planning`) and pulsing `--in-progress` emphasis
- **Current-step highlighting**: active nodes set `data-current-step` for valid native step indices so CSS selectors highlight the currently executing `.card-step-item` and pulse its step dot
- **Zoom-out differentiation**: `.graph-task-node--active` adds amplified glow and subtle scale/border tint so active nodes remain distinguishable at reduced zoom levels
- **In-review visual treatment**: `in-review` nodes get a static `.graph-task-node--in-review` left accent in `--in-review` to distinguish waiting-review work from active execution nodes
- **Graph node classes**: `.graph-task-node`, `.graph-task-node--active`, `.graph-task-node--in-review`, `.graph-task-node--highlighted`, `.graph-task-node--dimmed`, `.graph-node--highlighted`, and `.graph-node--dimmed` are available for graph-specific layering/highlight states while card internals remain owned by `TaskCard.css`
- **Graph edge classes**: `.graph-edge--highlighted` and `.graph-edge--dimmed` are applied during dependency-chain emphasis states
- **Drag behavior**: graph nodes pass `disableDrag={true}` to `TaskCard` so card-level HTML5 drag does not conflict with canvas pan/zoom
## Position persistence
- Canonical base key: `fusion-plugin-dependency-graph:positions`
- Storage key format: `kb:${projectId}:fusion-plugin-dependency-graph:positions` (falls back to `fusion-plugin-dependency-graph:positions` when no project is selected)
- Read path: positions load on graph mount and whenever `projectId` changes, then merge with fresh auto-layout so new tasks still receive layout defaults
- Write path: positions persist on drag end only (not on every drag frame), filtered to currently visible tasks for stale cleanup
- Reset behavior: Fit to graph / Reset view clear persisted positions and re-apply auto-layout
- Implementation detail: the plugin now reuses dashboard `projectStorage` helpers (`getScopedItem` / `setScopedItem` / `removeScopedItem`) instead of duplicating scoped localStorage logic
## Dependency chain highlighting
- **Hover** a node to highlight the full transitive upstream + downstream chain for that task.
- **Click** a node to persist selection highlighting until the same node is clicked again or the canvas pane is clicked; this same click also opens task detail once through the host detail callback.
- **Priority**: hover state overrides selected state; when hover leaves, selected highlighting reappears.
- **Dimming**: when a chain is active, unrelated nodes and edges are dimmed.
- **Neutral state**: when nothing is hovered/selected, no highlight/dim classes are applied.
- **Drag suppression**: drag movements above the node drag threshold suppress the post-drag click, preventing accidental detail opens on pointer release.
- **Edge rule**: an edge is highlighted only when both its source and target nodes are in the active chain.
## Controls
### Toolbar (bottom-right)
- **Zoom in** (`ZoomIn`) — button + `Ctrl+=` / `Cmd+=`
- **Zoom out** (`ZoomOut`) — button + `Ctrl+-` / `Cmd+-`
- **Zoom percent label** — live readout (for example `100%`, `75%`, `250%`)
- **Fit to graph** (`Maximize`) — button + `Ctrl+Shift+F` / `Cmd+Shift+F`
- **Reset view** (`RotateCcw`) — button + `Ctrl+0` / `Cmd+0`
### Additional keyboard behavior
- **Escape** resets to default view (`zoom=1`, `pan=0,0`)
- Shortcuts are suppressed when focus is inside `input`, `textarea`, `select`, or `contentEditable` elements
All controls are rendered as floating `.btn-icon` actions in the bottom-right corner, with mobile-friendly `44px` touch targets in the `@media (max-width: 768px)` override.