@runfusion/fusion 0.15.0 → 0.16.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 (65) hide show
  1. package/dist/bin.js +3946 -2118
  2. package/dist/client/assets/AgentDetailView-BoEpPOjM.css +1 -0
  3. package/dist/client/assets/AgentDetailView-KQz9dg0n.js +18 -0
  4. package/dist/client/assets/AgentsView-8Xo-c04o.js +517 -0
  5. package/dist/client/assets/ChatView-CjgOh8x7.js +1 -0
  6. package/dist/client/assets/DevServerView-BOPrzi-j.js +1 -0
  7. package/dist/client/assets/{DirectoryPicker-DPfkGnj5.js → DirectoryPicker-CFj7FOgn.js} +1 -1
  8. package/dist/client/assets/DocumentsView-CcJNmH06.js +1 -0
  9. package/dist/client/assets/{InsightsView-BKhvyEyQ.js → InsightsView-B0DKRIYV.js} +1 -1
  10. package/dist/client/assets/MemoryView-DDbr1DaJ.js +2 -0
  11. package/dist/client/assets/NodesView-BprfihLf.css +1 -0
  12. package/dist/client/assets/NodesView-BqtHfXsl.js +14 -0
  13. package/dist/client/assets/{PiExtensionsManager-C4fTzemh.js → PiExtensionsManager-QgzWFBAT.js} +3 -3
  14. package/dist/client/assets/{PluginManager-C2-dExUL.js → PluginManager-KHiz9oLY.js} +1 -1
  15. package/dist/client/assets/ResearchView-CVxPC1vz.css +1 -0
  16. package/dist/client/assets/ResearchView-s3SrjNBm.js +1 -0
  17. package/dist/client/assets/RoadmapsView-BlbAZTdi.js +6 -0
  18. package/dist/client/assets/RoadmapsView-DdGlfuu-.css +1 -0
  19. package/dist/client/assets/SettingsModal-D0QA2W5K.css +1 -0
  20. package/dist/client/assets/{SettingsModal-BGnSAeqa.js → SettingsModal-D5EUFR2z.js} +1 -1
  21. package/dist/client/assets/SettingsModal-c9MG4sxl.js +31 -0
  22. package/dist/client/assets/{SetupWizardModal-C_d9clJp.js → SetupWizardModal-tTXAm1Wb.js} +1 -1
  23. package/dist/client/assets/SkillsView-CS4ONN3D.js +1 -0
  24. package/dist/client/assets/{folder-open-CKivQd8c.js → folder-open-DObdkm5J.js} +1 -1
  25. package/dist/client/assets/index-BRaIPmpp.js +682 -0
  26. package/dist/client/assets/index-DeED_ky2.css +1 -0
  27. package/dist/client/assets/{star-damu_EYz.js → star-BkGA2L-k.js} +1 -1
  28. package/dist/client/assets/{upload-uH6CHlEw.js → upload-B0NF4J5P.js} +1 -1
  29. package/dist/client/assets/{users-CUySbfji.js → users-DgomiHTd.js} +1 -1
  30. package/dist/client/index.html +2 -2
  31. package/dist/client/version.json +1 -1
  32. package/dist/extension.js +4126 -3248
  33. package/dist/pi-claude-cli/package.json +1 -1
  34. package/dist/plugins/fusion-plugin-dependency-graph/manifest.json +16 -0
  35. package/dist/plugins/fusion-plugin-dependency-graph/package.json +32 -0
  36. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.css +101 -0
  37. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.tsx +320 -0
  38. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/storage.test.ts +31 -0
  39. package/dist/plugins/fusion-plugin-dependency-graph/src/index.ts +25 -0
  40. package/dist/plugins/fusion-plugin-dependency-graph/src/storage.ts +23 -0
  41. package/package.json +8 -4
  42. package/skill/fusion/references/engine-tools.md +2 -2
  43. package/dist/client/assets/AgentDetailView-B1zViykq.js +0 -18
  44. package/dist/client/assets/AgentDetailView-B5tq9ius.css +0 -1
  45. package/dist/client/assets/AgentsView-Bl9JH5C8.js +0 -522
  46. package/dist/client/assets/ChatView-liNErE53.js +0 -1
  47. package/dist/client/assets/DevServerView-CV_PpbnZ.js +0 -1
  48. package/dist/client/assets/DocumentsView-CESb6RI7.js +0 -1
  49. package/dist/client/assets/MemoryView-DB-l2miV.js +0 -2
  50. package/dist/client/assets/NodesView-DCoS6iYh.css +0 -1
  51. package/dist/client/assets/NodesView-DgTXO8mm.js +0 -14
  52. package/dist/client/assets/ResearchView-BzRdUzNq.css +0 -1
  53. package/dist/client/assets/ResearchView-CkVwRDVA.js +0 -1
  54. package/dist/client/assets/RoadmapsView-BOYnyMCh.css +0 -1
  55. package/dist/client/assets/RoadmapsView-Cu85_XrQ.js +0 -6
  56. package/dist/client/assets/SettingsModal-C0DokcId.js +0 -31
  57. package/dist/client/assets/SettingsModal-DcGFm6NR.css +0 -1
  58. package/dist/client/assets/SkillMultiselect-DDHJnrkn.css +0 -1
  59. package/dist/client/assets/SkillMultiselect-DwGWYZi6.js +0 -1
  60. package/dist/client/assets/SkillsView-C096TB7i.js +0 -1
  61. package/dist/client/assets/TodoView-CUiAt2mR.js +0 -6
  62. package/dist/client/assets/TodoView-SeO9o7km.css +0 -1
  63. package/dist/client/assets/index-B4StE1qN.js +0 -662
  64. package/dist/client/assets/index-DYJk0WDc.css +0 -1
  65. package/dist/client/assets/list-checks-B3oufblU.js +0 -6
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusion/pi-claude-cli",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
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,
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "fusion-plugin-dependency-graph",
3
+ "name": "Dependency Graph",
4
+ "version": "0.1.0",
5
+ "description": "Top-level dependency graph dashboard view",
6
+ "dashboardViews": [
7
+ {
8
+ "viewId": "graph",
9
+ "label": "Graph",
10
+ "componentPath": "./src/DependencyGraphView.tsx",
11
+ "icon": "Network",
12
+ "placement": "more",
13
+ "order": 40
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@fusion-plugin-examples/dependency-graph",
3
+ "version": "0.1.2",
4
+ "type": "module",
5
+ "description": "Dependency graph dashboard view plugin for Fusion",
6
+ "private": true,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/index.ts",
10
+ "import": "./src/index.ts"
11
+ },
12
+ "./dashboard-view": {
13
+ "types": "./src/DependencyGraphView.tsx",
14
+ "import": "./src/DependencyGraphView.tsx"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "test": "vitest run --silent=passed-only --reporter=dot"
20
+ },
21
+ "dependencies": {
22
+ "@fusion/plugin-sdk": "workspace:*",
23
+ "@fusion/core": "workspace:*"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^25.5.2",
27
+ "@types/react": "^19.0.0",
28
+ "typescript": "^5.7.0",
29
+ "vitest": "^3.2.4",
30
+ "react": "^19.0.0"
31
+ }
32
+ }
@@ -0,0 +1,101 @@
1
+ .dependency-graph-view {
2
+ --dependency-graph-canvas-min-height: calc(var(--space-2xl) * 10);
3
+ --dependency-graph-canvas-min-height-mobile: calc(var(--space-2xl) * 8);
4
+ --dependency-graph-edge-width: var(--btn-border-width);
5
+ --dependency-graph-node-max-width-mobile: calc(var(--space-2xl) * 10);
6
+
7
+ display: flex;
8
+ flex-direction: column;
9
+ gap: var(--space-md);
10
+ padding: var(--space-lg);
11
+ }
12
+
13
+ .dependency-graph-controls {
14
+ display: flex;
15
+ flex-wrap: wrap;
16
+ gap: var(--space-sm);
17
+ }
18
+
19
+ .dependency-graph-canvas {
20
+ overflow: auto;
21
+ border: var(--btn-border-width) solid var(--border);
22
+ border-radius: var(--radius-md);
23
+ background: var(--surface);
24
+ min-height: var(--dependency-graph-canvas-min-height);
25
+ cursor: grab;
26
+ }
27
+
28
+ .dependency-graph-canvas:active {
29
+ cursor: grabbing;
30
+ }
31
+
32
+ .dependency-graph-scene {
33
+ position: relative;
34
+ transition: transform var(--transition-fast);
35
+ }
36
+
37
+ .dependency-graph-edges {
38
+ position: absolute;
39
+ inset: 0;
40
+ width: 100%;
41
+ height: 100%;
42
+ pointer-events: none;
43
+ }
44
+
45
+ .dependency-graph-edge {
46
+ stroke: var(--border);
47
+ stroke-width: var(--dependency-graph-edge-width);
48
+ transition: stroke var(--transition-fast), opacity var(--transition-fast);
49
+ }
50
+
51
+ .dependency-graph-edge.is-related {
52
+ stroke: var(--todo);
53
+ }
54
+
55
+ .dependency-graph-edge.is-dimmed {
56
+ opacity: 0.4;
57
+ }
58
+
59
+ .dependency-graph-node {
60
+ position: absolute;
61
+ top: 0;
62
+ left: 0;
63
+ cursor: grab;
64
+ transition: opacity var(--transition-fast), filter var(--transition-fast), box-shadow var(--transition-fast);
65
+ }
66
+
67
+ .dependency-graph-node:active {
68
+ cursor: grabbing;
69
+ }
70
+
71
+ .dependency-graph-node .card {
72
+ height: 100%;
73
+ }
74
+
75
+ .dependency-graph-node.is-selected .card {
76
+ box-shadow: var(--focus-ring-strong);
77
+ border-color: var(--todo);
78
+ }
79
+
80
+ .dependency-graph-node.is-related:not(.is-selected) .card {
81
+ border-color: var(--in-progress);
82
+ }
83
+
84
+ .dependency-graph-node.is-dimmed {
85
+ opacity: 0.5;
86
+ filter: saturate(0.8);
87
+ }
88
+
89
+ @media (max-width: 768px) {
90
+ .dependency-graph-view {
91
+ padding: var(--space-md);
92
+ }
93
+
94
+ .dependency-graph-canvas {
95
+ min-height: var(--dependency-graph-canvas-min-height-mobile);
96
+ }
97
+
98
+ .dependency-graph-node {
99
+ width: min(100%, var(--dependency-graph-node-max-width-mobile)) !important;
100
+ }
101
+ }
@@ -0,0 +1,320 @@
1
+ import { useMemo, useRef, useState } from "react";
2
+ import type { PointerEvent as ReactPointerEvent, ReactNode } from "react";
3
+ import type { Task } from "@fusion/core";
4
+ import { loadPositions, savePositions } from "./storage";
5
+ import "./DependencyGraphView.css";
6
+
7
+ const ACTIVE_COLUMNS = new Set(["triage", "todo", "in-progress", "in-review"]);
8
+ const NODE_WIDTH_REM = 18;
9
+ const NODE_HEIGHT_REM = 9;
10
+ const GRID_GAP_X_REM = 3;
11
+ const GRID_GAP_Y_REM = 4;
12
+ const DRAG_THRESHOLD_REM = 0.5;
13
+ const SCENE_PADDING_REM = 2;
14
+ const FIT_PADDING_REM = 2;
15
+ const MIN_SCALE = 0.4;
16
+ const MAX_SCALE = 2;
17
+
18
+ export interface DependencyGraphHostContext {
19
+ projectId?: string;
20
+ tasks: Task[];
21
+ openTaskDetail: (task: Task) => void;
22
+ renderTaskCard: (task: Task) => ReactNode;
23
+ }
24
+
25
+ export interface PluginDashboardViewComponentProps {
26
+ context: DependencyGraphHostContext;
27
+ }
28
+
29
+ type Position = { x: number; y: number };
30
+
31
+ 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);
35
+ }
36
+
37
+ export function DependencyGraphView({ context }: PluginDashboardViewComponentProps) {
38
+ const [scale, setScale] = useState(1);
39
+ const [pan, setPan] = useState<Position>({ x: 0, y: 0 });
40
+ const [nodeOverrides, setNodeOverrides] = useState<Record<string, Position>>({});
41
+ const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
42
+ const [hoveredTaskId, setHoveredTaskId] = useState<string | null>(null);
43
+ const persisted = useMemo(() => loadPositions(context.projectId), [context.projectId]);
44
+ const canvasRef = useRef<HTMLDivElement | null>(null);
45
+ const interactionRef = useRef<
46
+ | { kind: "node"; taskId: string; startPointer: Position; startNode: Position; moved: boolean }
47
+ | { kind: "pan"; startPointer: Position; startPan: Position; moved: boolean }
48
+ | null
49
+ >(null);
50
+
51
+ const tasks = useMemo(
52
+ () => context.tasks.filter((task) => ACTIVE_COLUMNS.has(task.column)),
53
+ [context.tasks],
54
+ );
55
+
56
+ const positioned = useMemo(() => {
57
+ return tasks.map((task, index) => {
58
+ const saved = nodeOverrides[task.id] ?? persisted[task.id];
59
+ return {
60
+ task,
61
+ x: saved?.x ?? (index % 4) * (NODE_WIDTH_REM + GRID_GAP_X_REM),
62
+ y: saved?.y ?? Math.floor(index / 4) * (NODE_HEIGHT_REM + GRID_GAP_Y_REM),
63
+ };
64
+ });
65
+ }, [nodeOverrides, persisted, tasks]);
66
+
67
+ const map = useMemo(() => new Map(positioned.map((node) => [node.task.id, node])), [positioned]);
68
+
69
+ const edges = useMemo(() => {
70
+ const lines: Array<{ from: string; to: string; x1: number; y1: number; x2: number; y2: number }> = [];
71
+ positioned.forEach((node) => {
72
+ (node.task.dependencies ?? []).forEach((dependencyId) => {
73
+ const dependency = map.get(dependencyId);
74
+ if (!dependency) return;
75
+ lines.push({
76
+ from: dependencyId,
77
+ to: node.task.id,
78
+ x1: dependency.x + NODE_WIDTH_REM,
79
+ y1: dependency.y + NODE_HEIGHT_REM / 2,
80
+ x2: node.x,
81
+ y2: node.y + NODE_HEIGHT_REM / 2,
82
+ });
83
+ });
84
+ });
85
+ return lines;
86
+ }, [map, positioned]);
87
+
88
+ const bounds = useMemo(() => {
89
+ if (positioned.length === 0) {
90
+ return { minX: 0, minY: 0, width: NODE_WIDTH_REM * 2, height: NODE_HEIGHT_REM * 2 };
91
+ }
92
+
93
+ const minX = Math.min(...positioned.map((node) => node.x)) - SCENE_PADDING_REM;
94
+ const minY = Math.min(...positioned.map((node) => node.y)) - SCENE_PADDING_REM;
95
+ const maxX = Math.max(...positioned.map((node) => node.x + NODE_WIDTH_REM)) + SCENE_PADDING_REM;
96
+ const maxY = Math.max(...positioned.map((node) => node.y + NODE_HEIGHT_REM)) + SCENE_PADDING_REM;
97
+
98
+ return {
99
+ minX,
100
+ minY,
101
+ width: Math.max(NODE_WIDTH_REM * 2, maxX - minX),
102
+ height: Math.max(NODE_HEIGHT_REM * 2, maxY - minY),
103
+ };
104
+ }, [positioned]);
105
+
106
+ const positionedForRender = useMemo(
107
+ () =>
108
+ positioned.map((node) => ({
109
+ ...node,
110
+ renderX: node.x - bounds.minX,
111
+ renderY: node.y - bounds.minY,
112
+ })),
113
+ [bounds.minX, bounds.minY, positioned],
114
+ );
115
+
116
+ const edgesForRender = useMemo(
117
+ () =>
118
+ edges.map((edge) => ({
119
+ ...edge,
120
+ renderX1: edge.x1 - bounds.minX,
121
+ renderY1: edge.y1 - bounds.minY,
122
+ renderX2: edge.x2 - bounds.minX,
123
+ renderY2: edge.y2 - bounds.minY,
124
+ })),
125
+ [bounds.minX, bounds.minY, edges],
126
+ );
127
+
128
+ const dependencyGraph = useMemo(() => {
129
+ const downstream = new Map<string, Set<string>>();
130
+ const upstream = new Map<string, Set<string>>();
131
+
132
+ edges.forEach((edge) => {
133
+ downstream.set(edge.from, (downstream.get(edge.from) ?? new Set<string>()).add(edge.to));
134
+ upstream.set(edge.to, (upstream.get(edge.to) ?? new Set<string>()).add(edge.from));
135
+ });
136
+
137
+ return { downstream, upstream };
138
+ }, [edges]);
139
+
140
+ const focusTaskId = hoveredTaskId ?? selectedTaskId;
141
+
142
+ const relatedTaskIds = useMemo(() => {
143
+ if (!focusTaskId) return null;
144
+
145
+ const related = new Set<string>([focusTaskId]);
146
+ const walk = (seed: string, map: Map<string, Set<string>>) => {
147
+ const queue = [seed];
148
+ while (queue.length > 0) {
149
+ const current = queue.shift();
150
+ if (!current) continue;
151
+ (map.get(current) ?? new Set<string>()).forEach((next) => {
152
+ if (related.has(next)) return;
153
+ related.add(next);
154
+ queue.push(next);
155
+ });
156
+ }
157
+ };
158
+
159
+ walk(focusTaskId, dependencyGraph.downstream);
160
+ walk(focusTaskId, dependencyGraph.upstream);
161
+
162
+ return related;
163
+ }, [dependencyGraph.downstream, dependencyGraph.upstream, focusTaskId]);
164
+
165
+ const fitToGraph = () => {
166
+ const canvas = canvasRef.current;
167
+ if (!canvas) return;
168
+
169
+ const rootFontSize = Number.parseFloat(globalThis.getComputedStyle(document.documentElement).fontSize) || 16;
170
+ const widthPx = bounds.width * rootFontSize;
171
+ const heightPx = bounds.height * rootFontSize;
172
+ const paddingPx = FIT_PADDING_REM * rootFontSize;
173
+ const availableWidth = Math.max(1, canvas.clientWidth - paddingPx * 2);
174
+ const availableHeight = Math.max(1, canvas.clientHeight - paddingPx * 2);
175
+
176
+ const nextScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, Math.min(availableWidth / widthPx, availableHeight / heightPx)));
177
+ const centeredPanX = (canvas.clientWidth - widthPx * nextScale) / (2 * rootFontSize * nextScale);
178
+ const centeredPanY = (canvas.clientHeight - heightPx * nextScale) / (2 * rootFontSize * nextScale);
179
+
180
+ setScale(nextScale);
181
+ setPan({ x: centeredPanX, y: centeredPanY });
182
+ };
183
+
184
+ const persistPosition = (taskId: string, next: Position) => {
185
+ setNodeOverrides((current) => ({ ...current, [taskId]: next }));
186
+ savePositions(context.projectId, { ...persisted, ...nodeOverrides, [taskId]: next });
187
+ };
188
+
189
+ const handlePointerDownOnNode = (taskId: string, event: ReactPointerEvent<HTMLDivElement>) => {
190
+ setSelectedTaskId((current) => (current === taskId ? null : taskId));
191
+ if (event.button !== 0) return;
192
+ event.preventDefault();
193
+ event.stopPropagation();
194
+ const hit = map.get(taskId);
195
+ if (!hit) return;
196
+ interactionRef.current = {
197
+ kind: "node",
198
+ taskId,
199
+ startPointer: { x: event.clientX, y: event.clientY },
200
+ startNode: { x: hit.x, y: hit.y },
201
+ moved: false,
202
+ };
203
+ event.currentTarget.setPointerCapture(event.pointerId);
204
+ };
205
+
206
+ const handleCanvasPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
207
+ if (event.button !== 0) return;
208
+ interactionRef.current = {
209
+ kind: "pan",
210
+ startPointer: { x: event.clientX, y: event.clientY },
211
+ startPan: pan,
212
+ moved: false,
213
+ };
214
+ event.currentTarget.setPointerCapture(event.pointerId);
215
+ };
216
+
217
+ const handlePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
218
+ const current = interactionRef.current;
219
+ if (!current) return;
220
+
221
+ const delta = {
222
+ x: (event.clientX - current.startPointer.x) / 16,
223
+ y: (event.clientY - current.startPointer.y) / 16,
224
+ };
225
+
226
+ if (current.kind === "node") {
227
+ const moved = getDistance({ x: 0, y: 0 }, delta) > DRAG_THRESHOLD_REM;
228
+ if (moved && !current.moved) current.moved = true;
229
+ if (!current.moved) return;
230
+ setNodeOverrides((existing) => ({
231
+ ...existing,
232
+ [current.taskId]: { x: current.startNode.x + delta.x / scale, y: current.startNode.y + delta.y / scale },
233
+ }));
234
+ return;
235
+ }
236
+
237
+ const moved = getDistance({ x: 0, y: 0 }, delta) > DRAG_THRESHOLD_REM;
238
+ if (moved && !current.moved) current.moved = true;
239
+ if (!current.moved) return;
240
+ setPan({ x: current.startPan.x + delta.x, y: current.startPan.y + delta.y });
241
+ };
242
+
243
+ const handlePointerUp = (event: ReactPointerEvent<HTMLDivElement>) => {
244
+ const current = interactionRef.current;
245
+ interactionRef.current = null;
246
+ if (!current) return;
247
+
248
+ if (current.kind === "node") {
249
+ const hit = map.get(current.taskId);
250
+ if (!hit) return;
251
+
252
+ if (!current.moved) {
253
+ context.openTaskDetail(hit.task);
254
+ return;
255
+ }
256
+
257
+ persistPosition(current.taskId, { x: hit.x, y: hit.y });
258
+ }
259
+
260
+ event.currentTarget.releasePointerCapture(event.pointerId);
261
+ };
262
+
263
+ return (
264
+ <section className="dependency-graph-view">
265
+ <div className="dependency-graph-controls">
266
+ <button className="btn btn-sm" onClick={() => setScale((value) => Math.min(value + 0.1, MAX_SCALE))}>Zoom In</button>
267
+ <button className="btn btn-sm" onClick={() => setScale((value) => Math.max(value - 0.1, MIN_SCALE))}>Zoom Out</button>
268
+ <button className="btn btn-sm" onClick={fitToGraph}>Fit</button>
269
+ </div>
270
+
271
+ <div
272
+ className="dependency-graph-canvas"
273
+ ref={canvasRef}
274
+ onPointerDown={handleCanvasPointerDown}
275
+ onPointerMove={handlePointerMove}
276
+ onPointerUp={handlePointerUp}
277
+ >
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
+ />
297
+ ))}
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>
317
+ </div>
318
+ </section>
319
+ );
320
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it, beforeEach } from "vitest";
2
+ import { projectScopedKey, loadPositions, savePositions } from "../storage";
3
+
4
+ const createMemoryStorage = () => {
5
+ const map = new Map<string, string>();
6
+ return {
7
+ getItem: (key: string) => map.get(key) ?? null,
8
+ setItem: (key: string, value: string) => {
9
+ map.set(key, value);
10
+ },
11
+ clear: () => map.clear(),
12
+ };
13
+ };
14
+
15
+ describe("storage", () => {
16
+ const localStorage = createMemoryStorage();
17
+
18
+ beforeEach(() => {
19
+ (globalThis as { window?: { localStorage?: typeof localStorage } }).window = { localStorage };
20
+ localStorage.clear();
21
+ });
22
+
23
+ it("builds project-scoped key", () => {
24
+ expect(projectScopedKey("proj_123")).toBe("kb:proj_123:dependency-graph-positions");
25
+ });
26
+
27
+ it("persists and restores positions", () => {
28
+ savePositions("proj_123", { "FN-1": { x: 10, y: 20 } });
29
+ expect(loadPositions("proj_123")).toEqual({ "FN-1": { x: 10, y: 20 } });
30
+ });
31
+ });
@@ -0,0 +1,25 @@
1
+ import { definePlugin } from "@fusion/plugin-sdk";
2
+
3
+ const plugin = definePlugin({
4
+ manifest: {
5
+ id: "fusion-plugin-dependency-graph",
6
+ name: "Dependency Graph",
7
+ version: "0.1.0",
8
+ description: "Top-level dependency graph dashboard view",
9
+ },
10
+ state: "installed",
11
+ hooks: {},
12
+ dashboardViews: [
13
+ {
14
+ viewId: "graph",
15
+ label: "Graph",
16
+ componentPath: "./src/DependencyGraphView.tsx",
17
+ icon: "Network",
18
+ placement: "more",
19
+ order: 40,
20
+ },
21
+ ],
22
+ });
23
+
24
+ export default plugin;
25
+ export { DependencyGraphView } from "./DependencyGraphView";
@@ -0,0 +1,23 @@
1
+ const BASE_KEY = "dependency-graph-positions";
2
+
3
+ export function projectScopedKey(projectId?: string): string {
4
+ const suffix = projectId ?? "default";
5
+ return `kb:${suffix}:${BASE_KEY}`;
6
+ }
7
+
8
+ export function loadPositions(projectId?: string): Record<string, { x: number; y: number }> {
9
+ if (typeof window === "undefined") return {};
10
+ try {
11
+ const raw = window.localStorage.getItem(projectScopedKey(projectId));
12
+ if (!raw) return {};
13
+ const parsed = JSON.parse(raw) as Record<string, { x: number; y: number }>;
14
+ return parsed ?? {};
15
+ } catch {
16
+ return {};
17
+ }
18
+ }
19
+
20
+ export function savePositions(projectId: string | undefined, positions: Record<string, { x: number; y: number }>): void {
21
+ if (typeof window === "undefined") return;
22
+ window.localStorage.setItem(projectScopedKey(projectId), JSON.stringify(positions));
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runfusion/fusion",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "license": "MIT",
5
5
  "description": "Fusion CLI: HTTP API server, daemon, dashboard launcher, and task tooling for the Fusion AI coding agent.",
6
6
  "homepage": "https://github.com/Runfusion/Fusion#readme",
@@ -35,12 +35,13 @@
35
35
  "dist/client/**",
36
36
  "dist/pi-claude-cli/**",
37
37
  "dist/droid-cli/**",
38
+ "dist/plugins/**",
38
39
  "skill/**",
39
40
  "README.md"
40
41
  ],
