Skip to content

Commit e3962aa

Browse files
committed
Remove canvas node snapping features
1 parent 66503ca commit e3962aa

6 files changed

Lines changed: 4 additions & 781 deletions

File tree

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ The details that make Supervisor feel native, fast, and right.
8181

8282
- **Desktop notifications** — Get notified when agents complete tasks or need input.
8383
- **Project colors** — Customize each project for instant recognition.
84-
- **Agent snapping** — Snap to grid for organized layouts.
8584
- **Collapse & expand** — Collapse agents and projects to compact pills.
8685
- **System tray** — Minimize to tray with a live count of running agents.
8786
- **~20MB on disk** — Tauri + Rust. No bundled Chromium. Lean and resource-efficient.

src/components/canvas/AgentNode.tsx

Lines changed: 2 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
import { memo, useCallback, useState, useRef, useEffect } from "react";
22
import { createPortal } from "react-dom";
3-
import { type Node as FlowNode, type NodeProps, NodeResizer, useReactFlow } from "@xyflow/react";
3+
import { type Node as FlowNode, type NodeProps, NodeResizer } from "@xyflow/react";
44
import { Trash2, RotateCcw, Play } from "lucide-react";
55
import { useAgentStore } from "@/stores/agent-store";
66
import { useProjectStore } from "@/stores/project-store";
7-
import {
8-
computeResizeSnap,
9-
getCandidates,
10-
nodeRect,
11-
getParentOffset,
12-
emitResizeGuides,
13-
triggerHaptic,
14-
type SnapGuide,
15-
} from "@/hooks/use-snap-guides";
167
import type { Agent, AgentTier } from "@/types";
178
import { tintedBg, tintedBorder } from "@/lib/project-colors";
189
import { AgentNodeDeleteDialog } from "./AgentNodeDeleteDialog";
@@ -164,72 +155,11 @@ export const AgentNode = memo(({ data }: NodeProps<AgentNodeNode>) => {
164155
}, 300);
165156
}, []);
166157

167-
const { getNodes, setNodes } = useReactFlow();
168-
const wasResizeSnapped = useRef(false);
169-
const resizeOriginalRect = useRef<{ x: number; y: number; w: number; h: number } | null>(null);
170-
171158
const handleResizeStart = useCallback(() => {
172159
setActive(true);
173-
// Capture the node's rect before resize begins so snap can skip already-aligned edges
174-
const nodeId = `agent-${agent.id}`;
175-
const thisNode = getNodes().find((n) => n.id === nodeId);
176-
if (thisNode) {
177-
resizeOriginalRect.current = nodeRect(thisNode);
178-
}
179-
}, [agent.id, getNodes]);
180-
181-
const handleResize = useCallback(
182-
(_event: unknown, params: { x: number; y: number; width: number; height: number; direction: number[] }) => {
183-
const nodeId = `agent-${agent.id}`;
184-
const allNodes = getNodes();
185-
const thisNode = allNodes.find((n) => n.id === nodeId);
186-
if (!thisNode) return;
187-
188-
const candidates = getCandidates(thisNode, allNodes);
189-
const candidateRects = candidates.map(nodeRect);
190-
const resizeRect = { x: params.x, y: params.y, w: params.width, h: params.height };
191-
const { rect, guides } = computeResizeSnap(resizeRect, candidateRects, params.direction, resizeOriginalRect.current ?? undefined);
192-
193-
// Offset guides to absolute canvas coords for rendering
194-
const parentOff = getParentOffset(thisNode, allNodes);
195-
const absoluteGuides: SnapGuide[] = guides.map((g) => {
196-
if (g.axis === "x") {
197-
return { ...g, value: g.value + parentOff.x, start: g.start + parentOff.y, end: g.end + parentOff.y };
198-
}
199-
return { ...g, value: g.value + parentOff.y, start: g.start + parentOff.x, end: g.end + parentOff.x };
200-
});
201-
emitResizeGuides(absoluteGuides);
202-
203-
const isSnapped = guides.length > 0;
204-
if (isSnapped && !wasResizeSnapped.current) {
205-
triggerHaptic();
206-
}
207-
wasResizeSnapped.current = isSnapped;
208-
209-
if (guides.length > 0) {
210-
setNodes((nds) =>
211-
nds.map((n) =>
212-
n.id === nodeId
213-
? {
214-
...n,
215-
position: { x: rect.x, y: rect.y },
216-
style: { ...n.style, width: rect.w, height: rect.h },
217-
}
218-
: n,
219-
),
220-
);
221-
}
222-
},
223-
[getNodes, setNodes, agent.id],
224-
);
225-
226-
const handleResizeEnd = useCallback(() => {
227-
emitResizeGuides([]);
228-
wasResizeSnapped.current = false;
229-
resizeOriginalRect.current = null;
230160
}, []);
231161

