From 2ba3ef6d475133ada336e9cb4dbbcfb43ebf79ea Mon Sep 17 00:00:00 2001 From: Luciano Castillo <2213102+luccast@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:52:29 -0500 Subject: [PATCH] feat(monitor): add follow mode to track new nodes (#23) * feat(monitor): implement follow mode in ActionGraph for new nodes - Added a follow mode feature that automatically pans the viewport to new nodes in the ActionGraph. - Introduced a button to toggle follow mode, enhancing user experience by allowing users to track new nodes easily. - Updated the ActionGraphInner component to manage viewport changes and node tracking effectively. * feat(graph-layout): implement caching for spawn Y positions to prevent jitter - Introduced a caching mechanism for spawn Y positions of child sessions to maintain stability during parent action accumulation. - Updated the layoutGraph function to utilize cached values, reducing recalculation and improving layout consistency. - Added cleanup logic to remove entries from the cache for sessions that no longer exist, ensuring efficient memory usage. * style(ActionGraph): enhance follow mode button appearance - Updated the styling of the follow mode button in the ActionGraphInner component to include a backdrop blur effect, improving visual feedback for users when the mode is active. --- package-lock.json | 70 ++++++++------------------ src/components/monitor/ActionGraph.tsx | 47 +++++++++++++++-- src/lib/graph-layout.ts | 26 +++++++++- 3 files changed, 90 insertions(+), 53 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5d062c3..c4cb678 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "@trpc/client": "^11.0.0", "@trpc/server": "^11.0.0", "@xyflow/react": "^12.10.0", - "bufferutil": "*", "framer-motion": "^12.29.0", "lucide-react": "^0.468.0", "react": "^19.2.0", @@ -73,6 +72,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -2281,6 +2281,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.20.tgz", "integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.90.20" }, @@ -2315,6 +2316,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.157.13.tgz", "integrity": "sha512-rLr6swdDhLjadAHbukg/XYurNi4q8AmmjP99QiWUnhRPKfrfmQ/O1UCvaowQ9GcaiyOG2COiJ9OlDAQU3k9MFQ==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/react-store": "^0.8.0", @@ -2461,6 +2463,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.157.13.tgz", "integrity": "sha512-dl1avpRH2Pi1tYhxDzGEqI6DU9VQUf5zBeW1JJFs3LfJuVkGd2lG93dSUzH9seJNp77hm8JWhoX+k4QYMEB6KA==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/store": "^0.8.0", @@ -2771,6 +2774,7 @@ "https://trpc.io/sponsor" ], "license": "MIT", + "peer": true, "peerDependencies": { "typescript": ">=5.7.2" } @@ -2943,6 +2947,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz", "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3171,6 +3176,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3185,20 +3191,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bufferutil": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", - "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001766", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", @@ -3403,6 +3395,7 @@ "integrity": "sha512-w6c4OdpRNnudVmcgr7brb/+/HmYjMQvYToO/oTrprTwxRUiom3LYWU1PMWuD006okbUWpII1Ea9/+kwpUfmyRg==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "srvx": ">=0.7.1" }, @@ -3444,7 +3437,8 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/d3-color": { "version": "3.1.0", @@ -3503,6 +3497,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -3557,6 +3552,7 @@ "integrity": "sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", @@ -5252,36 +5248,6 @@ } } }, - "node_modules/nitro/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/nitro/node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", - "dev": true, - "license": "BlueOak-1.0.0", - "optional": true, - "peer": true, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/nitro/node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -5289,7 +5255,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 20.19.0" }, @@ -5445,7 +5410,8 @@ "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-2.0.0-alpha.3.tgz", "integrity": "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/ohash": { "version": "2.0.11", @@ -5680,6 +5646,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5689,6 +5656,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -5817,6 +5785,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz", "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -5888,6 +5857,7 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.0.tgz", "integrity": "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -6065,6 +6035,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6154,6 +6125,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6380,6 +6352,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -6490,6 +6463,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/src/components/monitor/ActionGraph.tsx b/src/components/monitor/ActionGraph.tsx index 3e26b30..5b4cd25 100644 --- a/src/components/monitor/ActionGraph.tsx +++ b/src/components/monitor/ActionGraph.tsx @@ -6,6 +6,8 @@ import { MiniMap, useNodesState, useEdgesState, + useReactFlow, + useOnViewportChange, type Node, type Edge, type NodeTypes, @@ -13,7 +15,7 @@ import { MarkerType, ReactFlowProvider, } from '@xyflow/react' -import { LayoutGrid, ArrowRightLeft, ArrowUpDown } from 'lucide-react' +import { LayoutGrid, ArrowRightLeft, ArrowUpDown, Crosshair } from 'lucide-react' import '@xyflow/react/dist/style.css' import { SessionNode } from './SessionNode' import { ActionNode } from './ActionNode' @@ -100,6 +102,22 @@ function ActionGraphInner({ // Layout direction: LR = horizontal (sessions spawn right), TB = vertical (sessions stack down) const [layoutDirection, setLayoutDirection] = useState<'LR' | 'TB'>('LR') + // Follow mode: auto-pan to new nodes + const [followMode, setFollowMode] = useState(false) + const isAnimatingRef = useRef(false) + + // Get ReactFlow instance for viewport control + const { setCenter } = useReactFlow() + + // Detect manual panning and auto-disable follow mode + useOnViewportChange({ + onEnd: useCallback(() => { + if (followMode && !isAnimatingRef.current) { + setFollowMode(false) + } + }, [followMode]), + }) + // Filter actions for selected session, or show all if none selected const visibleActions = useMemo(() => { if (!selectedSession) return actions.slice(-50) @@ -457,6 +475,9 @@ function ActionGraphInner({ const prevPositions = nodePositionsRef.current const crab = crabRef.current + // Track the latest new node for follow mode + let latestNewNode: { x: number; y: number } | null = null + // Check for new nodes for (const node of layoutedNodes) { if (!node.id.includes('crab')) { @@ -470,7 +491,7 @@ function ActionGraphInner({ crab.target = { ...nodeCenter, nodeId: node.id } crab.state = 'chasing' if (timeoutRef.current) clearTimeout(timeoutRef.current) - break + latestNewNode = nodeCenter } // Existing node moved - if we were tracking it or idle, chase it @@ -489,8 +510,17 @@ function ActionGraphInner({ } } + // Follow mode: pan to the latest new node + if (followMode && latestNewNode) { + isAnimatingRef.current = true + setCenter(latestNewNode.x, latestNewNode.y, { zoom: 0.85, duration: 500 }) + setTimeout(() => { + isAnimatingRef.current = false + }, 550) + } + prevNodeIdsRef.current = currentIds - }, [layoutedNodes]) + }, [layoutedNodes, followMode, setCenter]) // Main animation loop - step-based crab movement at 10fps timing useEffect(() => { @@ -754,6 +784,17 @@ function ActionGraphInner({ className="bg-shell-900! border-shell-700! shadow-lg! [&>button]:bg-shell-800! [&>button]:border-shell-700! [&>button]:text-gray-300! [&>button:hover]:bg-shell-700! [&>button>svg]:fill-gray-300!" />
+