41
42
  "dependencies": {
42
- "@mariozechner/pi-ai": "^0.70.0",
43
- "@mariozechner/pi-coding-agent": "^0.70.0",
43
+ "@mariozechner/pi-ai": "^0.72.1",
44
+ "@mariozechner/pi-coding-agent": "^0.72.1",
44
45
  "express": "^5.1.0",
45
46
  "ink": "^6.8.0",
46
47
  "ink-spinner": "^5.0.0",
@@ -90,6 +91,9 @@
90
91
  "build:exe:all": "bun run build.ts --all",
91
92
  "typecheck": "tsc --noEmit",
92
93
  "test": "vitest run --silent=passed-only --reporter=dot",
93
- "test:build-exe": "cross-env FUSION_TEST_BUILD_EXE=1 vitest run --config vitest.build-exe.config.ts --silent=passed-only --reporter=dot"
94
+ "test:slow-cli": "cross-env FUSION_TEST_SLOW_CLI=1 vitest run src/commands/__tests__/agent-export.test.ts --silent=passed-only --reporter=dot",
95
+ "test:extension-integration": "cross-env FUSION_TEST_EXTENSION_INTEGRATION=1 vitest run src/__tests__/extension.test.ts --silent=passed-only --reporter=dot",
96
+ "test:build-exe": "cross-env FUSION_TEST_BUILD_EXE=1 vitest run --config vitest.build-exe.config.ts --silent=passed-only --reporter=dot",
97
+ "test:pre-release": "pnpm test:slow-cli && pnpm test:build-exe"
94
98
  }
