mirror of
https://github.com/crabwise-ai/crabwalk.git
synced 2026-08-14 09:02:07 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c84182b2e4 | ||
|
|
80ddf4d821 | ||
|
|
5af254087c | ||
|
|
f9dbd9b0bf |
Generated
+22
-48
@@ -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"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, useTransition } from 'react'
|
||||
import {
|
||||
ReactFlow,
|
||||
Background,
|
||||
@@ -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, Loader2 } from 'lucide-react'
|
||||
import '@xyflow/react/dist/style.css'
|
||||
import { SessionNode } from './SessionNode'
|
||||
import { ActionNode } from './ActionNode'
|
||||
@@ -33,6 +35,7 @@ interface ActionGraphProps {
|
||||
execs: MonitorExecProcess[]
|
||||
selectedSession: string | null
|
||||
onSessionSelect: (key: string | null) => void
|
||||
isHydrating?: boolean
|
||||
}
|
||||
|
||||
/** Cast domain data to ReactFlow's Node data type */
|
||||
@@ -80,6 +83,7 @@ function ActionGraphInner({
|
||||
execs,
|
||||
selectedSession,
|
||||
onSessionSelect,
|
||||
isHydrating = false,
|
||||
}: ActionGraphProps) {
|
||||
// Crab AI state
|
||||
const crabRef = useRef<CrabAI>({
|
||||
@@ -100,6 +104,26 @@ 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)
|
||||
|
||||
// Transition for large graph layout calculations
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const [asyncLayoutResult, setAsyncLayoutResult] = useState<{ nodes: Node[]; edges: Edge[] } | null>(null)
|
||||
|
||||
// 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)
|
||||
@@ -340,23 +364,52 @@ function ActionGraphInner({
|
||||
return edges
|
||||
}, [sessions, visibleActions, visibleExecs, selectedSession])
|
||||
|
||||
// Apply layout
|
||||
const { nodes: layoutedNodes, edges: layoutedEdges } = useMemo(() => {
|
||||
if (rawNodes.length === 1) {
|
||||
return {
|
||||
nodes: [{ ...rawNodes[0]!, position: { x: 0, y: 0 } }],
|
||||
edges: [],
|
||||
// Apply layout - fast path for small graphs, transition for large graphs
|
||||
const LARGE_GRAPH_THRESHOLD = 100
|
||||
|
||||
// Fast path for small graphs (synchronous)
|
||||
const immediateLayout = useMemo(() => {
|
||||
if (rawNodes.length < LARGE_GRAPH_THRESHOLD) {
|
||||
if (rawNodes.length === 1) {
|
||||
return {
|
||||
nodes: [{ ...rawNodes[0]!, position: { x: 0, y: 0 } }],
|
||||
edges: [],
|
||||
}
|
||||
}
|
||||
return layoutGraph(rawNodes, rawEdges, {
|
||||
direction: layoutDirection,
|
||||
nodeWidth: 200,
|
||||
nodeHeight: 80,
|
||||
rankSep: 60,
|
||||
nodeSep: 30,
|
||||
})
|
||||
}
|
||||
return layoutGraph(rawNodes, rawEdges, {
|
||||
direction: layoutDirection,
|
||||
nodeWidth: 200,
|
||||
nodeHeight: 80,
|
||||
rankSep: 60,
|
||||
nodeSep: 30,
|
||||
})
|
||||
return null
|
||||
}, [rawNodes, rawEdges, layoutDirection])
|
||||
|
||||
// Async path for large graphs (uses transition to keep UI responsive)
|
||||
useEffect(() => {
|
||||
if (rawNodes.length >= LARGE_GRAPH_THRESHOLD) {
|
||||
startTransition(() => {
|
||||
const result = layoutGraph(rawNodes, rawEdges, {
|
||||
direction: layoutDirection,
|
||||
nodeWidth: 200,
|
||||
nodeHeight: 80,
|
||||
rankSep: 60,
|
||||
nodeSep: 30,
|
||||
})
|
||||
setAsyncLayoutResult(result)
|
||||
})
|
||||
} else {
|
||||
// Clear async result when switching to small graph
|
||||
setAsyncLayoutResult(null)
|
||||
}
|
||||
}, [rawNodes, rawEdges, layoutDirection])
|
||||
|
||||
// Use whichever result is available
|
||||
const layoutedNodes = immediateLayout?.nodes ?? asyncLayoutResult?.nodes ?? []
|
||||
const layoutedEdges = immediateLayout?.edges ?? asyncLayoutResult?.edges ?? []
|
||||
|
||||
// Initial nodes with chaser (click handler added later)
|
||||
const initialNodes = useMemo(() => {
|
||||
const crab = crabRef.current
|
||||
@@ -457,6 +510,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 +526,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 +545,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 +819,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!"
|
||||
/>
|
||||
<div className="absolute top-2 right-2 z-10 flex gap-1.5">
|
||||
<button
|
||||
onClick={() => setFollowMode((prev) => !prev)}
|
||||
title={followMode ? 'Following new nodes (click to disable)' : 'Follow new nodes'}
|
||||
className={`p-1.5 rounded border shadow-lg cursor-pointer transition-colors ${
|
||||
followMode
|
||||
? 'bg-neon-cyan/20 border-neon-cyan text-neon-cyan backdrop-blur-lg'
|
||||
: 'bg-shell-800 border-shell-700 text-gray-300 hover:bg-shell-700'
|
||||
}`}
|
||||
>
|
||||
<Crosshair className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setLayoutDirection((d) => (d === 'LR' ? 'TB' : 'LR'))
|
||||
@@ -776,6 +852,26 @@ function ActionGraphInner({
|
||||
<LayoutGrid className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/* Loading overlay for layout calculation */}
|
||||
{isPending && (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-shell-950/80 backdrop-blur-sm">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-neon-cyan" />
|
||||
<span className="font-mono text-sm text-gray-400">
|
||||
Calculating layout for {rawNodes.length.toLocaleString()} nodes...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Loading overlay for initial hydration */}
|
||||
{isHydrating && layoutedNodes.length === 0 && (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-shell-950">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-neon-cyan" />
|
||||
<span className="font-mono text-sm text-gray-400">Loading graph data...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<MiniMap
|
||||
nodeColor={(node) => {
|
||||
if (node.type === 'crab') return '#ef4444'
|
||||
|
||||
+24
-2
@@ -35,6 +35,10 @@ const ROOT_START_Y = 200 // Vertical offset from crab to first root session
|
||||
const MIN_SESSION_GAP = 120 // Minimum vertical gap between sessions in same column
|
||||
const ROOT_HORIZONTAL_GAP = 0 // Gap between root sessions in horizontal mode
|
||||
|
||||
// Cache spawn Y positions so they don't change as parent actions accumulate
|
||||
// Key: session key, Value: calculated spawn Y offset
|
||||
const spawnYCache = new Map<string, number>()
|
||||
|
||||
interface SessionColumn {
|
||||
sessionKey: string
|
||||
columnIndex: number
|
||||
@@ -215,6 +219,7 @@ export function layoutGraph(
|
||||
|
||||
// Calculate spawn Y positions for child sessions
|
||||
// When a session is spawned, find the Y position of the parent at that time
|
||||
// Cache these values so they don't jitter as parent actions accumulate
|
||||
for (const session of sessions) {
|
||||
if (!session.spawnedBy) continue
|
||||
|
||||
@@ -222,6 +227,13 @@ export function layoutGraph(
|
||||
const childCol = sessionColumns.get(session.key)
|
||||
if (!parentCol || !childCol) continue
|
||||
|
||||
// Use cached spawn Y if available (prevents jitter from recalculation)
|
||||
const cachedSpawnY = spawnYCache.get(session.key)
|
||||
if (cachedSpawnY !== undefined) {
|
||||
childCol.spawnY = cachedSpawnY
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the approximate position in parent where spawn happened
|
||||
// Use the child's creation time (approximated by first action time or session activity)
|
||||
const childActions = actionsBySession.get(session.key) ?? []
|
||||
@@ -239,8 +251,10 @@ export function layoutGraph(
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate Y based on parent's item count
|
||||
childCol.spawnY = parentItemsBeforeSpawn * (NODE_DIMENSIONS.action.height + ROW_GAP) + SPAWN_OFFSET
|
||||
// Calculate Y based on parent's item count and cache it
|
||||
const calculatedSpawnY = parentItemsBeforeSpawn * (NODE_DIMENSIONS.action.height + ROW_GAP) + SPAWN_OFFSET
|
||||
spawnYCache.set(session.key, calculatedSpawnY)
|
||||
childCol.spawnY = calculatedSpawnY
|
||||
}
|
||||
|
||||
// Position all nodes
|
||||
@@ -362,6 +376,14 @@ export function layoutGraph(
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up spawn Y cache for sessions that no longer exist
|
||||
const currentSessionKeys = new Set(sessions.map(s => s.key))
|
||||
for (const key of spawnYCache.keys()) {
|
||||
if (!currentSessionKeys.has(key)) {
|
||||
spawnYCache.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
return { nodes: positionedNodes, edges }
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,9 @@ function MonitorPage() {
|
||||
// Settings panel state
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
|
||||
// Hydrating state for large graph loading
|
||||
const [isHydrating, setIsHydrating] = useState(false)
|
||||
|
||||
// Live queries from TanStack DB collections
|
||||
const sessionsQuery = useLiveQuery(sessionsCollection)
|
||||
const actionsQuery = useLiveQuery(actionsCollection)
|
||||
@@ -160,17 +163,20 @@ function MonitorPage() {
|
||||
try {
|
||||
const status = await trpc.clawdbot.persistenceStatus.query()
|
||||
if (status.sessionCount > 0 || status.actionCount > 0 || status.execEventCount > 0) {
|
||||
setIsHydrating(true)
|
||||
const data = await trpc.clawdbot.persistenceHydrate.query()
|
||||
hydrateFromServer(data.sessions, data.actions, data.execEvents ?? [])
|
||||
console.log(
|
||||
`[monitor] hydrated ${data.sessions.length} sessions, ${data.actions.length} actions, ${(data.execEvents ?? []).length} exec events`
|
||||
)
|
||||
setIsHydrating(false)
|
||||
}
|
||||
setPersistenceEnabled(status.enabled)
|
||||
setPersistenceStartedAt(status.startedAt)
|
||||
setPersistenceSessionCount(status.sessionCount)
|
||||
setPersistenceActionCount(status.actionCount)
|
||||
} catch (e) {
|
||||
setIsHydrating(false)
|
||||
console.error('Failed to hydrate:', e)
|
||||
}
|
||||
}
|
||||
@@ -499,6 +505,7 @@ function MonitorPage() {
|
||||
execs={execs}
|
||||
selectedSession={selectedSession}
|
||||
onSessionSelect={setSelectedSession}
|
||||
isHydrating={isHydrating}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user