@runfusion/fusion 0.16.0 → 0.17.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.
Files changed (62) hide show
  1. package/README.md +3 -19
  2. package/dist/bin.js +3368 -1162
  3. package/dist/client/assets/{AgentDetailView-KQz9dg0n.js → AgentDetailView-BmxnuM0D.js} +3 -3
  4. package/dist/client/assets/AgentDetailView-yu8Xltqk.css +1 -0
  5. package/dist/client/assets/AgentsView-1xSqjJxs.js +517 -0
  6. package/dist/client/assets/AgentsView-Bs03ptrd.css +1 -0
  7. package/dist/client/assets/ChatView-CkWkEwXL.js +1 -0
  8. package/dist/client/assets/DevServerView-DIrmWI5T.js +1 -0
  9. package/dist/client/assets/{DirectoryPicker-CFj7FOgn.js → DirectoryPicker-Sqwdifcb.js} +1 -1
  10. package/dist/client/assets/DocumentsView-Cx_02o_z.js +1 -0
  11. package/dist/client/assets/InsightsView-DAJSq4gV.js +11 -0
  12. package/dist/client/assets/{MemoryView-DDbr1DaJ.js → MemoryView-CCIBAre3.js} +2 -2
  13. package/dist/client/assets/NodesView-D02HxGCl.js +14 -0
  14. package/dist/client/assets/{NodesView-BprfihLf.css → NodesView-DuAXX_0j.css} +1 -1
  15. package/dist/client/assets/{PiExtensionsManager-QgzWFBAT.js → PiExtensionsManager-DD0fTQNf.js} +3 -3
  16. package/dist/client/assets/{PluginManager-KHiz9oLY.js → PluginManager-Cfl0VBX9.js} +1 -1
  17. package/dist/client/assets/ResearchView-B9RqOVbr.js +1 -0
  18. package/dist/client/assets/ResearchView-BzCcDAS4.css +1 -0
  19. package/dist/client/assets/{RoadmapsView-BlbAZTdi.js → RoadmapsView-DsH7Hicx.js} +2 -2
  20. package/dist/client/assets/{SettingsModal-D5EUFR2z.js → SettingsModal-Cn_CIPXu.js} +1 -1
  21. package/dist/client/assets/{SettingsModal-D0QA2W5K.css → SettingsModal-Dq4a5KSX.css} +1 -1
  22. package/dist/client/assets/SettingsModal-YH_rM1ZT.js +31 -0
  23. package/dist/client/assets/{SetupWizardModal-tTXAm1Wb.js → SetupWizardModal-k5vqrHZU.js} +1 -1
  24. package/dist/client/assets/SkillsView-BIdt5cfB.js +1 -0
  25. package/dist/client/assets/{folder-open-DObdkm5J.js → folder-open-B3TO7t7Z.js} +1 -1
  26. package/dist/client/assets/index-BIJgrHEn.css +1 -0
  27. package/dist/client/assets/index-BlkXZ4C5.js +682 -0
  28. package/dist/client/assets/{star-BkGA2L-k.js → star-DW-M-BD_.js} +1 -1
  29. package/dist/client/assets/{upload-B0NF4J5P.js → upload-BzG6fknr.js} +1 -1
  30. package/dist/client/assets/{users-DgomiHTd.js → users-DEicv0kj.js} +1 -1
  31. package/dist/client/index.html +2 -2
  32. package/dist/client/version.json +1 -1
  33. package/dist/extension.js +2252 -718
  34. package/dist/pi-claude-cli/package.json +1 -1
  35. package/dist/pi-claude-cli/src/__tests__/process-manager.test.ts +11 -0
  36. package/dist/pi-claude-cli/src/__tests__/provider.test.ts +25 -0
  37. package/dist/plugins/fusion-plugin-dependency-graph/package.json +7 -5
  38. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.css +31 -0
  39. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.tsx +156 -48
  40. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraphView.test.tsx +261 -0
  41. package/package.json +1 -1
  42. package/skill/fusion/SKILL.md +5 -5
  43. package/skill/fusion/references/engine-tools.md +2 -2
  44. package/skill/fusion/references/extension-tools.md +3 -3
  45. package/skill/fusion/references/fusion-capabilities.md +1 -1
  46. package/skill/fusion/references/skill-patterns.md +1 -1
  47. package/skill/fusion/workflows/dashboard-cli.md +3 -3
  48. package/skill/fusion/workflows/task-management.md +1 -1
  49. package/dist/client/assets/AgentDetailView-BoEpPOjM.css +0 -1
  50. package/dist/client/assets/AgentsView-8Xo-c04o.js +0 -517
  51. package/dist/client/assets/AgentsView-V5GhlBYu.css +0 -1
  52. package/dist/client/assets/ChatView-CjgOh8x7.js +0 -1
  53. package/dist/client/assets/DevServerView-BOPrzi-j.js +0 -1
  54. package/dist/client/assets/DocumentsView-CcJNmH06.js +0 -1
  55. package/dist/client/assets/InsightsView-B0DKRIYV.js +0 -11
  56. package/dist/client/assets/NodesView-BqtHfXsl.js +0 -14
  57. package/dist/client/assets/ResearchView-CVxPC1vz.css +0 -1
  58. package/dist/client/assets/ResearchView-s3SrjNBm.js +0 -1
  59. package/dist/client/assets/SettingsModal-c9MG4sxl.js +0 -31
  60. package/dist/client/assets/SkillsView-CS4ONN3D.js +0 -1
  61. package/dist/client/assets/index-BRaIPmpp.js +0 -682
  62. package/dist/client/assets/index-DeED_ky2.css +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusion/pi-claude-cli",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "description": "Fusion vendored fork: pi coding-agent extension that routes LLM calls through the Claude Code CLI. Forked from rchern/pi-claude-cli (MIT). See UPSTREAM.md.",
