@runfusion/fusion 0.23.0 → 0.24.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.
Files changed (204) hide show
  1. package/dist/bin.js +26610 -20597
  2. package/dist/client/assets/AgentDetailView-BwJaLqZh.css +1 -0
  3. package/dist/client/assets/AgentDetailView-gy_5SUj2.js +18 -0
  4. package/dist/client/assets/AgentsView-BkB9FiMT.js +29 -0
  5. package/dist/client/assets/{AgentsView-DSGQWObq.css → AgentsView-CV3vm7Qk.css} +1 -1
  6. package/dist/client/assets/ChatView-B_-B8fqu.js +1 -0
  7. package/dist/client/assets/ChatView-DwJAd5G1.css +1 -0
  8. package/dist/client/assets/{DevServerView-C9lzHrcT.js → DevServerView-BkvtjZBa.js} +1 -1
  9. package/dist/client/assets/{DirectoryPicker-aVdFaV37.js → DirectoryPicker-BK-KbnhP.js} +1 -1
  10. package/dist/client/assets/{DocumentsView-DIpg3NSP.js → DocumentsView-BEg1CQAk.js} +1 -1
  11. package/dist/client/assets/{DocumentsView-BrhyOdeE.css → DocumentsView-gv4zG3aT.css} +1 -1
  12. package/dist/client/assets/EvalsView-Berf9bQm.js +1 -0
  13. package/dist/client/assets/EvalsView-CUNJ1TLc.css +1 -0
  14. package/dist/client/assets/{agentSkills-DDHJnrkn.css → ExperimentalAgentOnboardingModal-B-APN_lM.css} +1 -1
  15. package/dist/client/assets/ExperimentalAgentOnboardingModal-jcInE50G.js +499 -0
  16. package/dist/client/assets/InsightsView-B0J4mhzV.css +1 -0
  17. package/dist/client/assets/InsightsView-BX5bSF1J.js +11 -0
  18. package/dist/client/assets/{MemoryView-nXlTqebk.js → MemoryView-CKElJY_3.js} +2 -2
  19. package/dist/client/assets/NodesView-DLUOBLf6.js +14 -0
  20. package/dist/client/assets/NodesView-DT4pXowv.css +1 -0
  21. package/dist/client/assets/{PiExtensionsManager-Buopv-jb.js → PiExtensionsManager-COlJf0Kx.js} +2 -2
  22. package/dist/client/assets/PluginManager-CfW55BF4.js +1 -0
  23. package/dist/client/assets/PluginManager-DtRQXia5.css +1 -0
  24. package/dist/client/assets/{ResearchView-_BHXUv2j.js → ResearchView-B256Lr8I.js} +1 -1
  25. package/dist/client/assets/SettingsModal-BeA_nQtW.js +31 -0
  26. package/dist/client/assets/SettingsModal-DzsLquBu.css +1 -0
  27. package/dist/client/assets/{SettingsModal-C89Ikhfm.js → SettingsModal-yRqM4DV8.js} +1 -1
  28. package/dist/client/assets/SetupWizardModal-uUZk3TKT.js +1 -0
  29. package/dist/client/assets/{SkillsView-hDpTBdFT.js → SkillsView-CP8JX0P_.js} +1 -1
  30. package/dist/client/assets/TodoView-Cx9cVhq7.css +1 -0
  31. package/dist/client/assets/TodoView-DCRIkDZ-.js +6 -0
  32. package/dist/client/assets/createLucideIcon-BazL2hk5.js +21 -0
  33. package/dist/client/assets/dashboard-view-BkTMSZYn.css +1 -0
  34. package/dist/client/assets/dashboard-view-CyWN-d02.js +63 -0
  35. package/dist/client/assets/dashboard-view-lR7YYmSC.js +21 -0
  36. package/dist/client/assets/{folder-open-usZkXdq2.js → folder-open-DHjELt8-.js} +1 -1
  37. package/dist/client/assets/index-CQyVRLOb.js +692 -0
  38. package/dist/client/assets/index-CxA2Nn0_.css +1 -0
  39. package/dist/client/assets/projectDetection-G3XuxD2X.js +1 -0
  40. package/dist/client/assets/{star-BAT_ObKE.js → star-DYesq1AV.js} +1 -1
  41. package/dist/client/assets/{upload-BC2YKNEV.js → upload-DTWF3Db5.js} +1 -1
  42. package/dist/client/assets/{users-Dkd4rtrN.js → users--syrel4l.js} +1 -1
  43. package/dist/client/index.html +12 -20
  44. package/dist/client/theme-data.css +106 -0
  45. package/dist/client/version.json +1 -1
  46. package/dist/droid-cli/package.json +1 -1
  47. package/dist/extension.js +14287 -9568
  48. package/dist/pi-claude-cli/package.json +1 -1
  49. package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +218 -0
  50. package/dist/plugins/fusion-plugin-cursor-runtime/manifest.json +6 -0
  51. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +11 -0
  52. package/dist/plugins/fusion-plugin-dependency-graph/manifest.json +1 -1
  53. package/dist/plugins/fusion-plugin-dependency-graph/package.json +6 -4
  54. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.css +58 -0
  55. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.tsx +301 -0
  56. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphHighlight.css +27 -0
  57. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.css +157 -0
  58. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.tsx +126 -0
  59. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.css +35 -0
  60. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.tsx +36 -0
  61. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.highlighting.test.tsx +112 -0
  62. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.persistence.test.tsx +115 -0
  63. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.test.tsx +128 -0
  64. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.drag.test.tsx +82 -0
  65. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.test.tsx +307 -0
  66. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphToolbar.test.tsx +60 -0
  67. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/edges.test.tsx +75 -0
  68. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filtering.test.tsx +62 -0
  69. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filters.test.ts +78 -0
  70. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/graphPositionStorage.test.ts +95 -0
  71. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/host-integration.test.ts +74 -0
  72. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/index.test.ts +58 -0
  73. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/interactions.test.tsx +121 -0
  74. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/layout.test.ts +70 -0
  75. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/persistence.test.tsx +89 -0
  76. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphData.test.ts +86 -0
  77. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphInteraction.test.ts +167 -0
  78. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphPositions.test.ts +66 -0
  79. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useNodeDrag.test.ts +81 -0
  80. package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-interop.d.ts +35 -0
  81. package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-view.tsx +19 -0
  82. package/dist/plugins/fusion-plugin-dependency-graph/src/edges.tsx +70 -0
  83. package/dist/plugins/fusion-plugin-dependency-graph/src/filters.ts +8 -0
  84. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/__tests__/useDependencyChain.test.ts +53 -0
  85. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useDependencyChain.ts +60 -0
  86. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useGraphPositions.ts +45 -0
  87. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useNodeDrag.ts +114 -0
  88. package/dist/plugins/fusion-plugin-dependency-graph/src/index.ts +1 -2
  89. package/dist/plugins/fusion-plugin-dependency-graph/src/layout.ts +91 -0
  90. package/dist/plugins/fusion-plugin-dependency-graph/src/styles/drag.css +15 -0
  91. package/dist/plugins/fusion-plugin-dependency-graph/src/types.ts +21 -0
  92. package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphData.ts +17 -0
  93. package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphInteraction.ts +292 -0
  94. package/dist/plugins/fusion-plugin-dependency-graph/src/utils/graphPositionStorage.ts +65 -0
  95. package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +136680 -0
  96. package/dist/plugins/fusion-plugin-droid-runtime/manifest.json +13 -0
  97. package/dist/plugins/fusion-plugin-droid-runtime/mcp-schema-server.cjs +49 -0
  98. package/dist/plugins/fusion-plugin-droid-runtime/package.json +11 -0
  99. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  100. package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +93 -6
  101. package/dist/plugins/fusion-plugin-openclaw-runtime/mcp-schema-server.cjs +59 -0
  102. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  103. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  104. package/dist/plugins/fusion-plugin-reports/manifest.json +33 -0
  105. package/dist/plugins/fusion-plugin-reports/package.json +26 -0
  106. package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +51 -0
  107. package/dist/plugins/fusion-plugin-reports/src/__tests__/review-panel.test.ts +166 -0
  108. package/dist/plugins/fusion-plugin-reports/src/__tests__/settings.test.ts +157 -0
  109. package/dist/plugins/fusion-plugin-reports/src/index.ts +41 -0
  110. package/dist/plugins/fusion-plugin-reports/src/review-panel.ts +294 -0
  111. package/dist/plugins/fusion-plugin-reports/src/review-types.ts +75 -0
  112. package/dist/plugins/fusion-plugin-reports/src/settings.ts +105 -0
  113. package/dist/plugins/fusion-plugin-roadmap/manifest.json +16 -0
  114. package/dist/plugins/fusion-plugin-roadmap/package.json +48 -0
  115. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/api-client.test.ts +101 -0
  116. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/index.test.ts +92 -0
  117. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-routes.test.ts +48 -0
  118. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-suggestions.test.ts +31 -0
  119. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.css +1299 -0
  120. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx +2559 -0
  121. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.test.tsx +1144 -0
  122. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/useRoadmaps.test.ts +1756 -0
  123. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/api.ts +70 -0
  124. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/test-setup.ts +7 -0
  125. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/types.ts +1 -0
  126. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useConfirm.ts +8 -0
  127. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useRoadmaps.ts +1188 -0
  128. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useViewportMode.ts +20 -0
  129. package/dist/plugins/fusion-plugin-roadmap/src/dashboard-view.tsx +6 -0
  130. package/dist/plugins/fusion-plugin-roadmap/src/index.ts +74 -0
  131. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-routes.ts +1 -0
  132. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-schema.ts +41 -0
  133. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.d.ts +15 -0
  134. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.ts +15 -0
  135. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts +283 -0
  136. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts.map +1 -0
  137. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js +21 -0
  138. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js.map +1 -0
  139. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.ts +310 -0
  140. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts +5 -0
  141. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts.map +1 -0
  142. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js +361 -0
  143. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js.map +1 -0
  144. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.ts +408 -0
  145. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts +68 -0
  146. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts.map +1 -0
  147. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js +300 -0
  148. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js.map +1 -0
  149. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.ts +381 -0
  150. package/dist/plugins/fusion-plugin-roadmap/src/server/index.d.ts +3 -0
  151. package/dist/plugins/fusion-plugin-roadmap/src/server/index.ts +1 -0
  152. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-handoff.test.ts +445 -0
  153. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-ordering.test.ts +334 -0
  154. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-store.test.ts +1318 -0
  155. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-handoff.ts +163 -0
  156. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts +37 -0
  157. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts.map +1 -0
  158. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js +188 -0
  159. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js.map +1 -0
  160. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.ts +311 -0
  161. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts +299 -0
  162. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts.map +1 -0
  163. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js +765 -0
  164. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js.map +1 -0
  165. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.ts +1001 -0
  166. package/dist/plugins/fusion-plugin-whatsapp-chat/manifest.json +8 -0
  167. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +34 -0
  168. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/auth-state.test.ts +99 -0
  169. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/connection.test.ts +145 -0
  170. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/index.test.ts +216 -0
  171. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/reply.test.ts +52 -0
  172. package/dist/plugins/fusion-plugin-whatsapp-chat/src/auth-state.ts +89 -0
  173. package/dist/plugins/fusion-plugin-whatsapp-chat/src/connection.ts +253 -0
  174. package/dist/plugins/fusion-plugin-whatsapp-chat/src/index.ts +262 -0
  175. package/dist/plugins/fusion-plugin-whatsapp-chat/src/qrcode.d.ts +1 -0
  176. package/dist/plugins/fusion-plugin-whatsapp-chat/src/reply.ts +37 -0
  177. package/package.json +2 -2
  178. package/skill/fusion/SKILL.md +2 -2
  179. package/skill/fusion/references/engine-tools.md +3 -0
  180. package/skill/fusion/references/extension-tools.md +39 -0
  181. package/skill/fusion/references/fusion-capabilities.md +3 -0
  182. package/dist/client/assets/AgentDetailView-C1XceMgi.js +0 -18
  183. package/dist/client/assets/AgentDetailView-CeO_1MK7.css +0 -1
  184. package/dist/client/assets/AgentsView-Deh125ss.js +0 -527
  185. package/dist/client/assets/ChatView-7D_RQDqT.js +0 -1
  186. package/dist/client/assets/InsightsView-AWo5o_81.css +0 -1
  187. package/dist/client/assets/InsightsView-jKjEFAx_.js +0 -11
  188. package/dist/client/assets/NodesView-Di2SvOhg.js +0 -14
  189. package/dist/client/assets/NodesView-fXqDk9ur.css +0 -1
  190. package/dist/client/assets/PluginManager-B9-NbQ8f.js +0 -1
  191. package/dist/client/assets/PluginManager-C1DbPaar.css +0 -1
  192. package/dist/client/assets/RoadmapsView-DHWjUoc8.js +0 -6
  193. package/dist/client/assets/SettingsModal-DHitIpsa.css +0 -1
  194. package/dist/client/assets/SettingsModal-DR_yirvK.js +0 -31
  195. package/dist/client/assets/SetupWizardModal-BtDMY9pa.js +0 -1
  196. package/dist/client/assets/agentSkills-B-w5wFHh.js +0 -1
  197. package/dist/client/assets/index-Bc6ZdGMz.css +0 -1
  198. package/dist/client/assets/index-D__RMku8.js +0 -694
  199. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.css +0 -141
  200. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.tsx +0 -428
  201. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraphView.test.tsx +0 -261
  202. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/storage.test.ts +0 -41
  203. package/dist/plugins/fusion-plugin-dependency-graph/src/storage.ts +0 -22
  204. /package/dist/client/assets/{RoadmapsView-DdGlfuu-.css → dashboard-view-DdGlfuu-.css} +0 -0
