@runfusion/fusion 0.24.0 → 0.25.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 (106) hide show
  1. package/dist/bin.js +2254 -1349
  2. package/dist/client/assets/AgentDetailView-ZbHEbYRT.js +18 -0
  3. package/dist/client/assets/{AgentsView-BkB9FiMT.js → AgentsView-B3jYk8Kt.js} +3 -3
  4. package/dist/client/assets/ChatView-DhPkiEGs.js +1 -0
  5. package/dist/client/assets/{DevServerView-BkvtjZBa.js → DevServerView-DyGDEiBP.js} +1 -1
  6. package/dist/client/assets/{DirectoryPicker-BK-KbnhP.js → DirectoryPicker-D5UIeIl6.js} +1 -1
  7. package/dist/client/assets/{DocumentsView-BEg1CQAk.js → DocumentsView-DNHu1T8K.js} +1 -1
  8. package/dist/client/assets/{EvalsView-Berf9bQm.js → EvalsView-CpRobtDi.js} +1 -1
  9. package/dist/client/assets/{ExperimentalAgentOnboardingModal-jcInE50G.js → ExperimentalAgentOnboardingModal-DOY_oZi7.js} +1 -1
  10. package/dist/client/assets/{InsightsView-BX5bSF1J.js → InsightsView-vp0RE8Mg.js} +1 -1
  11. package/dist/client/assets/MemoryView-PSc5lGJt.js +2 -0
  12. package/dist/client/assets/MemoryView-zaXewZzi.css +1 -0
  13. package/dist/client/assets/{NodesView-DLUOBLf6.js → NodesView-DMj6HGeC.js} +1 -1
  14. package/dist/client/assets/{PiExtensionsManager-COlJf0Kx.js → PiExtensionsManager-DL_QcN56.js} +2 -2
  15. package/dist/client/assets/PluginManager-BtYKm8IT.js +1 -0
  16. package/dist/client/assets/{ResearchView-B256Lr8I.js → ResearchView-BhWqfdV0.js} +1 -1
  17. package/dist/client/assets/{SettingsModal-BeA_nQtW.js → SettingsModal-BAgB4_AR.js} +4 -4
  18. package/dist/client/assets/{SettingsModal-yRqM4DV8.js → SettingsModal-CUCyaAyE.js} +1 -1
  19. package/dist/client/assets/{SetupWizardModal-uUZk3TKT.js → SetupWizardModal-BKscasuh.js} +1 -1
  20. package/dist/client/assets/{SkillsView-CP8JX0P_.js → SkillsView-BdELqTy7.js} +1 -1
  21. package/dist/client/assets/{TodoView-DCRIkDZ-.js → TodoView-DFNGBDNV.js} +1 -1
  22. package/dist/client/assets/{folder-open-DHjELt8-.js → folder-open-k1xmUMyr.js} +1 -1
  23. package/dist/client/assets/index-Qq2JOOWx.css +1 -0
  24. package/dist/client/assets/{index-CQyVRLOb.js → index-TFYXEVpn.js} +160 -160
  25. package/dist/client/assets/{star-DYesq1AV.js → star-ne32r3Y4.js} +1 -1
  26. package/dist/client/assets/{upload-DTWF3Db5.js → upload-MS-2Gx53.js} +1 -1
  27. package/dist/client/assets/{users--syrel4l.js → users-C519GSjH.js} +1 -1
  28. package/dist/client/index.html +2 -2
  29. package/dist/client/version.json +1 -1
  30. package/dist/droid-cli/package.json +1 -1
  31. package/dist/extension.js +1370 -629
  32. package/dist/pi-claude-cli/package.json +1 -1
  33. package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +9 -11
  34. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  35. package/dist/plugins/fusion-plugin-dependency-graph/bundled.js +30 -0
  36. package/dist/plugins/fusion-plugin-dependency-graph/package.json +3 -28
  37. package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +899 -895
  38. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  39. package/dist/plugins/fusion-plugin-hermes-runtime/bundled.js +68 -71
  40. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  41. package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +47 -50
  42. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  43. package/dist/plugins/fusion-plugin-paperclip-runtime/bundled.js +155 -109
  44. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  45. package/dist/plugins/fusion-plugin-reports/package.json +1 -1
  46. package/dist/plugins/fusion-plugin-reports/src/index.ts +49 -3
  47. package/dist/plugins/fusion-plugin-reports/src/report-schema.ts +38 -0
  48. package/dist/plugins/fusion-plugin-reports/src/store/__tests__/report-schema.test.ts +66 -0
  49. package/dist/plugins/fusion-plugin-reports/src/store/__tests__/report-store.test.ts +177 -0
  50. package/dist/plugins/fusion-plugin-reports/src/store/report-store.ts +341 -0
  51. package/dist/plugins/fusion-plugin-reports/src/store/report-types.ts +77 -0
  52. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  53. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  54. package/package.json +1 -1
  55. package/dist/client/assets/AgentDetailView-gy_5SUj2.js +0 -18
  56. package/dist/client/assets/ChatView-B_-B8fqu.js +0 -1
  57. package/dist/client/assets/MemoryView-CKElJY_3.js +0 -2
  58. package/dist/client/assets/MemoryView-DiajLXby.css +0 -1
  59. package/dist/client/assets/PluginManager-CfW55BF4.js +0 -1
  60. package/dist/client/assets/createLucideIcon-BazL2hk5.js +0 -21
  61. package/dist/client/assets/dashboard-view-BkTMSZYn.css +0 -1
  62. package/dist/client/assets/dashboard-view-CyWN-d02.js +0 -63
  63. package/dist/client/assets/dashboard-view-DdGlfuu-.css +0 -1
  64. package/dist/client/assets/dashboard-view-lR7YYmSC.js +0 -21
  65. package/dist/client/assets/index-CxA2Nn0_.css +0 -1
  66. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.css +0 -58
  67. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.tsx +0 -301
  68. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphHighlight.css +0 -27
  69. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.css +0 -157
  70. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.tsx +0 -126
  71. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.css +0 -35
  72. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.tsx +0 -36
  73. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.highlighting.test.tsx +0 -112
  74. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.persistence.test.tsx +0 -115
  75. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.test.tsx +0 -128
  76. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.drag.test.tsx +0 -82
  77. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.test.tsx +0 -307
  78. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphToolbar.test.tsx +0 -60
  79. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/edges.test.tsx +0 -75
  80. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filtering.test.tsx +0 -62
  81. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filters.test.ts +0 -78
  82. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/graphPositionStorage.test.ts +0 -95
  83. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/host-integration.test.ts +0 -74
  84. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/index.test.ts +0 -58
  85. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/interactions.test.tsx +0 -121
  86. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/layout.test.ts +0 -70
  87. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/persistence.test.tsx +0 -89
  88. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphData.test.ts +0 -86
  89. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphInteraction.test.ts +0 -167
  90. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphPositions.test.ts +0 -66
  91. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useNodeDrag.test.ts +0 -81
  92. package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-interop.d.ts +0 -35
  93. package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-view.tsx +0 -19
  94. package/dist/plugins/fusion-plugin-dependency-graph/src/edges.tsx +0 -70
  95. package/dist/plugins/fusion-plugin-dependency-graph/src/filters.ts +0 -8
  96. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/__tests__/useDependencyChain.test.ts +0 -53
  97. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useDependencyChain.ts +0 -60
  98. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useGraphPositions.ts +0 -45
  99. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useNodeDrag.ts +0 -114
  100. package/dist/plugins/fusion-plugin-dependency-graph/src/index.ts +0 -24
  101. package/dist/plugins/fusion-plugin-dependency-graph/src/layout.ts +0 -91
  102. package/dist/plugins/fusion-plugin-dependency-graph/src/styles/drag.css +0 -15
  103. package/dist/plugins/fusion-plugin-dependency-graph/src/types.ts +0 -21
  104. package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphData.ts +0 -17
  105. package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphInteraction.ts +0 -292
  106. package/dist/plugins/fusion-plugin-dependency-graph/src/utils/graphPositionStorage.ts +0 -65