95
99
  }
@@ -14,8 +14,8 @@ These tools are **not** part of the pi extension's user-invokable `extension.ts`
14
14
  | `fn_task_log` | executor, heartbeat | Write significant task log entries | `message` (string), `outcome?` (string) |
15
15
  | `fn_task_document_write` | triage, executor, heartbeat | Save/update a named task document revision | `key` (string), `content` (string), `author?` (string) |
16
16
  | `fn_task_document_read` | triage, executor, heartbeat | Read one task document or list all | `key?` (string) |
17
- | `fn_memory_search` | triage, executor, heartbeat | Search project/agent memory snippets | `query` (string), `limit?` (number) |
18
- | `fn_memory_get` | triage, executor, heartbeat | Read a bounded memory file window | `path` (string), `startLine?` (number), `lineCount?` (number) |
17
+ | `fn_memory_search` | triage, executor, heartbeat | Search project memory plus per-agent layered memory snippets | `query` (string), `limit?` (number) |
18
+ | `fn_memory_get` | triage, executor, heartbeat | Read a bounded memory file window (including bounded per-agent layered paths) | `path` (string), `startLine?` (number), `lineCount?` (number) |
19
19
  | `fn_memory_append` | executor, heartbeat (when writable backend enabled) | Append long-term/daily memory notes | `scope?` (`project` \| `agent`), `layer` (`long-term` \| `daily`), `content` (string) |
20
20
  | `fn_research_run` | triage, executor | Start a bounded research run (optionally wait for completion) and return structured findings metadata | `query` (string), `wait_for_completion?` (boolean), `max_wait_ms?` (number) |
21
21
  | `fn_research_list` | triage, executor | List recent research runs with status/summary metadata | `status?` (`pending` \| `running` \| `completed` \| `failed` \| `cancelled`), `limit?` (number) |