@@ -0,0 +1,157 @@
1
+ .graph-task-node {
2
+ position: absolute;
3
+ z-index: 1;
4
+ width: min(100%, var(--graph-task-node-width, calc(var(--space-2xl) * 9)));
5
+ max-width: var(--graph-task-node-max-width, calc(var(--space-2xl) * 9.5));
6
+ transition:
7
+ box-shadow var(--transition-fast),
8
+ z-index var(--transition-fast),
9
+ opacity var(--transition-fast),
10
+ border-color var(--transition-fast);
11
+ border: var(--btn-border-width) solid transparent;
12
+ border-radius: var(--radius-sm);
13
+ }
14
+
15
+ .graph-task-node:hover {
16
+ box-shadow: var(--shadow-md);
17
+ z-index: 2;
18
+ }
19
+
20
+ .graph-task-node--highlighted {
21
+ box-shadow: 0 0 0 var(--btn-border-width) var(--todo), var(--shadow-md);
22
+ z-index: 2;
23
+ }
24
+
25
+ .graph-task-node--dimmed {
26
+ opacity: 0.4;
27
+ }
28
+
29
+ .graph-task-node--active {
30
+ border-color: rgba(var(--in-progress-rgb), 0.5);
31
+ box-shadow: 0 0 0 var(--btn-border-width) rgba(var(--in-progress-rgb), 0.45), 0 0 0.75rem rgba(var(--in-progress-rgb), 0.5), 0 0 1.5rem rgba(var(--in-progress-rgb), 0.2);
32
+ transform: scale(1.01);
33
+ }
34
+
35
+ .graph-task-node--in-review {
36
+ border-inline-start: calc(var(--btn-border-width) * 3) solid var(--in-review);
37
+ border-start-start-radius: var(--radius-md);
38
+ border-end-start-radius: var(--radius-md);
39
+ }
40
+
41
+ .graph-task-active-indicator {
42
+ display: flex;
43
+ align-items: center;
44
+ min-height: 1.25rem;
45
+ width: 100%;
46
+ padding-inline: var(--space-sm);
47
+ background: rgba(var(--in-progress-rgb), 0.85);
48
+ border-radius: var(--radius-sm) var(--radius-sm) 0 0;
49
+ animation: graph-task-active-pulse calc(var(--transition-normal) * 8) ease-in-out infinite;
50
+ }
51
+
52
+ .graph-task-active-indicator-text {
53
+ font-family: var(--font-mono);
54
+ font-size: 0.625rem;
55
+ font-weight: 600;
56
+ line-height: 1;
57
+ letter-spacing: 0.04em;
58
+ text-transform: uppercase;
59
+ color: var(--card);
60
+ }
61
+
62
+ .graph-task-node .card {
63
+ width: 100%;
64
+ height: 100%;
65
+ }
66
+
67
+ .graph-task-node[data-current-step="0"] .card-steps-list .card-step-item:nth-child(1),
68
+ .graph-task-node[data-current-step="1"] .card-steps-list .card-step-item:nth-child(2),
69
+ .graph-task-node[data-current-step="2"] .card-steps-list .card-step-item:nth-child(3),
70
+ .graph-task-node[data-current-step="3"] .card-steps-list .card-step-item:nth-child(4),
71
+ .graph-task-node[data-current-step="4"] .card-steps-list .card-step-item:nth-child(5),
72
+ .graph-task-node[data-current-step="5"] .card-steps-list .card-step-item:nth-child(6),
73
+ .graph-task-node[data-current-step="6"] .card-steps-list .card-step-item:nth-child(7),
74
+ .graph-task-node[data-current-step="7"] .card-steps-list .card-step-item:nth-child(8),
75
+ .graph-task-node[data-current-step="8"] .card-steps-list .card-step-item:nth-child(9),
76
+ .graph-task-node[data-current-step="9"] .card-steps-list .card-step-item:nth-child(10),
77
+ .graph-task-node[data-current-step="10"] .card-steps-list .card-step-item:nth-child(11),
78
+ .graph-task-node[data-current-step="11"] .card-steps-list .card-step-item:nth-child(12),
79
+ .graph-task-node[data-current-step="12"] .card-steps-list .card-step-item:nth-child(13),
80
+ .graph-task-node[data-current-step="13"] .card-steps-list .card-step-item:nth-child(14),
81
+ .graph-task-node[data-current-step="14"] .card-steps-list .card-step-item:nth-child(15),
82
+ .graph-task-node[data-current-step="15"] .card-steps-list .card-step-item:nth-child(16),
83
+ .graph-task-node[data-current-step="16"] .card-steps-list .card-step-item:nth-child(17),
84
+ .graph-task-node[data-current-step="17"] .card-steps-list .card-step-item:nth-child(18),
85
+ .graph-task-node[data-current-step="18"] .card-steps-list .card-step-item:nth-child(19),
86
+ .graph-task-node[data-current-step="19"] .card-steps-list .card-step-item:nth-child(20) {
87
+ background: rgba(var(--in-progress-rgb), 0.15);
88
+ border-left: calc(var(--btn-border-width) * 2) solid var(--in-progress);
89
+ padding-left: var(--space-xs);
90
+ font-weight: 500;
91
+ }
92
+
93
+ .graph-task-node[data-current-step="0"] .card-steps-list .card-step-item:nth-child(1) .card-step-dot,
94
+ .graph-task-node[data-current-step="1"] .card-steps-list .card-step-item:nth-child(2) .card-step-dot,
95
+ .graph-task-node[data-current-step="2"] .card-steps-list .card-step-item:nth-child(3) .card-step-dot,
96
+ .graph-task-node[data-current-step="3"] .card-steps-list .card-step-item:nth-child(4) .card-step-dot,
97
+ .graph-task-node[data-current-step="4"] .card-steps-list .card-step-item:nth-child(5) .card-step-dot,
98
+ .graph-task-node[data-current-step="5"] .card-steps-list .card-step-item:nth-child(6) .card-step-dot,
99
+ .graph-task-node[data-current-step="6"] .card-steps-list .card-step-item:nth-child(7) .card-step-dot,
100
+ .graph-task-node[data-current-step="7"] .card-steps-list .card-step-item:nth-child(8) .card-step-dot,
101
+ .graph-task-node[data-current-step="8"] .card-steps-list .card-step-item:nth-child(9) .card-step-dot,
102
+ .graph-task-node[data-current-step="9"] .card-steps-list .card-step-item:nth-child(10) .card-step-dot,
103
+ .graph-task-node[data-current-step="10"] .card-steps-list .card-step-item:nth-child(11) .card-step-dot,
104
+ .graph-task-node[data-current-step="11"] .card-steps-list .card-step-item:nth-child(12) .card-step-dot,
105
+ .graph-task-node[data-current-step="12"] .card-steps-list .card-step-item:nth-child(13) .card-step-dot,
106
+ .graph-task-node[data-current-step="13"] .card-steps-list .card-step-item:nth-child(14) .card-step-dot,
107
+ .graph-task-node[data-current-step="14"] .card-steps-list .card-step-item:nth-child(15) .card-step-dot,
108
+ .graph-task-node[data-current-step="15"] .card-steps-list .card-step-item:nth-child(16) .card-step-dot,
109
+ .graph-task-node[data-current-step="16"] .card-steps-list .card-step-item:nth-child(17) .card-step-dot,
110
+ .graph-task-node[data-current-step="17"] .card-steps-list .card-step-item:nth-child(18) .card-step-dot,
111
+ .graph-task-node[data-current-step="18"] .card-steps-list .card-step-item:nth-child(19) .card-step-dot,
112
+ .graph-task-node[data-current-step="19"] .card-steps-list .card-step-item:nth-child(20) .card-step-dot {
113
+ width: 0.5rem;
114
+ height: 0.5rem;
115
+ animation: graph-task-step-pulse calc(var(--transition-normal) * 10) ease-in-out infinite;
116
+ }
117
+
118
+ @keyframes graph-task-active-pulse {
119
+ 0%,
120
+ 100% {
121
+ background: rgba(var(--in-progress-rgb), 0.85);
122
+ }
123
+ 50% {
124
+ background: rgba(var(--in-progress-rgb), 0.65);
125
+ }
126
+ }
127
+
128
+ @keyframes graph-task-step-pulse {
129
+ 0%,
130
+ 100% {
131
+ transform: scale(1);
132
+ opacity: 1;
133
+ }
134
+ 50% {
135
+ transform: scale(1.2);
136
+ opacity: 0.75;
137
+ }
138
+ }
139
+
140
+ @media (max-width: 768px) {
141
+ .graph-task-node {
142
+ width: min(100%, var(--graph-task-node-mobile-width, calc(var(--space-2xl) * 8)));
143
+ }
144
+
145
+ .graph-task-node--active {
146
+ transform: scale(1.005);
147
+ }
148
+
149
+ .graph-task-active-indicator {
150
+ min-height: 1.5rem;
151
+ padding-inline: var(--space-md);
152
+ }
153
+
154
+ .graph-task-active-indicator-text {
155
+ font-size: 0.6875rem;
156
+ }
157
+ }
@@ -0,0 +1,126 @@
1
+ import type { CSSProperties, ComponentProps, HTMLAttributes } from "react";
2
+ import type { GraphPosition } from "./types";
3
+ import { useNodeDrag } from "./hooks/useNodeDrag";
4
+ import { TaskCard } from "@fusion/dashboard/app/components/TaskCard";
5
+ import { isTaskStuck } from "@fusion/dashboard/app/utils/taskStuck";
6
+ import "./GraphTaskNode.css";
7
+ import "./GraphHighlight.css";
8
+ import "./styles/drag.css";
9
+
10
+ type TaskCardComponentProps = ComponentProps<typeof TaskCard>;
11
+
12
+ type TaskCardBridgeProps = Pick<
13
+ TaskCardComponentProps,
14
+ | "task"
15
+ | "projectId"
16
+ | "onOpenDetail"
17
+ | "addToast"
18
+ | "globalPaused"
19
+ | "onUpdateTask"
20
+ | "onArchiveTask"
21
+ | "onUnarchiveTask"
22
+ | "onDeleteTask"
23
+ | "onRetryTask"
24
+ | "onOpenDetailWithTab"
25
+ | "taskStuckTimeoutMs"
26
+ | "onOpenMission"
27
+ | "onMoveTask"
28
+ | "lastFetchTimeMs"
29
+ | "workflowStepNameLookup"
30
+ >;
31
+
32
+ export interface GraphTaskNodeProps extends TaskCardBridgeProps, Pick<HTMLAttributes<HTMLDivElement>, "onMouseEnter" | "onMouseLeave" | "onClick"> {
33
+ style?: CSSProperties;
34
+ position: GraphPosition;
35
+ scale: number;
36
+ isHighlighted?: boolean;
37
+ isDimmed?: boolean;
38
+ onNodePositionChange: (taskId: string, position: GraphPosition) => void;
39
+ onNodeDragStateChange?: (isDragging: boolean) => void;
40
+ onNodeDragEnd?: () => void;
41
+ }
42
+
43
+ const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging", "merging-fix"]);
44
+
45
+ function getStatusLabel(status?: string): string {
46
+ if (!status) {
47
+ return "Executing";
48
+ }
49
+
50
+ return status.charAt(0).toUpperCase() + status.slice(1);
51
+ }
52
+
53
+ export function GraphTaskNode({
54
+ style,
55
+ position,
56
+ scale,
57
+ isHighlighted = false,
58
+ isDimmed = false,
59
+ onMouseEnter,
60
+ onMouseLeave,
61
+ onClick,
62
+ onNodePositionChange,
63
+ onNodeDragStateChange,
64
+ onNodeDragEnd,
65
+ ...taskCardProps
66
+ }: GraphTaskNodeProps) {
67
+ const { task, globalPaused, taskStuckTimeoutMs, lastFetchTimeMs, onOpenDetail } = taskCardProps;
68
+ const isFailed = task.status === "failed";
69
+ const isPaused = task.paused === true;
70
+ const isStuck = isTaskStuck(task, taskStuckTimeoutMs, lastFetchTimeMs);
71
+ const isAwaitingApproval = task.column === "triage" && task.status === "awaiting-approval";
72
+ const isActive =
73
+ !globalPaused &&
74
+ !isFailed &&
75
+ !isPaused &&
76
+ !isStuck &&
77
+ !isAwaitingApproval &&
78
+ (task.column === "in-progress" || ACTIVE_STATUSES.has(task.status as string));
79
+
80
+ const hasValidCurrentStep =
81
+ typeof task.currentStep === "number" &&
82
+ task.currentStep >= 0 &&
83
+ Array.isArray(task.steps) &&
84
+ task.currentStep < task.steps.length;
85
+ const isInReview = task.column === "in-review";
86
+
87
+ const drag = useNodeDrag({
88
+ taskId: task.id,
89
+ position,
90
+ scale,
91
+ onPositionChange: onNodePositionChange,
92
+ onDragStateChange: onNodeDragStateChange,
93
+ onDragEnd: onNodeDragEnd,
94
+ });
95
+
96
+ return (
97
+ <div
98
+ className={`graph-task-node graph-node--draggable${drag.isDragging ? " graph-node--dragging" : ""}${isHighlighted ? " graph-task-node--highlighted graph-node--highlighted" : ""}${isDimmed ? " graph-task-node--dimmed graph-node--dimmed" : ""}${isActive ? " graph-task-node--active" : ""}${isInReview ? " graph-task-node--in-review" : ""}`}
99
+ style={style}
100
+ draggable={false}
101
+ data-testid={`graph-task-node-${task.id}`}
102
+ data-current-step={isActive && hasValidCurrentStep ? String(task.currentStep) : undefined}
103
+ onMouseEnter={onMouseEnter}
104
+ onMouseLeave={onMouseLeave}
105
+ onClick={(event) => {
106
+ onClick?.(event);
107
+ if (event.defaultPrevented) {
108
+ return;
109
+ }
110
+ onOpenDetail(task);
111
+ }}
112
+ onClickCapture={drag.onClickCapture}
113
+ onPointerDown={drag.onPointerDown}
114
+ onPointerMove={drag.onPointerMove}
115
+ onPointerUp={drag.onPointerUp}
116
+ onPointerCancel={drag.onPointerCancel}
117
+ >
118
+ {isActive ? (
119
+ <div className="graph-task-active-indicator">
120
+ <span className="graph-task-active-indicator-text">{getStatusLabel(task.status)}</span>
121
+ </div>
122
+ ) : null}
123
+ <TaskCard {...taskCardProps} onOpenDetail={() => {}} disableDrag={true} />
124
+ </div>
125
+ );
126
+ }
@@ -0,0 +1,35 @@
1
+ .graph-toolbar {
2
+ position: absolute;
3
+ right: var(--space-md);
4
+ bottom: var(--space-md);
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: var(--space-xs);
8
+ padding: var(--space-sm);
9
+ background: var(--surface);
10
+ border: var(--btn-border-width) solid var(--border);
11
+ border-radius: var(--radius-md);
12
+ box-shadow: var(--shadow-md);
13
+ z-index: 10;
14
+ }
15
+
16
+ .graph-toolbar__zoom-label {
17
+ min-width: 3.5ch;
18
+ color: var(--text-muted);
19
+ font-family: var(--font-mono);
20
+ font-size: 0.75rem;
21
+ text-align: center;
22
+ }
23
+
24
+ @media (max-width: 768px) {
25
+ .graph-toolbar {
26
+ right: var(--space-sm);
27
+ bottom: var(--space-sm);
28
+ padding: var(--space-md);
29
+ }
30
+
31
+ .graph-toolbar .btn-icon {
32
+ min-width: calc(var(--space-xs) * 11);
33
+ min-height: calc(var(--space-xs) * 11);
34
+ }
35
+ }
@@ -0,0 +1,36 @@
1
+ import { Maximize, RotateCcw, ZoomIn, ZoomOut } from "lucide-react";
2
+ import "./GraphToolbar.css";
3
+
4
+ export interface GraphToolbarProps {
5
+ zoom: number;
6
+ onZoomIn: () => void;
7
+ onZoomOut: () => void;
8
+ onFitToGraph: () => void;
9
+ onResetView: () => void;
10
+ }
11
+
12
+ export function GraphToolbar({
13
+ zoom,
14
+ onZoomIn,
15
+ onZoomOut,
16
+ onFitToGraph,
17
+ onResetView,
18
+ }: GraphToolbarProps) {
19
+ return (
20
+ <div className="graph-toolbar" data-testid="graph-toolbar">
21
+ <button className="btn btn-icon" title="Zoom in (Ctrl+=)" aria-label="Zoom in" onClick={onZoomIn}>
22
+ <ZoomIn size={16} />
23
+ </button>
24
+ <button className="btn btn-icon" title="Zoom out (Ctrl+-)" aria-label="Zoom out" onClick={onZoomOut}>
25
+ <ZoomOut size={16} />
26
+ </button>
27
+ <div className="graph-toolbar__zoom-label" aria-live="polite">{Math.round(zoom * 100)}%</div>
28
+ <button className="btn btn-icon" title="Fit to graph (Ctrl+Shift+F)" aria-label="Fit to graph" onClick={onFitToGraph}>
29
+ <Maximize size={16} />
30
+ </button>
31
+ <button className="btn btn-icon" title="Reset view (Ctrl+0)" aria-label="Reset view" onClick={onResetView}>
32
+ <RotateCcw size={16} />
33
+ </button>
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,112 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3
+ import type { Task } from "@fusion/core";
4
+ import { DependencyGraph } from "../DependencyGraph";
5
+
6
+ vi.mock("@fusion/dashboard/app/components/TaskCard", () => ({
7
+ TaskCard: ({ task, onOpenDetail }: { task: Task; onOpenDetail: (task: Task) => void }) => (
8
+ <button data-testid={`task-${task.id}`} onClick={() => onOpenDetail(task)}>{task.id}</button>
9
+ ),
10
+ }));
11
+
12
+ vi.mock("../useGraphInteraction", () => ({
13
+ useGraphInteraction: () => ({
14
+ transform: "translate(0px, 0px) scale(1)",
15
+ zoom: 1,
16
+ transitioning: false,
17
+ zoomIn: vi.fn(),
18
+ zoomOut: vi.fn(),
19
+ resetView: vi.fn(),
20
+ fitToGraph: vi.fn(),
21
+ onPointerDown: vi.fn(),
22
+ onPointerMove: vi.fn(),
23
+ onPointerUp: vi.fn(),
24
+ onWheelZoom: vi.fn(),
25
+ handleKeyDown: vi.fn(),
26
+ }),
27
+ }));
28
+
29
+ function createTask(id: string, dependencies: string[] = []): Task {
30
+ return {
31
+ id,
32
+ description: id,
33
+ column: "todo",
34
+ dependencies,
35
+ steps: [],
36
+ currentStep: 0,
37
+ log: [],
38
+ createdAt: new Date().toISOString(),
39
+ updatedAt: new Date().toISOString(),
40
+ } as Task;
41
+ }
42
+
43
+ afterEach(() => {
44
+ cleanup();
45
+ });
46
+
47
+ describe("DependencyGraph highlighting", () => {
48
+ const tasks = [createTask("A"), createTask("B", ["A"]), createTask("C", ["B"]), createTask("D")];
49
+
50
+ it("highlights chain on hover and returns to neutral on mouse leave", () => {
51
+ render(<DependencyGraph tasks={tasks} onOpenDetail={vi.fn()} />);
52
+
53
+ fireEvent.mouseEnter(screen.getByTestId("graph-task-node-C"));
54
+ expect(screen.getByTestId("graph-task-node-A").className).toContain("graph-task-node--highlighted");
55
+ expect(screen.getByTestId("graph-task-node-B").className).toContain("graph-task-node--highlighted");
56
+ expect(screen.getByTestId("graph-task-node-C").className).toContain("graph-task-node--highlighted");
57
+ expect(screen.getByTestId("graph-task-node-D").className).toContain("graph-task-node--dimmed");
58
+
59
+ fireEvent.mouseLeave(screen.getByTestId("graph-task-node-C"));
60
+ expect(screen.getByTestId("graph-task-node-A").className).not.toContain("graph-task-node--highlighted");
61
+ expect(screen.getByTestId("graph-task-node-D").className).not.toContain("graph-task-node--dimmed");
62
+ });
63
+
64
+ it("keeps selection until toggled or pane clicked", () => {
65
+ render(<DependencyGraph tasks={tasks} onOpenDetail={vi.fn()} />);
66
+
67
+ fireEvent.click(screen.getByTestId("graph-task-node-B"));
68
+ expect(screen.getByTestId("graph-task-node-A").className).toContain("graph-task-node--highlighted");
69
+
70
+ fireEvent.click(screen.getByTestId("graph-task-node-B"));
71
+ expect(screen.getByTestId("graph-task-node-A").className).not.toContain("graph-task-node--highlighted");
72
+
73
+ fireEvent.click(screen.getByTestId("graph-task-node-B"));
74
+ fireEvent.click(document.querySelector(".dependency-graph__viewport") as Element);
75
+ expect(screen.getByTestId("graph-task-node-B").className).not.toContain("graph-task-node--highlighted");
76
+ });
77
+
78
+ it("hover overrides selection and reverts when hover leaves", () => {
79
+ render(<DependencyGraph tasks={tasks} onOpenDetail={vi.fn()} />);
80
+
81
+ fireEvent.click(screen.getByTestId("graph-task-node-B"));
82
+ fireEvent.mouseEnter(screen.getByTestId("graph-task-node-D"));
83
+ expect(screen.getByTestId("graph-task-node-D").className).toContain("graph-task-node--highlighted");
84
+ expect(screen.getByTestId("graph-task-node-A").className).toContain("graph-task-node--dimmed");
85
+
86
+ fireEvent.mouseLeave(screen.getByTestId("graph-task-node-D"));
87
+ expect(screen.getByTestId("graph-task-node-A").className).toContain("graph-task-node--highlighted");
88
+ });
89
+
90
+ it("applies edge dimming/highlighting and preserves click-to-detail", () => {
91
+ const onOpenDetail = vi.fn();
92
+ render(<DependencyGraph tasks={tasks} onOpenDetail={onOpenDetail} />);
93
+
94
+ fireEvent.mouseEnter(screen.getByTestId("graph-task-node-C"));
95
+ const edges = screen.getAllByTestId("dependency-edge");
96
+ const edgeAB = edges.find((edge) => edge.getAttribute("data-edge-id") === "B->A");
97
+ const edgeCB = edges.find((edge) => edge.getAttribute("data-edge-id") === "C->B");
98
+
99
+ expect(edgeAB?.className.baseVal || edgeAB?.className).toContain("graph-edge--highlighted");
100
+ expect(edgeCB?.className.baseVal || edgeCB?.className).toContain("graph-edge--highlighted");
101
+
102
+ fireEvent.click(screen.getByTestId("task-C"));
103
+ expect(onOpenDetail).toHaveBeenCalledTimes(1);
104
+ expect(onOpenDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "C" }));
105
+ });
106
+
107
+ it("highlights only isolated node with no dependencies", () => {
108
+ render(<DependencyGraph tasks={[createTask("X")]} onOpenDetail={vi.fn()} />);
109
+ fireEvent.mouseEnter(screen.getByTestId("graph-task-node-X"));
110
+ expect(screen.getByTestId("graph-task-node-X").className).toContain("graph-task-node--highlighted");
111
+ });
112
+ });
@@ -0,0 +1,115 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3
+ import type { Task } from "@fusion/core";
4
+ import { DependencyGraph } from "../DependencyGraph";
5
+
6
+ const fitToGraph = vi.fn();
7
+
8
+ vi.mock("@fusion/dashboard/app/components/TaskCard", () => ({
9
+ TaskCard: ({ task, onOpenDetail }: { task: Task; onOpenDetail: (task: Task) => void }) => (
10
+ <button data-testid={`task-${task.id}`} onClick={() => onOpenDetail(task)}>{task.id}</button>
11
+ ),
12
+ }));
13
+
14
+ vi.mock("../useGraphInteraction", () => ({
15
+ useGraphInteraction: () => ({
16
+ transform: "translate(0px, 0px) scale(1)",
17
+ zoom: 1,
18
+ transitioning: false,
19
+ zoomIn: vi.fn(),
20
+ zoomOut: vi.fn(),
21
+ resetView: vi.fn(),
22
+ fitToGraph,
23
+ onPointerDown: vi.fn(),
24
+ onPointerMove: vi.fn(),
25
+ onPointerUp: vi.fn(),
26
+ onWheelZoom: vi.fn(),
27
+ handleKeyDown: vi.fn(),
28
+ }),
29
+ }));
30
+
31
+ vi.mock("../layout", () => ({
32
+ computeAutoLayout: ({ nodes }: { nodes: Array<{ task: { id: string } }> }) => {
33
+ const map = new Map<string, { x: number; y: number }>();
34
+ for (const node of nodes) {
35
+ if (node.task.id === "A") map.set("A", { x: 0, y: 0 });
36
+ if (node.task.id === "B") map.set("B", { x: 200, y: 0 });
37
+ }
38
+ return map;
39
+ },
40
+ }));
41
+
42
+ function createStorage() {
43
+ const store = new Map<string, string>();
44
+ return {
45
+ getItem: (key: string) => store.get(key) ?? null,
46
+ setItem: (key: string, value: string) => {
47
+ store.set(key, value);
48
+ },
49
+ removeItem: (key: string) => {
50
+ store.delete(key);
51
+ },
52
+ };
53
+ }
54
+
55
+ function createTask(id: string, column: Task["column"] = "todo"): Task {
56
+ return { id, description: id, column, dependencies: [], steps: [], currentStep: 0, log: [] } as Task;
57
+ }
58
+
59
+ describe("DependencyGraph persistence", () => {
60
+ afterEach(() => {
61
+ cleanup();
62
+ });
63
+
64
+ beforeEach(() => {
65
+ Object.defineProperty(window, "localStorage", { value: createStorage(), configurable: true });
66
+ fitToGraph.mockReset();
67
+ });
68
+
69
+ it("persists dragged node position across remount", () => {
70
+ const { unmount } = render(<DependencyGraph tasks={[createTask("A")]} projectId="p1" />);
71
+
72
+ const node = screen.getByTestId("graph-task-node-A");
73
+ fireEvent.pointerDown(node, { pointerId: 1, isPrimary: true, clientX: 10, clientY: 10 });
74
+ fireEvent.pointerMove(node, { pointerId: 1, isPrimary: true, clientX: 30, clientY: 40 });
75
+ fireEvent.pointerUp(node, { pointerId: 1, isPrimary: true, clientX: 30, clientY: 40 });
76
+
77
+ expect(window.localStorage.getItem("kb:p1:fusion-plugin-dependency-graph:positions")).toContain('"A":{"x":20,"y":30}');
78
+
79
+ unmount();
80
+ render(<DependencyGraph tasks={[createTask("A")]} projectId="p1" />);
81
+
82
+ expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("left: 20px");
83
+ expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("top: 30px");
84
+ });
85
+
86
+ it("merges saved positions with auto-layout for new tasks", () => {
87
+ window.localStorage.setItem("kb:p1:fusion-plugin-dependency-graph:positions", JSON.stringify({ A: { x: 25, y: 35 } }));
88
+
89
+ render(<DependencyGraph tasks={[createTask("A"), createTask("B")]} projectId="p1" />);
90
+
91
+ expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("left: 25px");
92
+ expect(screen.getByTestId("graph-task-node-B").getAttribute("style")).toContain("left: 200px");
93
+ });
94
+
95
+ it("fit to graph clears saved positions and reapplies auto-layout", () => {
96
+ window.localStorage.setItem("kb:p1:fusion-plugin-dependency-graph:positions", JSON.stringify({ A: { x: 25, y: 35 } }));
97
+
98
+ render(<DependencyGraph tasks={[createTask("A")]} projectId="p1" />);
99
+ fireEvent.click(screen.getByRole("button", { name: "Fit to graph" }));
100
+
101
+ expect(window.localStorage.getItem("kb:p1:fusion-plugin-dependency-graph:positions")).toBeNull();
102
+ expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("left: 0px");
103
+ });
104
+
105
+ it("switching projects loads project-scoped positions", () => {
106
+ window.localStorage.setItem("kb:p1:fusion-plugin-dependency-graph:positions", JSON.stringify({ A: { x: 11, y: 22 } }));
107
+ window.localStorage.setItem("kb:p2:fusion-plugin-dependency-graph:positions", JSON.stringify({ A: { x: 33, y: 44 } }));
108
+
109
+ const { rerender } = render(<DependencyGraph tasks={[createTask("A")]} projectId="p1" />);
110
+ expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("left: 11px");
111
+
112
+ rerender(<DependencyGraph tasks={[createTask("A")]} projectId="p2" />);
113
+ expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("left: 33px");
114
+ });
115
+ });