@@ -1,66 +0,0 @@
1
- import { act, renderHook } from "@testing-library/react";
2
- import { beforeEach, describe, expect, it, vi } from "vitest";
3
- import { useGraphPositions } from "../hooks/useGraphPositions";
4
- import * as storage from "../utils/graphPositionStorage";
5
-
6
- vi.mock("../utils/graphPositionStorage", () => ({
7
- loadPositions: vi.fn(),
8
- savePositions: vi.fn(),
9
- clearPositions: vi.fn(),
10
- }));
11
-
12
- describe("useGraphPositions", () => {
13
- beforeEach(() => {
14
- vi.clearAllMocks();
15
- });
16
-
17
- it("loads saved positions on mount with project scope", () => {
18
- vi.mocked(storage.loadPositions).mockReturnValue({ a: { x: 1, y: 2 } });
19
-
20
- const { result } = renderHook(() => useGraphPositions({ projectId: "p1", visibleTaskIds: new Set(["a"]) }));
21
-
22
- expect(storage.loadPositions).toHaveBeenCalledWith("p1");
23
- expect(result.current.savedPositions).toEqual({ a: { x: 1, y: 2 } });
24
- });
25
-
26
- it("reloads positions when project id changes", () => {
27
- vi.mocked(storage.loadPositions).mockReturnValueOnce({ a: { x: 1, y: 1 } }).mockReturnValueOnce({ b: { x: 2, y: 2 } });
28
-
29
- const { result, rerender } = renderHook(
30
- ({ projectId }) => useGraphPositions({ projectId, visibleTaskIds: new Set(["a", "b"]) }),
31
- { initialProps: { projectId: "p1" } },
32
- );
33
-
34
- rerender({ projectId: "p2" });
35
-
36
- expect(storage.loadPositions).toHaveBeenNthCalledWith(1, "p1");
37
- expect(storage.loadPositions).toHaveBeenNthCalledWith(2, "p2");
38
- expect(result.current.savedPositions).toEqual({ b: { x: 2, y: 2 } });
39
- });
40
-
41
- it("persistPositions writes scoped and filters non-visible ids", () => {
42
- vi.mocked(storage.loadPositions).mockReturnValue({});
43
-
44
- const { result } = renderHook(() => useGraphPositions({ projectId: "p1", visibleTaskIds: new Set(["a"]) }));
45
-
46
- act(() => {
47
- result.current.persistPositions({ a: { x: 1, y: 2 }, hidden: { x: 9, y: 9 } });
48
- });
49
-
50
- expect(storage.savePositions).toHaveBeenCalledWith({ a: { x: 1, y: 2 }, hidden: { x: 9, y: 9 } }, new Set(["a"]), "p1");
51
- expect(result.current.savedPositions).toEqual({ a: { x: 1, y: 2 } });
52
- });
53
-
54
- it("clearSavedPositions clears storage and resets state", () => {
55
- vi.mocked(storage.loadPositions).mockReturnValue({ a: { x: 1, y: 2 } });
56
-
57
- const { result } = renderHook(() => useGraphPositions({ projectId: "p1", visibleTaskIds: new Set(["a"]) }));
58
-
59
- act(() => {
60
- result.current.clearSavedPositions();
61
- });
62
-
63
- expect(storage.clearPositions).toHaveBeenCalledWith("p1");
64
- expect(result.current.savedPositions).toBeNull();
65
- });
66
- });
@@ -1,81 +0,0 @@
1
- import { act, renderHook } from "@testing-library/react";
2
- import type React from "react";
3
- import { describe, expect, it, vi } from "vitest";
4
- import { __internal, useNodeDrag } from "../hooks/useNodeDrag";
5
-
6
- function pointerEvent(overrides: Partial<PointerEvent> = {}) {
7
- const target = {
8
- setPointerCapture: vi.fn(),
9
- releasePointerCapture: vi.fn(),
10
- hasPointerCapture: vi.fn(() => true),
11
- };
12
- return {
13
- isPrimary: true,
14
- pointerId: 1,
15
- clientX: 0,
16
- clientY: 0,
17
- stopPropagation: vi.fn(),
18
- currentTarget: target,
19
- ...overrides,
20
- } as unknown as React.PointerEvent<HTMLElement>;
21
- }
22
-
23
- describe("useNodeDrag", () => {
24
- it("transitions pending to dragging and back on pointer up", () => {
25
- const onPositionChange = vi.fn();
26
- const onDragStateChange = vi.fn();
27
- const { result } = renderHook(() =>
28
- useNodeDrag({ taskId: "A", position: { x: 10, y: 10 }, scale: 1, onPositionChange, onDragStateChange }),
29
- );
30
-
31
- act(() => result.current.onPointerDown(pointerEvent({ clientX: 10, clientY: 20 })));
32
- act(() => result.current.onPointerMove(pointerEvent({ clientX: 16, clientY: 26 })));
33
- expect(result.current.isDragging).toBe(true);
34
- expect(onPositionChange).toHaveBeenCalledWith("A", { x: 16, y: 16 });
35
-
36
- act(() => result.current.onPointerUp(pointerEvent({ clientX: 16, clientY: 26 })));
37
- expect(result.current.isDragging).toBe(false);
38
- expect(onDragStateChange).toHaveBeenCalledWith(true);
39
- expect(onDragStateChange).toHaveBeenCalledWith(false);
40
- });
41
-
42
- it("stays click-only below threshold", () => {
43
- const onPositionChange = vi.fn();
44
- const { result } = renderHook(() =>
45
- useNodeDrag({ taskId: "A", position: { x: 0, y: 0 }, scale: 1, onPositionChange }),
46
- );
47
-
48
- act(() => result.current.onPointerDown(pointerEvent()));
49
- act(() => result.current.onPointerMove(pointerEvent({ clientX: __internal.DRAG_THRESHOLD_PX - 1, clientY: 0 })));
50
- act(() => result.current.onPointerUp(pointerEvent({ clientX: __internal.DRAG_THRESHOLD_PX - 1, clientY: 0 })));
51
-
52
- expect(result.current.isDragging).toBe(false);
53
- expect(onPositionChange).not.toHaveBeenCalled();
54
- });
55
-
56
- it("divides pointer delta by zoom scale", () => {
57
- const onPositionChange = vi.fn();
58
- const { result } = renderHook(() =>
59
- useNodeDrag({ taskId: "A", position: { x: 0, y: 0 }, scale: 2, onPositionChange }),
60
- );
61
-
62
- act(() => result.current.onPointerDown(pointerEvent()));
63
- act(() => result.current.onPointerMove(pointerEvent({ clientX: 10, clientY: 6 })));
64
-
65
- expect(onPositionChange).toHaveBeenCalledWith("A", { x: 5, y: 3 });
66
- });
67
-
68
- it("cancels drag cleanly on pointer cancel", () => {
69
- const onPositionChange = vi.fn();
70
- const { result } = renderHook(() =>
71
- useNodeDrag({ taskId: "A", position: { x: 0, y: 0 }, scale: 1, onPositionChange }),
72
- );
73
-
74
- act(() => result.current.onPointerDown(pointerEvent()));
75
- act(() => result.current.onPointerMove(pointerEvent({ clientX: 8, clientY: 0 })));
76
- expect(result.current.isDragging).toBe(true);
77
-
78
- act(() => result.current.onPointerCancel(pointerEvent({ clientX: 8, clientY: 0 })));
79
- expect(result.current.isDragging).toBe(false);
80
- });
81
- });
@@ -1,35 +0,0 @@
1
- declare module "@fusion/dashboard/app/utils/taskStuck" {
2
- import type { Task } from "@fusion/core";
3
-
4
- export function isTaskStuck(task: Task, taskStuckTimeoutMs?: number, lastFetchTimeMs?: number): boolean;
5
- }
6
-
7
- declare module "@fusion/dashboard/app/components/TaskCard" {
8
- import type { Column, Task, TaskDetail } from "@fusion/core";
9
- import type { ReactElement } from "react";
10
-
11
- interface TaskCardProps {
12
- task: Task;
13
- projectId?: string;
14
- onOpenDetail: (task: Task | TaskDetail) => void;
15
- addToast: (message: string, type?: "success" | "error" | "info" | "warning") => void;
16
- globalPaused?: boolean;
17
- onUpdateTask?: (
18
- id: string,
19
- updates: { title?: string; description?: string; dependencies?: string[] }
20
- ) => Promise<Task>;
21
- onArchiveTask?: (id: string) => Promise<Task>;
22
- onUnarchiveTask?: (id: string) => Promise<Task>;
23
- onDeleteTask?: (id: string, options?: { removeDependencyReferences?: boolean }) => Promise<Task>;
24
- onRetryTask?: (id: string) => Promise<Task>;
25
- onOpenDetailWithTab?: (task: Task | TaskDetail, initialTab: "changes") => void;
26
- taskStuckTimeoutMs?: number;
27
- onOpenMission?: (missionId: string) => void;
28
- onMoveTask?: (id: string, column: Column, optionsOrPosition?: { preserveProgress?: boolean } | number) => Promise<Task>;
29
- lastFetchTimeMs?: number;
30
- workflowStepNameLookup?: ReadonlyMap<string, string>;
31
- disableDrag?: boolean;
32
- }
33
-
34
- export function TaskCard(props: TaskCardProps): ReactElement;
35
- }
@@ -1,19 +0,0 @@
1
- import type { Task, TaskDetail, WorkflowStep } from "@fusion/core";
2
- import type { PluginDashboardViewContext } from "@fusion/dashboard/app/plugins/types";
3
- import { createElement } from "react";
4
- import { DependencyGraph } from "./DependencyGraph";
5
-
6
- function createWorkflowStepNameLookup(workflowSteps: WorkflowStep[] | undefined): ReadonlyMap<string, string> {
7
- return new Map((workflowSteps ?? []).map((step) => [step.id, step.name] as const));
8
- }
9
-
10
- export function DependencyGraphDashboardView({ context }: { context?: PluginDashboardViewContext }) {
11
- return createElement(DependencyGraph, {
12
- tasks: context?.tasks ?? [],
13
- projectId: context?.projectId,
14
- workflowStepNameLookup: createWorkflowStepNameLookup(context?.workflowSteps),
15
- onOpenDetail: context?.openTaskDetail as ((task: Task | TaskDetail) => void) | undefined,
16
- });
17
- }
18
-
19
- export { DependencyGraph };
@@ -1,70 +0,0 @@
1
- import type { GraphEdge, GraphPosition } from "./types";
2
- import "./GraphHighlight.css";
3
-
4
- interface GraphEdgesProps {
5
- edges: GraphEdge[];
6
- positions: Map<string, GraphPosition>;
7
- nodeWidth?: number;
8
- nodeHeight?: number;
9
- highlightedEdgeIds?: Set<string>;
10
- }
11
-
12
- const DEFAULT_NODE_WIDTH = 280;
13
- const DEFAULT_NODE_HEIGHT = 100;
14
-
15
- export function GraphEdges({
16
- edges,
17
- positions,
18
- nodeWidth = DEFAULT_NODE_WIDTH,
19
- nodeHeight = DEFAULT_NODE_HEIGHT,
20
- highlightedEdgeIds,
21
- }: GraphEdgesProps) {
22
- const hasHighlights = Boolean(highlightedEdgeIds && highlightedEdgeIds.size > 0);
23
-
24
- return (
25
- <svg className="dependency-graph-edges" aria-hidden="true">
26
- <defs>
27
- <marker
28
- id="dependency-graph-arrowhead"
29
- markerWidth="10"
30
- markerHeight="7"
31
- refX="10"
32
- refY="3.5"
33
- orient="auto"
34
- markerUnits="strokeWidth"
35
- >
36
- <path d="M 0 0 L 10 3.5 L 0 7 z" fill="var(--border)" />
37
- </marker>
38
- </defs>
39
- {edges.map((edge) => {
40
- const source = positions.get(edge.source);
41
- const target = positions.get(edge.target);
42
- if (!source || !target) return null;
43
-
44
- const edgeId = `${edge.source}->${edge.target}`;
45
- const isActiveHighlight = hasHighlights && (highlightedEdgeIds?.has(edgeId) ?? false);
46
- const x1 = source.x + nodeWidth / 2;
47
- const y1 = source.y + nodeHeight;
48
- const x2 = target.x + nodeWidth / 2;
49
- const y2 = target.y;
50
- const controlY = y1 + (y2 - y1) / 2;
51
-
52
- return (
53
- <path
54
- key={edgeId}
55
- data-testid="dependency-edge"
56
- data-edge-id={edgeId}
57
- className={`dependency-graph-edge${isActiveHighlight ? " graph-edge--highlighted" : ""}${hasHighlights && !isActiveHighlight ? " graph-edge--dimmed" : ""}`}
58
- d={`M ${x1} ${y1} C ${x1} ${controlY}, ${x2} ${controlY}, ${x2} ${y2}`}
59
- fill="none"
60
- stroke={isActiveHighlight ? "var(--todo)" : "var(--border)"}
61
- strokeWidth={isActiveHighlight ? "var(--space-xs)" : "var(--btn-border-width)"}
62
- opacity={hasHighlights && !isActiveHighlight ? 0.15 : 1}
63
- markerEnd="url(#dependency-graph-arrowhead)"
64
- style={{ transition: "opacity var(--transition-fast), stroke var(--transition-fast), stroke-width var(--transition-fast)" }}
65
- />
66
- );
67
- })}
68
- </svg>
69
- );
70
- }
@@ -1,8 +0,0 @@
1
- import type { Column, Task } from "@fusion/core";
2
-
3
- export const INCLUDED_COLUMNS: ReadonlySet<Column> = new Set(["triage", "todo", "in-progress", "in-review"]);
4
- export const EXCLUDED_COLUMNS: ReadonlySet<Column> = new Set(["done", "archived"]);
5
-
6
- export function filterGraphTasks(tasks: Task[]): Task[] {
7
- return tasks.filter((task) => INCLUDED_COLUMNS.has(task.column));
8
- }
@@ -1,53 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { renderHook } from "@testing-library/react";
3
- import type { Task } from "@fusion/core";
4
- import { useDependencyChain } from "../useDependencyChain";
5
-
6
- function createTask(id: string, dependencies: string[] = []): Task {
7
- return {
8
- id,
9
- description: id,
10
- column: "todo",
11
- dependencies,
12
- steps: [],
13
- currentStep: 0,
14
- log: [],
15
- createdAt: new Date().toISOString(),
16
- updatedAt: new Date().toISOString(),
17
- } as Task;
18
- }
19
-
20
- describe("useDependencyChain", () => {
21
- it("returns empty set for unknown task in empty list", () => {
22
- const { result } = renderHook(() => useDependencyChain([]));
23
- expect(result.current.getChain("A").size).toBe(0);
24
- });
25
-
26
- it("returns single task when no dependencies", () => {
27
- const { result } = renderHook(() => useDependencyChain([createTask("A")]));
28
- expect(result.current.getChain("A")).toEqual(new Set(["A"]));
29
- });
30
-
31
- it("returns full linear chain", () => {
32
- const tasks = [createTask("A"), createTask("B", ["A"]), createTask("C", ["B"]), createTask("D")];
33
- const { result } = renderHook(() => useDependencyChain(tasks));
34
- expect(result.current.getChain("C")).toEqual(new Set(["A", "B", "C"]));
35
- });
36
-
37
- it("returns full diamond chain", () => {
38
- const tasks = [createTask("A"), createTask("B", ["A"]), createTask("C", ["A"]), createTask("D", ["B", "C"]), createTask("E")];
39
- const { result } = renderHook(() => useDependencyChain(tasks));
40
- expect(result.current.getChain("D")).toEqual(new Set(["A", "B", "C", "D"]));
41
- });
42
-
43
- it("does not include disconnected tasks", () => {
44
- const { result } = renderHook(() => useDependencyChain([createTask("A"), createTask("B")]));
45
- expect(result.current.getChain("A")).toEqual(new Set(["A"]));
46
- });
47
-
48
- it("handles circular dependencies safely", () => {
49
- const tasks = [createTask("A", ["B"]), createTask("B", ["A"]), createTask("C")];
50
- const { result } = renderHook(() => useDependencyChain(tasks));
51
- expect(result.current.getChain("A")).toEqual(new Set(["A", "B"]));
52
- });
53
- });
@@ -1,60 +0,0 @@
1
- import { useCallback, useMemo } from "react";
2
- import type { Task } from "@fusion/core";
3
-
4
- export function useDependencyChain(tasks: Task[]) {
5
- const { upstreamMap, downstreamMap } = useMemo(() => {
6
- const upstream = new Map<string, Set<string>>();
7
- const downstream = new Map<string, Set<string>>();
8
-
9
- for (const task of tasks) {
10
- upstream.set(task.id, new Set(task.dependencies ?? []));
11
- if (!downstream.has(task.id)) downstream.set(task.id, new Set());
12
- }
13
-
14
- for (const task of tasks) {
15
- for (const dependencyId of task.dependencies ?? []) {
16
- if (!downstream.has(dependencyId)) downstream.set(dependencyId, new Set());
17
- downstream.get(dependencyId)?.add(task.id);
18
- }
19
- }
20
-
21
- return { upstreamMap: upstream, downstreamMap: downstream };
22
- }, [tasks]);
23
-
24
- const getChain = useCallback(
25
- (taskId: string): Set<string> => {
26
- if (!upstreamMap.has(taskId) && !downstreamMap.has(taskId)) {
27
- return new Set();
28
- }
29
-
30
- const chain = new Set<string>([taskId]);
31
-
32
- const visit = (origin: string, adjacency: Map<string, Set<string>>) => {
33
- const queue = [origin];
34
- const visited = new Set<string>([origin]);
35
-
36
- while (queue.length > 0) {
37
- const current = queue.shift();
38
- if (!current) continue;
39
- const neighbors = adjacency.get(current);
40
- if (!neighbors) continue;
41
-
42
- for (const neighbor of neighbors) {
43
- if (visited.has(neighbor)) continue;
44
- visited.add(neighbor);
45
- chain.add(neighbor);
46
- queue.push(neighbor);
47
- }
48
- }
49
- };
50
-
51
- visit(taskId, upstreamMap);
52
- visit(taskId, downstreamMap);
53
-
54
- return chain;
55
- },
56
- [downstreamMap, upstreamMap],
57
- );
58
-
59
- return { getChain };
60
- }
@@ -1,45 +0,0 @@
1
- import { useCallback, useEffect, useState } from "react";
2
- import { clearPositions, loadPositions, savePositions, type NodePositions } from "../utils/graphPositionStorage";
3
-
4
- function filterVisiblePositions(positions: NodePositions, visibleTaskIds: Set<string>): NodePositions {
5
- const filtered: NodePositions = {};
6
- for (const [taskId, position] of Object.entries(positions)) {
7
- if (visibleTaskIds.has(taskId)) {
8
- filtered[taskId] = position;
9
- }
10
- }
11
- return filtered;
12
- }
13
-
14
- export function useGraphPositions({
15
- projectId,
16
- visibleTaskIds,
17
- }: {
18
- projectId: string | undefined;
19
- visibleTaskIds: Set<string>;
20
- }): {
21
- savedPositions: NodePositions | null;
22
- persistPositions: (positions: NodePositions) => void;
23
- clearSavedPositions: () => void;
24
- } {
25
- const [savedPositions, setSavedPositions] = useState<NodePositions | null>(null);
26
-
27
- useEffect(() => {
28
- setSavedPositions(loadPositions(projectId));
29
- }, [projectId]);
30
-
31
- const persistPositions = useCallback(
32
- (positions: NodePositions) => {
33
- savePositions(positions, visibleTaskIds, projectId);
34
- setSavedPositions(filterVisiblePositions(positions, visibleTaskIds));
35
- },
36
- [projectId, visibleTaskIds],
37
- );
38
-
39
- const clearSavedPositions = useCallback(() => {
40
- clearPositions(projectId);
41
- setSavedPositions(null);
42
- }, [projectId]);
43
-
44
- return { savedPositions, persistPositions, clearSavedPositions };
45
- }
@@ -1,114 +0,0 @@
1
- import { useCallback, useMemo, useRef, useState } from "react";
2
- import type { MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent } from "react";
3
- import type { GraphPosition } from "../types";
4
-
5
- const DRAG_THRESHOLD_PX = 4;
6
-
7
- interface UseNodeDragOptions {
8
- taskId: string;
9
- position: GraphPosition;
10
- scale: number;
11
- onPositionChange: (taskId: string, position: GraphPosition) => void;
12
- onDragStateChange?: (isDragging: boolean) => void;
13
- onDragEnd?: () => void;
14
- }
15
-
16
- interface PendingState {
17
- pointerId: number;
18
- startPointer: { x: number; y: number };
19
- startPosition: GraphPosition;
20
- }
21
-
22
- export function useNodeDrag({ taskId, position, scale, onPositionChange, onDragStateChange, onDragEnd }: UseNodeDragOptions) {
23
- const [isDragging, setIsDragging] = useState(false);
24
- const pendingRef = useRef<PendingState | null>(null);
25
- const positionRef = useRef(position);
26
- const suppressClickRef = useRef(false);
27
-
28
- positionRef.current = position;
29
-
30
- const endDrag = useCallback((dragging: boolean) => {
31
- pendingRef.current = null;
32
- setIsDragging(false);
33
- if (dragging) {
34
- onDragStateChange?.(false);
35
- onDragEnd?.();
36
- suppressClickRef.current = true;
37
- }
38
- }, [onDragEnd, onDragStateChange]);
39
-
40
- const onPointerDown = useCallback((event: ReactPointerEvent<HTMLElement>) => {
41
- if (!event.isPrimary) return;
42
- event.stopPropagation();
43
- const currentTarget = event.currentTarget;
44
- if (typeof currentTarget.setPointerCapture === "function") {
45
- currentTarget.setPointerCapture(event.pointerId);
46
- }
47
- pendingRef.current = {
48
- pointerId: event.pointerId,
49
- startPointer: { x: event.clientX, y: event.clientY },
50
- startPosition: positionRef.current,
51
- };
52
- }, []);
53
-
54
- const onPointerMove = useCallback((event: ReactPointerEvent<HTMLElement>) => {
55
- const pending = pendingRef.current;
56
- if (!pending || pending.pointerId !== event.pointerId) return;
57
- event.stopPropagation();
58
-
59
- const deltaX = event.clientX - pending.startPointer.x;
60
- const deltaY = event.clientY - pending.startPointer.y;
61
- const distance = Math.hypot(deltaX, deltaY);
62
-
63
- if (!isDragging && distance >= DRAG_THRESHOLD_PX) {
64
- setIsDragging(true);
65
- onDragStateChange?.(true);
66
- }
67
-
68
- if (distance < DRAG_THRESHOLD_PX) return;
69
-
70
- const safeScale = scale > 0 ? scale : 1;
71
- onPositionChange(taskId, {
72
- x: pending.startPosition.x + deltaX / safeScale,
73
- y: pending.startPosition.y + deltaY / safeScale,
74
- });
75
- }, [isDragging, onDragStateChange, onPositionChange, scale, taskId]);
76
-
77
- const onPointerUp = useCallback((event: ReactPointerEvent<HTMLElement>) => {
78
- const pending = pendingRef.current;
79
- if (!pending || pending.pointerId !== event.pointerId) return;
80
- event.stopPropagation();
81
- if (typeof event.currentTarget.hasPointerCapture === "function" && event.currentTarget.hasPointerCapture(event.pointerId) && typeof event.currentTarget.releasePointerCapture === "function") {
82
- event.currentTarget.releasePointerCapture(event.pointerId);
83
- }
84
- endDrag(isDragging);
85
- }, [endDrag, isDragging]);
86
-
87
- const onPointerCancel = useCallback((event: ReactPointerEvent<HTMLElement>) => {
88
- const pending = pendingRef.current;
89
- if (!pending || pending.pointerId !== event.pointerId) return;
90
- event.stopPropagation();
91
- if (typeof event.currentTarget.hasPointerCapture === "function" && event.currentTarget.hasPointerCapture(event.pointerId) && typeof event.currentTarget.releasePointerCapture === "function") {
92
- event.currentTarget.releasePointerCapture(event.pointerId);
93
- }
94
- endDrag(isDragging);
95
- }, [endDrag, isDragging]);
96
-
97
- const onClickCapture = useCallback((event: ReactMouseEvent<HTMLElement>) => {
98
- if (!suppressClickRef.current) return;
99
- suppressClickRef.current = false;
100
- event.preventDefault();
101
- event.stopPropagation();
102
- }, []);
103
-
104
- return useMemo(() => ({
105
- isDragging,
106
- onPointerDown,
107
- onPointerMove,
108
- onPointerUp,
109
- onPointerCancel,
110
- onClickCapture,
111
- }), [isDragging, onClickCapture, onPointerCancel, onPointerDown, onPointerMove, onPointerUp]);
112
- }
113
-
114
- export const __internal = { DRAG_THRESHOLD_PX };
@@ -1,24 +0,0 @@
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: "./dashboard-view",
17
- icon: "Network",
18
- placement: "more",
19
- order: 40,
20
- },
21
- ],
22
- });
23
-
24
- export default plugin;
@@ -1,91 +0,0 @@
1
- import type { GraphData, GraphPosition } from "./types";
2
-
3
- export interface LayoutOptions {
4
- nodeWidth?: number;
5
- nodeHeight?: number;
6
- horizontalGap?: number;
7
- verticalGap?: number;
8
- }
9
-
10
- const DEFAULT_LAYOUT_OPTIONS: Required<LayoutOptions> = {
11
- nodeWidth: 280,
12
- nodeHeight: 100,
13
- horizontalGap: 40,
14
- verticalGap: 80,
15
- };
16
-
17
- export function computeAutoLayout(
18
- graphData: GraphData,
19
- options?: LayoutOptions,
20
- ): Map<string, GraphPosition> {
21
- const settings = { ...DEFAULT_LAYOUT_OPTIONS, ...options };
22
- const nodeIds = graphData.nodes.map((node) => node.task.id);
23
- if (nodeIds.length === 0) return new Map();
24
-
25
- const dependentsByDependency = new Map<string, string[]>();
26
- const inDegree = new Map<string, number>();
27
-
28
- for (const id of nodeIds) {
29
- inDegree.set(id, 0);
30
- dependentsByDependency.set(id, []);
31
- }
32
-
33
- for (const edge of graphData.edges) {
34
- if (!inDegree.has(edge.source) || !inDegree.has(edge.target)) continue;
35
- dependentsByDependency.get(edge.target)?.push(edge.source);
36
- inDegree.set(edge.source, (inDegree.get(edge.source) ?? 0) + 1);
37
- }
38
-
39
- const queue = nodeIds.filter((id) => (inDegree.get(id) ?? 0) === 0);
40
- const topologicalOrder: string[] = [];
41
-
42
- while (queue.length > 0) {
43
- const current = queue.shift()!;
44
- topologicalOrder.push(current);
45
- for (const dependent of dependentsByDependency.get(current) ?? []) {
46
- const nextInDegree = (inDegree.get(dependent) ?? 0) - 1;
47
- inDegree.set(dependent, nextInDegree);
48
- if (nextInDegree === 0) queue.push(dependent);
49
- }
50
- }
51
-
52
- for (const id of nodeIds) {
53
- if (!topologicalOrder.includes(id)) topologicalOrder.push(id);
54
- }
55
-
56
- const depthByNode = new Map<string, number>();
57
- for (const id of topologicalOrder) {
58
- const parents = graphData.edges.filter((edge) => edge.source === id).map((edge) => edge.target);
59
- let depth = 0;
60
- for (const parent of parents) {
61
- depth = Math.max(depth, (depthByNode.get(parent) ?? 0) + 1);
62
- }
63
- depthByNode.set(id, depth);
64
- }
65
-
66
- const layers = new Map<number, string[]>();
67
- for (const id of nodeIds) {
68
- const depth = depthByNode.get(id) ?? 0;
69
- const layer = layers.get(depth) ?? [];
70
- layer.push(id);
71
- layers.set(depth, layer);
72
- }
73
-
74
- const positions = new Map<string, GraphPosition>();
75
- const sortedDepths = Array.from(layers.keys()).sort((a, b) => a - b);
76
-
77
- for (const depth of sortedDepths) {
78
- const layer = layers.get(depth) ?? [];
79
- layer.sort();
80
- const layerWidth = layer.length * settings.nodeWidth + Math.max(0, layer.length - 1) * settings.horizontalGap;
81
- const startX = -layerWidth / 2;
82
-
83
- layer.forEach((id, index) => {
84
- const x = startX + index * (settings.nodeWidth + settings.horizontalGap);
85
- const y = depth * (settings.nodeHeight + settings.verticalGap);
86
- positions.set(id, { x, y });
87
- });
88
- }
89
-
90
- return positions;
91
- }