232-
// Collapse: store current size in store, Canvas effect handles dimension snap
162+
// Collapse: store current size in store so Canvas can restore it on expand
233163
const handleCollapse = useCallback(() => {
234164
const el = nodeRef.current?.closest('.react-flow__node') as HTMLElement | null;
235165
if (el) {
@@ -263,8 +193,6 @@ export const AgentNode = memo(({ data }: NodeProps<AgentNodeNode>) => {
263193
minHeight={EXPANDED_MIN_HEIGHT}
264194
isVisible
265195
onResizeStart={handleResizeStart}
266-
onResize={handleResize}
267-
onResizeEnd={handleResizeEnd}
268196
lineStyle={{ borderColor: "transparent", borderWidth: 20 }}
269197
handleStyle={{
270198
backgroundColor: "transparent",

src/components/canvas/Canvas.tsx

Lines changed: 1 addition & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,8 @@ import { AgentNodeDeleteDialog } from "./AgentNodeDeleteDialog";
2121
import type { DeletePreviewProject, DeletePreviewAgent } from "./AgentNodeDeleteDialog";
2222
import { CanvasControls } from "./CanvasControls";
2323
import { CanvasContextMenu, CanvasContextMenuItem, CanvasContextMenuDivider } from "./CanvasContextMenu";
24-
import { SnapGuides } from "./SnapGuides";
2524
import { ProjectZone } from "./ProjectZone";
2625
import { EmptyState } from "./EmptyState";
27-
import { useSnapGuides, setResizeGuidesListener, type SnapGuide } from "../../hooks/use-snap-guides";
2826
import { useCanvasKeyboard } from "../../hooks/use-canvas-keyboard";
2927
import { useCanvasContextMenu } from "../../hooks/use-canvas-context-menu";
3028
import { useCanvasPositions } from "../../hooks/use-canvas-positions";
@@ -155,17 +153,6 @@ function CanvasInner({ onCreateAgent, onCreateProject, onAgentClick }: CanvasInn
155153

156154
const hasActiveFilter = canvasFilter.query !== "" || canvasFilter.status !== null;
157155

158-
// Snap guides
159-
const { calculateSnap, clearGuides } = useSnapGuides();
160-
const [activeGuides, setActiveGuides] = useState<SnapGuide[]>([]);
161-
const [resizeGuides, setResizeGuides] = useState<SnapGuide[]>([]);
162-
163-
// Listen for resize snap guides emitted from node components
164-
useEffect(() => {
165-
setResizeGuidesListener(setResizeGuides);
166-
return () => setResizeGuidesListener(null);
167-
}, []);
168-
169156
// Delete confirmation state for React Flow node removals
170157
const [pendingDeletePreview, setPendingDeletePreview] = useState<DeletePreview | null>(null);
171158

@@ -596,44 +583,9 @@ function CanvasInner({ onCreateAgent, onCreateProject, onAgentClick }: CanvasInn
596583
return () => clearTimeout(fitTimer.current);
597584
}, [positionsLoaded, hasActiveFilter, canvasFilter.query, canvasFilter.status, canvasFilter.scope, getNodes, fitBounds, projectMap]);
598585

599-
// ── Snap guides during drag ──
600-
const handleNodeDrag = useCallback(
601-
(_event: React.MouseEvent, draggedNode: Node) => {
602-
const allNodes = getNodes();
603-
const result = calculateSnap(draggedNode, allNodes);
604-
setActiveGuides(result.guides);
605-
606-
let finalX = result.x;
607-
let finalY = result.y;
608-
609-
// Clamp agents inside project zones so they can't overlap the toolbar
610-
if (draggedNode.parentId && draggedNode.type === "agent") {
611-
finalY = Math.max(finalY, PROJECT_TOOLBAR_HEIGHT);
612-
}
613-
614-
// Apply snapped position
615-
const dx = finalX - draggedNode.position.x;
616-
const dy = finalY - draggedNode.position.y;
617-
if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
618-
setNodes((nds) =>
619-
nds.map((n) =>
620-
n.id === draggedNode.id
621-
? { ...n, position: { x: finalX, y: finalY } }
622-
: n,
623-
),
624-
);
625-
}
626-
},
627-
[getNodes, calculateSnap, setNodes],
628-
);
629-
630586
// ── Prevent free agents from overlapping project zones ──
631587
const handleNodeDragStop = useCallback(
632588
(_event: React.MouseEvent, draggedNode: Node) => {
633-
// Clear snap guides
634-
clearGuides();
635-
setActiveGuides([]);
636-
637589
if (draggedNode.type !== "agent" || draggedNode.parentId) return;
638590

639591
const allNodes = getNodes();
@@ -682,7 +634,7 @@ function CanvasInner({ onCreateAgent, onCreateProject, onAgentClick }: CanvasInn
682634
);
683635
}
684636
},
685-
[getNodes, setNodes, clearGuides],
637+
[getNodes, setNodes],
686638
);
687639

688640
// ── Handle confirmed deletion from the dialog ──
@@ -723,7 +675,6 @@ function CanvasInner({ onCreateAgent, onCreateProject, onAgentClick }: CanvasInn
723675
nodes={nodes}
724676
edges={edges}
725677
onNodesChange={handleNodesChange}
726-
onNodeDrag={handleNodeDrag}
727678
onNodeDragStop={handleNodeDragStop}
728679
nodeTypes={nodeTypes}
729680
proOptions={{ hideAttribution: true }}
@@ -747,7 +698,6 @@ function CanvasInner({ onCreateAgent, onCreateProject, onAgentClick }: CanvasInn
747698
hasActiveFilter ? "!fill-muted-foreground/2" : "!fill-muted-foreground/8",
748699
)}
749700
/>
750-
<SnapGuides guides={[...activeGuides, ...resizeGuides]} />
751701
<CanvasControls />
752702
</ReactFlow>
753703

src/components/canvas/ProjectZone.tsx

Lines changed: 1 addition & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,6 @@ import {
77
Plus, Trash2, Settings, ChevronDown, ChevronRight,
88
} from "lucide-react";
99
import { NodeResizer, useReactFlow, useStore, type Node as FlowNode, type NodeProps } from "@xyflow/react";
10-
import {
11-
computeResizeSnap,
12-
getCandidates,
13-
nodeRect,
14-
emitResizeGuides,
15-
triggerHaptic,
16-
} from "@/hooks/use-snap-guides";
1710
import { Button } from "@/components/ui/button";
1811
import {
1912
AlertDialog,
@@ -53,7 +46,7 @@ export const ProjectZone = memo(({ id, data }: NodeProps<ProjectZoneNode>) => {
5346
const { project, agentCount } = data;
5447
const deleteProject = useProjectStore((s) => s.deleteProject);
5548
const updateProject = useProjectStore((s) => s.updateProject);
56-
const { setNodes, getNodes } = useReactFlow();
49+
const { setNodes } = useReactFlow();
5750
const [showCreate, setShowCreate] = useState(false);
5851
const [showSettings, setShowSettings] = useState(false);
5952
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
@@ -80,60 +73,6 @@ export const ProjectZone = memo(({ id, data }: NodeProps<ProjectZoneNode>) => {
8073
(s) => s.projects.find((p) => p.id === project.id)?.color || project.color || "gray",
8174
);
8275

83-
// ── Resize snapping ──
84-
const wasResizeSnapped = useRef(false);
85-
const resizeOriginalRect = useRef<{ x: number; y: number; w: number; h: number } | null>(null);
86-
87-
const handleResizeStart = useCallback(() => {
88-
const thisNode = getNodes().find((n) => n.id === id);
89-
if (thisNode) {
90-
resizeOriginalRect.current = nodeRect(thisNode);
91-
}
92-
}, [getNodes, id]);
93-
94-
const handleResize = useCallback(
95-
(_event: unknown, params: { x: number; y: number; width: number; height: number; direction: number[] }) => {
96-
const allNodes = getNodes();
97-
const thisNode = allNodes.find((n) => n.id === id);
98-
if (!thisNode) return;
99-
100-
const candidates = getCandidates(thisNode, allNodes);
101-
const candidateRects = candidates.map(nodeRect);
102-
const resizeRect = { x: params.x, y: params.y, w: params.width, h: params.height };
103-
const { rect, guides } = computeResizeSnap(resizeRect, candidateRects, params.direction, resizeOriginalRect.current ?? undefined);
104-
105-
// ProjectZones are root-level, no offset needed
106-
emitResizeGuides(guides);
107-
108-
const isSnapped = guides.length > 0;
109-
if (isSnapped && !wasResizeSnapped.current) {
110-
triggerHaptic();
111-
}
112-
wasResizeSnapped.current = isSnapped;
113-
114-
if (guides.length > 0) {
115-
setNodes((nds) =>
116-
nds.map((n) =>
117-
n.id === id
118-
? {
119-
...n,
120-
position: { x: rect.x, y: rect.y },
121-
style: { ...n.style, width: rect.w, height: rect.h },
122-
}
123-
: n,
124-
),
125-
);
126-
}
127-
},
128-
[getNodes, setNodes, id],
129-
);
130-
131-
const handleResizeEnd = useCallback(() => {
132-
emitResizeGuides([]);
133-
wasResizeSnapped.current = false;
134-
resizeOriginalRect.current = null;
135-
}, []);
136-
13776
const handleDelete = useCallback(() => {
13877
deleteProject(project.id);
13978
}, [deleteProject, project.id]);
@@ -228,9 +167,6 @@ export const ProjectZone = memo(({ id, data }: NodeProps<ProjectZoneNode>) => {
228167
isVisible
229168
minWidth={400}
230169
minHeight={200}
231-
onResizeStart={handleResizeStart}
232-
onResize={handleResize}
233-
onResizeEnd={handleResizeEnd}
234170
lineStyle={{ borderColor: "transparent", borderWidth: 20 }}
235171
handleStyle={{
236172
backgroundColor: "transparent",

src/components/canvas/SnapGuides.tsx

Lines changed: 0 additions & 66 deletions
This file was deleted.

0 commit comments

Comments
 (0)