5
5
  "license": "MIT",
6
6
  "private": true,
@@ -713,6 +713,17 @@ describe("resume session flag", () => {
713
713
  expect(args).not.toContain("--resume");
714
714
  });
715
715
 
716
+ it("does not include --session-id when --resume is provided", () => {
717
+ spawnClaude("claude-sonnet-4-5-20250929", undefined, {
718
+ resumeSessionId: "session-abc",
719
+ newSessionId: "session-new",
720
+ });
721
+ const args = (spawn as any).mock.calls[0][1] as string[];
722
+
723
+ expect(args).toContain("--resume");
724
+ expect(args).not.toContain("--session-id");
725
+ });
726
+
716
727
  it("includes both --resume and --effort when both are provided", () => {
717
728
  spawnClaude("claude-sonnet-4-5-20250929", undefined, {
718
729
  resumeSessionId: "session-abc",
@@ -1512,6 +1512,31 @@ describe("streamViaCli", { timeout: 90_000 }, () => {
1512
1512
  });
1513
1513
  });
1514
1514
 
1515
+ describe("resume behavior", () => {
1516
+ it("uses --resume for follow-up quick-chat turns when sessionId is present", async () => {
1517
+ const model = mockModels[0] as any;
1518
+ const context = {
1519
+ messages: [
1520
+ { role: "user", content: "Turn 1" },
1521
+ { role: "assistant", content: "Reply 1" },
1522
+ { role: "user", content: "Follow-up" },
1523
+ ],
1524
+ };
1525
+
1526
+ streamViaCli(model, context, { sessionId: "session-follow-up" } as any);
1527
+ await vi.advanceTimersByTimeAsync(0);
1528
+
1529
+ const args = (spawn as any).mock.calls[0][1] as string[];
1530
+ expect(args).toContain("--resume");
1531
+ expect(args).toContain("session-follow-up");
1532
+ expect(args).not.toContain("--session-id");
1533
+
1534
+ const proc = (spawn as any).mock.results[0].value;
1535
+ proc.stdout.end();
1536
+ await vi.advanceTimersByTimeAsync(100);
1537
+ });
1538
+ });
1539
+
1515
1540
  describe("MCP config with custom tool results", () => {
1516
1541
  it("keeps MCP config even when conversation ends with custom tool result", async () => {
1517
1542
  const model = mockModels[0] as any;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusion-plugin-examples/dependency-graph",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "Dependency graph dashboard view plugin for Fusion",
6
6
  "private": true,
@@ -19,14 +19,16 @@
19
19
  "test": "vitest run --silent=passed-only --reporter=dot"
20
20
  },
21
21
  "dependencies": {
22
- "@fusion/plugin-sdk": "workspace:*",
23
- "@fusion/core": "workspace:*"
22
+ "@fusion/core": "workspace:*",
23
+ "@fusion/plugin-sdk": "workspace:*"
24
24
  },
25
25
  "devDependencies": {
26
+ "@testing-library/react": "^16.3.2",
26
27
  "@types/node": "^25.5.2",
27
28
  "@types/react": "^19.0.0",
29
+ "react": "^19.0.0",
30
+ "react-dom": "^19.2.4",
28
31
  "typescript": "^5.7.0",
29
- "vitest": "^3.2.4",
30
- "react": "^19.0.0"
32
+ "vitest": "^3.2.4"
31
33
  }
32
34
  }
@@ -23,6 +23,7 @@
23
23
  background: var(--surface);
24
24
  min-height: var(--dependency-graph-canvas-min-height);
25
25
  cursor: grab;
26
+ touch-action: none;
26
27
  }
27
28
 
28
29
  .dependency-graph-canvas:active {
@@ -86,16 +87,46 @@
86
87
  filter: saturate(0.8);
87
88
  }
88
89
 
90
+ .dependency-graph-empty {
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: center;
94
+ height: 100%;
95
+ min-height: var(--dependency-graph-canvas-min-height);
96
+ padding: var(--space-xl);
97
+ color: var(--text-muted);
98
+ text-align: center;
99
+ }
100
+
89
101
  @media (max-width: 768px) {
90
102
  .dependency-graph-view {
91
103
  padding: var(--space-md);
92
104
  }
93
105
 
106
+ .dependency-graph-controls {
107
+ position: sticky;
108
+ top: 0;
109
+ z-index: 1;
110
+ background: var(--bg);
111
+ padding: var(--space-sm) 0;
112
+ }
113
+
114
+ .dependency-graph-controls .btn {
115
+ min-height: 44px;
116
+ min-width: 44px;
117
+ }
118
+
94
119
  .dependency-graph-canvas {
120
+ flex: 1;
121
+ min-height: 0;
95
122
  min-height: var(--dependency-graph-canvas-min-height-mobile);
96
123
  }
97
124
 
98
125
  .dependency-graph-node {
99
126
  width: min(100%, var(--dependency-graph-node-max-width-mobile)) !important;
100
127
  }
128
+
129
+ .dependency-graph-empty {
130
+ min-height: var(--dependency-graph-canvas-min-height-mobile);
131
+ }
101
132
  }
@@ -1,5 +1,5 @@
1
- import { useMemo, useRef, useState } from "react";
2
- import type { PointerEvent as ReactPointerEvent, ReactNode } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import type { PointerEvent as ReactPointerEvent, ReactNode, WheelEvent as ReactWheelEvent } from "react";
3
3
  import type { Task } from "@fusion/core";
4
4
  import { loadPositions, savePositions } from "./storage";
5
5
  import "./DependencyGraphView.css";
@@ -14,6 +14,8 @@ const SCENE_PADDING_REM = 2;
14
14
  const FIT_PADDING_REM = 2;
15
15
  const MIN_SCALE = 0.4;
16
16
  const MAX_SCALE = 2;
17
+ const WHEEL_ZOOM_FACTOR = 0.002;
18
+ const MOBILE_BREAKPOINT = 768;
17
19
 
18
20
  export interface DependencyGraphHostContext {
19
21
  projectId?: string;
@@ -29,9 +31,12 @@ export interface PluginDashboardViewComponentProps {
29
31
  type Position = { x: number; y: number };
30
32
 
31
33
  function getDistance(a: Position, b: Position): number {
32
- const deltaX = a.x - b.x;
33
- const deltaY = a.y - b.y;
34
- return Math.hypot(deltaX, deltaY);
34
+ return Math.hypot(a.x - b.x, a.y - b.y);
35
+ }
36
+
37
+ function isMobileViewport(): boolean {
38
+ if (typeof window === "undefined") return false;
39
+ return window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`).matches;
35
40
  }
36
41
 
37
42
  export function DependencyGraphView({ context }: PluginDashboardViewComponentProps) {
@@ -42,11 +47,16 @@ export function DependencyGraphView({ context }: PluginDashboardViewComponentPro
42
47
  const [hoveredTaskId, setHoveredTaskId] = useState<string | null>(null);
43
48
  const persisted = useMemo(() => loadPositions(context.projectId), [context.projectId]);
44
49
  const canvasRef = useRef<HTMLDivElement | null>(null);
50
+
51
+ // Multi-pointer tracking (no setPointerCapture — it breaks two-finger gestures)
52
+ const pointersRef = useRef<Map<number, Position>>(new Map());
45
53
  const interactionRef = useRef<
46
54
  | { kind: "node"; taskId: string; startPointer: Position; startNode: Position; moved: boolean }
47
55
  | { kind: "pan"; startPointer: Position; startPan: Position; moved: boolean }
48
56
  | null
49
57
  >(null);
58
+ const pinchRef = useRef<{ startDistance: number; startScale: number } | null>(null);
59
+ const autoFitDoneRef = useRef(false);
50
60
 
51
61
  const tasks = useMemo(
52
62
  () => context.tasks.filter((task) => ACTIVE_COLUMNS.has(task.column)),
@@ -162,7 +172,7 @@ export function DependencyGraphView({ context }: PluginDashboardViewComponentPro
162
172
  return related;
163
173
  }, [dependencyGraph.downstream, dependencyGraph.upstream, focusTaskId]);
164
174
 
165
- const fitToGraph = () => {
175
+ const fitToGraph = useCallback(() => {
166
176
  const canvas = canvasRef.current;
167
177
  if (!canvas) return;
168
178
 
@@ -179,7 +189,26 @@ export function DependencyGraphView({ context }: PluginDashboardViewComponentPro
179
189
 
180
190
  setScale(nextScale);
181
191
  setPan({ x: centeredPanX, y: centeredPanY });
182
- };
192
+ }, [bounds.width, bounds.height]);
193
+
194
+ // Auto-fit on initial mobile load
195
+ useEffect(() => {
196
+ if (autoFitDoneRef.current) return;
197
+ if (!isMobileViewport()) return;
198
+ if (positioned.length === 0) return;
199
+
200
+ const canvas = canvasRef.current;
201
+ if (!canvas) return;
202
+
203
+ // Ensure the canvas has non-zero dimensions before fitting
204
+ if (canvas.clientWidth === 0 || canvas.clientHeight === 0) return;
205
+
206
+ autoFitDoneRef.current = true;
207
+ // Use rAF to ensure layout is settled
208
+ requestAnimationFrame(() => {
209
+ fitToGraph();
210
+ });
211
+ }, [fitToGraph, positioned.length]);
183
212
 
184
213
  const persistPosition = (taskId: string, next: Position) => {
185
214
  setNodeOverrides((current) => ({ ...current, [taskId]: next }));
@@ -193,6 +222,10 @@ export function DependencyGraphView({ context }: PluginDashboardViewComponentPro
193
222
  event.stopPropagation();
194
223
  const hit = map.get(taskId);
195
224
  if (!hit) return;
225
+
226
+ // Track this pointer in the global map
227
+ pointersRef.current.set(event.pointerId, { x: event.clientX, y: event.clientY });
228
+
196
229
  interactionRef.current = {
197
230
  kind: "node",
198
231
  taskId,
@@ -200,21 +233,48 @@ export function DependencyGraphView({ context }: PluginDashboardViewComponentPro
200
233
  startNode: { x: hit.x, y: hit.y },
201
234
  moved: false,
202
235
  };
203
- event.currentTarget.setPointerCapture(event.pointerId);
204
236
  };
205
237
 
206
238
  const handleCanvasPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
207
239
  if (event.button !== 0) return;
240
+
241
+ // Track this pointer
242
+ pointersRef.current.set(event.pointerId, { x: event.clientX, y: event.clientY });
243
+
244
+ // If we already have another pointer, this is the start of a pinch gesture
245
+ if (pointersRef.current.size === 2) {
246
+ // Cancel any ongoing pan interaction
247
+ interactionRef.current = null;
248
+ const [p1, p2] = Array.from(pointersRef.current.values());
249
+ const distance = getDistance(p1, p2);
250
+ pinchRef.current = { startDistance: distance, startScale: scale };
251
+ return;
252
+ }
253
+
208
254
  interactionRef.current = {
209
255
  kind: "pan",
210
256
  startPointer: { x: event.clientX, y: event.clientY },
211
257
  startPan: pan,
212
258
  moved: false,
213
259
  };
214
- event.currentTarget.setPointerCapture(event.pointerId);
215
260
  };
216
261
 
217
262
  const handlePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
263
+ // Update tracked pointer position
264
+ if (pointersRef.current.has(event.pointerId)) {
265
+ pointersRef.current.set(event.pointerId, { x: event.clientX, y: event.clientY });
266
+ }
267
+
268
+ // Handle pinch gesture when two pointers are active
269
+ if (pointersRef.current.size >= 2 && pinchRef.current) {
270
+ const [p1, p2] = Array.from(pointersRef.current.values());
271
+ const currentDistance = getDistance(p1, p2);
272
+ const scaleFactor = currentDistance / pinchRef.current.startDistance;
273
+ const newScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, pinchRef.current.startScale * scaleFactor));
274
+ setScale(newScale);
275
+ return;
276
+ }
277
+
218
278
  const current = interactionRef.current;
219
279
  if (!current) return;
220
280
 
@@ -241,6 +301,16 @@ export function DependencyGraphView({ context }: PluginDashboardViewComponentPro
241
301
  };
242
302
 
243
303
  const handlePointerUp = (event: ReactPointerEvent<HTMLDivElement>) => {
304
+ pointersRef.current.delete(event.pointerId);
305
+
306
+ // If we had a pinch and one finger remains, end pinch mode
307
+ if (pinchRef.current) {
308
+ if (pointersRef.current.size < 2) {
309
+ pinchRef.current = null;
310
+ }
311
+ return;
312
+ }
313
+
244
314
  const current = interactionRef.current;
245
315
  interactionRef.current = null;
246
316
  if (!current) return;
@@ -256,8 +326,38 @@ export function DependencyGraphView({ context }: PluginDashboardViewComponentPro
256
326
 
257
327
  persistPosition(current.taskId, { x: hit.x, y: hit.y });
258
328
  }
329
+ };
259
330
 
260
- event.currentTarget.releasePointerCapture(event.pointerId);
331
+ const handlePointerCancel = (event: ReactPointerEvent<HTMLDivElement>) => {
332
+ pointersRef.current.delete(event.pointerId);
333
+ pinchRef.current = null;
334
+ interactionRef.current = null;
335
+ };
336
+
337
+ const handleWheel = (event: ReactWheelEvent<HTMLDivElement>) => {
338
+ event.preventDefault();
339
+ const canvas = canvasRef.current;
340
+ if (!canvas) return;
341
+
342
+ const rootFontSize = Number.parseFloat(globalThis.getComputedStyle(document.documentElement).fontSize) || 16;
343
+ const delta = -event.deltaY * WHEEL_ZOOM_FACTOR;
344
+ const newScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, scale * (1 + delta)));
345
+
346
+ // Zoom toward the pointer position
347
+ const rect = canvas.getBoundingClientRect();
348
+ const pointerX = event.clientX - rect.left;
349
+ const pointerY = event.clientY - rect.top;
350
+
351
+ // How much the point under the cursor should shift in rem
352
+ const scaleRatio = newScale / scale;
353
+ const panOffsetX = (pointerX / rootFontSize) * (1 - scaleRatio) / scale;
354
+ const panOffsetY = (pointerY / rootFontSize) * (1 - scaleRatio) / scale;
355
+
356
+ setScale(newScale);
357
+ setPan((prev) => ({
358
+ x: prev.x + panOffsetX * newScale / scale,
359
+ y: prev.y + panOffsetY * newScale / scale,
360
+ }));
261
361
  };
262
362
 
263
363
  return (
@@ -274,46 +374,54 @@ export function DependencyGraphView({ context }: PluginDashboardViewComponentPro
274
374
  onPointerDown={handleCanvasPointerDown}
275
375
  onPointerMove={handlePointerMove}
276
376
  onPointerUp={handlePointerUp}
377
+ onPointerCancel={handlePointerCancel}
378
+ onWheel={handleWheel}
277
379
  >
278
- <div
279
- className="dependency-graph-scene"
280
- style={{
281
- width: `${bounds.width}rem`,
282
- height: `${bounds.height}rem`,
283
- transform: `translate(${pan.x}rem, ${pan.y}rem) scale(${scale})`,
284
- transformOrigin: "top left",
285
- }}
286
- >
287
- <svg className="dependency-graph-edges" viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
288
- {edgesForRender.map((edge) => (
289
- <line
290
- key={`${edge.from}-${edge.to}`}
291
- x1={edge.renderX1}
292
- y1={edge.renderY1}
293
- x2={edge.renderX2}
294
- y2={edge.renderY2}
295
- className={`dependency-graph-edge${relatedTaskIds ? relatedTaskIds.has(edge.from) && relatedTaskIds.has(edge.to) ? " is-related" : " is-dimmed" : ""}`}
296
- />
380
+ {tasks.length === 0 ? (
381
+ <div className="dependency-graph-empty">
382
+ <p>No tasks to display. Tasks in Triage, Todo, In Progress, or In Review columns will appear here.</p>
383
+ </div>
384
+ ) : (
385
+ <div
386
+ className="dependency-graph-scene"
387
+ style={{
388
+ width: `${bounds.width}rem`,
389
+ height: `${bounds.height}rem`,
390
+ transform: `translate(${pan.x}rem, ${pan.y}rem) scale(${scale})`,
391
+ transformOrigin: "top left",
392
+ }}
393
+ >
394
+ <svg className="dependency-graph-edges" viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
395
+ {edgesForRender.map((edge) => (
396
+ <line
397
+ key={`${edge.from}-${edge.to}`}
398
+ x1={edge.renderX1}
399
+ y1={edge.renderY1}
400
+ x2={edge.renderX2}
401
+ y2={edge.renderY2}
402
+ className={`dependency-graph-edge${relatedTaskIds ? relatedTaskIds.has(edge.from) && relatedTaskIds.has(edge.to) ? " is-related" : " is-dimmed" : ""}`}
403
+ />
404
+ ))}
405
+ </svg>
406
+
407
+ {positionedForRender.map((node) => (
408
+ <div
409
+ key={node.task.id}
410
+ className={`dependency-graph-node${selectedTaskId === node.task.id ? " is-selected" : ""}${relatedTaskIds ? relatedTaskIds.has(node.task.id) ? " is-related" : " is-dimmed" : ""}`}
411
+ style={{
412
+ width: `${NODE_WIDTH_REM}rem`,
413
+ minHeight: `${NODE_HEIGHT_REM}rem`,
414
+ transform: `translate(${node.renderX}rem, ${node.renderY}rem)`,
415
+ }}
416
+ onPointerDown={(event) => handlePointerDownOnNode(node.task.id, event)}
417
+ onPointerEnter={() => setHoveredTaskId(node.task.id)}
418
+ onPointerLeave={() => setHoveredTaskId((current) => (current === node.task.id ? null : current))}
419
+ >
420
+ {context.renderTaskCard(node.task)}
421
+ </div>
297
422
  ))}
298
- </svg>
299
-
300
- {positionedForRender.map((node) => (
301
- <div
302
- key={node.task.id}
303
- className={`dependency-graph-node${selectedTaskId === node.task.id ? " is-selected" : ""}${relatedTaskIds ? relatedTaskIds.has(node.task.id) ? " is-related" : " is-dimmed" : ""}`}
304
- style={{
305
- width: `${NODE_WIDTH_REM}rem`,
306
- minHeight: `${NODE_HEIGHT_REM}rem`,
307
- transform: `translate(${node.renderX}rem, ${node.renderY}rem)`,
308
- }}
309
- onPointerDown={(event) => handlePointerDownOnNode(node.task.id, event)}
310
- onPointerEnter={() => setHoveredTaskId(node.task.id)}
311
- onPointerLeave={() => setHoveredTaskId((current) => (current === node.task.id ? null : current))}
312
- >
313
- {context.renderTaskCard(node.task)}
314
- </div>
315
- ))}
316
- </div>
423
+ </div>
424
+ )}
317
425
  </div>
318
426
  </section>
319
427
  );