feat(viewer): consolidate stable core and split-ready compatibility#94
feat(viewer): consolidate stable core and split-ready compatibility#94jack-arturo merged 45 commits intomainfrom
Conversation
- Add server URL input field with default Railway URL - Use setServerConfig to persist both URL and token - Test connection before saving credentials - Improve error display with styled alert box 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add flask-cors dependency - Enable CORS on all routes for cross-origin browser access 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Backend: - Add viewer blueprint to serve React SPA from /viewer/ - Support hash-based token auth (#token=xxx) for embedded mode - Enable CORS for cross-origin browser access - Optional via ENABLE_GRAPH_VIEWER env (default: true) Frontend (packages/graph-viewer/): - 3D WebGL graph visualization with React Three Fiber - Force-directed layout using d3-force-3d - Search, filter, and community isolation features - Inspector panel for memory details - Configurable server URL and token Infrastructure: - Multi-stage Dockerfile (Node.js build + Python runtime) - Frontend built during Docker build, not committed to repo Access at: https://your-server/viewer/#token=YOUR_API_TOKEN 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The viewer serves static files that don't need API auth. Authentication is handled client-side via URL hash fragment (#token=xxx). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Assets were loading from root (/) instead of /viewer/static/, causing 401 errors since those paths require authentication. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements Meta Quest-style hand gesture controls using MediaPipe Hands: - Two-hand spread/pinch for zoom in/out - Two-hand rotation for orbiting camera - Two-hand pan for moving view - Hand skeleton wireframe overlay (cyan left, magenta right) - Toggle button in header to enable/disable gesture control Requires camera permission when enabled. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add GestureDebugOverlay component showing all hand tracking data: - FPS counter, tracking status, hands detected - Two-hand gesture values (distance, rotation, zoom/rotate/pan deltas) - Single-hand gestures (pinch strength, grab strength with progress bars) - Pinch ray data (origin, direction, strength) for both hands - Key landmark positions for each detected hand - Implement Meta Quest-style pinch ray (laser pointer): - Ray origin at midpoint between thumb and index tips - Direction from wrist toward pinch point - Visual intensity based on pinch strength - Glow sphere and ring at origin when pinch active - Dashed line when not fully pinched, solid when engaged - Integrate debug overlay into App with toggle button: - Debug button appears when gestures are enabled - Real-time gesture state updates via callback 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create Hand2DOverlay component using SVG for crisp rendering - Hand appears as screen overlay (not inside 3D scene) - Scale based on hand depth: closer to camera = larger hand - Include pinch-to-laser with glow effects - Mirror X axis to match natural hand movement - Disable 3D HandSkeletonOverlay in favor of 2D version The hand now appears life-size against the screen, matching the user's actual hand position relative to the camera. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove X-axis mirroring so hand points same direction as user's - Invert depth scaling: closer to camera = smaller (going into screen) - Laser now points toward center of graph (the nexus) - Hand shrinks as it "reaches into" the screen toward the memory graph 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
… and two-hand support - Add position interpolation with SMOOTHING_FACTOR for fluid hand tracking - Implement ghost effect with configurable fade when hand disappears - Track both left (cyan) and right (magenta) hands - Lasers always point toward center nexus - Grip indicator: laser turns white/bright when pinched - Connection line between hands when both gripping - Impact 'warm spot' at center where laser hits - SVG glow filters for visual effects 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When both hands are gripping (pinch valid), users can: - SPREAD hands apart = zoom in (move camera closer) - PINCH hands together = zoom out - ROTATE hands around each other = orbit camera - MOVE both hands together = pan view - PULL both hands toward camera = dolly in (pull graph closer) - PUSH both hands away = dolly out Falls back to wrist-based gestures when hands are detected but not gripping. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Performance optimizations implemented: - Instanced mesh rendering for nodes (1 draw call for all nodes) - Batched LineSegments for edges (1 draw call for all edges) - Reduced sphere geometry from 32x32 to 12x12 segments - LOD labels: only show labels for nearby/selected nodes (max 10) - Performance mode toggle: disables Bloom/Vignette post-processing - Single useFrame callback instead of per-node callbacks - Reusable temp objects to avoid GC pressure - Frustum culling enabled on instanced mesh Before: ~100 draw calls, 200 useFrame callbacks, 100k vertices After: ~3 draw calls, 3 useFrame callbacks, shared geometry 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Disable auto-rotate by default (start still, less disorienting) - Add BUILD_VERSION indicator in header to verify deployments - Current version: 2024-12-11-perf-v2 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ost hand UI improvements for hand gesture controls: - Add gesture smoothing with lerp, deadzone, and max speed clamps - Implement gentle recentering when not actively manipulating - Make lasers default toward center (70%) with hand-based deviation (30%) - Replace skeleton splines with translucent ghost hand effect - Ghost hand features palm fill, finger paths, gradients, and glow effects 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Un-mirror hand X coordinates (webcam is mirrored, flip back for natural feel) - Change gesture from camera manipulation to cloud translation - Pinch + pull back = pull memory cloud closer toward you - Spread + push forward = push memory cloud away - More physically intuitive interaction model 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Gesture improvements: - Single hand: pinch + pull/push now translates cloud in Z - Two hands: compound forces create 3D rotation (X and Y axes) - Left hand X movement rotates Y, right hand X movement rotates opposite - Creates torque effect at laser intersection point - Both hands contribute to Z translation additively - Smooth recentering for both position and rotation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Visual improvements: - Inverted depth scaling: hand closer to camera = smaller (farther in 3D) - Puffy white glove appearance like Mario/cartoon hands - Semi-transparent with soft edges and volumetric feel - Rounded fingertips with highlight dots - Knuckle bumps and wrist cuff details - Layered strokes create puffy tube effect on fingers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Reimplemented hand rendering with Smash Bros Master Hand aesthetic: - Filled volumetric finger shapes (capsule/sausage geometry) - Proper palm shape connecting finger bases - Radial gradient from white to soft lavender - Ambient occlusion shadows in finger creases - Knuckle definition shadows - Rim lighting effect on edges - Specular highlight dots on fingertips - Soft drop shadow for depth - Wrist cuff detail 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduces stable pointer ray with arm model and One Euro Filter for accurate hand-based node selection and manipulation. Adds ExpandedNodeView for animated node expansion, LaserPointer for 3D laser visualization, and useHandInteraction hook for gesture-driven graph interaction. Updates GraphCanvas and Hand2DOverlay to support pinch-to-select, Z manipulation, two-hand rotation/zoom, and visual hit indicators.
Introduces a new WebSocket server for iPhone hand tracking with web visualization in packages/graph-viewer/scripts. Refines gesture and pointer logic in GraphCanvas, useHandInteraction, and related components for more accurate hand-based interactions. Updates AGENTS.md with a task completion checklist. Removes unused code and types for clarity.
Introduces a new hand lock and grab state machine for intentional gesture-based cloud manipulation. Adds the useHandLockAndGrab hook, HandControlOverlay UI, and integrates these into App and GraphCanvas. Updates useHandInteraction to allow disabling pinch-to-select when using grab controls, and enhances iPhone hand tracking to compute grab strength. This enables more robust and user-friendly hand gesture interactions for zooming and rotating the graph.
Updated Vite configuration to allow dynamic API target via environment variable, improving flexibility for local and production environments. Adjusted Hand2DOverlay to include hand lock state for visual feedback during interactions. Enhanced GraphCanvas with new aiming and pinch-click features, refining user experience for gesture-based interactions. Added support for URL parameters to override server settings for local development, streamlining testing against remote backends.
Adds a dev:all launcher for starting the iPhone bridge + Vite together, and upgrades the UI/bridge status reporting (browser↔bridge, phone↔bridge, LiDAR availability, ports/IPs) for easier debugging.
Plan to improve memory display and organization while keeping hand gesture features. Key additions: - Obsidian-style settings panel (filters, display, forces, clustering) - Multi-layer relationship visualization with styled edges - Smart clustering (by type, tags, or semantic similarity) - Enhanced selection focus mode with context highlighting - Keyboard shortcuts supplementing gesture navigation Research sources included for graph visualization best practices.
Sprint 1 implementation: - Add SettingsPanel with collapsible sections (Filters, Relationships, Display, Clustering, Forces) - Add SliderControl and ToggleControl UI components - Add ForceConfig, DisplayConfig, ClusterConfig types - Wire force configuration to useForceLayout hook - Add reheat button to restart force simulation - Settings panel docked to right side with toggle button This provides real-time control over graph visualization parameters including node size, link thickness, force strengths, and filtering.
Sprint 2 implementation: - Add edge styles with distinct colors per relationship type - Causal (blue): LEADS_TO, EVOLVED_INTO, DERIVED_FROM - Temporal (orange/gray): OCCURRED_BEFORE, INVALIDATED_BY - Associative (green): EXEMPLIFIES, REINFORCES, RELATES_TO - Conflict (red): CONTRADICTS - Hierarchical (violet/slate): PREFERS_OVER, PART_OF - Filter edges by relationship type visibility - Apply link thickness and opacity from display settings - Apply node size scale from display settings - Toggle label visibility and configure fade distance Display settings now control: - Edge colors based on relationship type - Edge visibility per relationship type - Link thickness and opacity - Node size scaling - Label visibility and fade distance
- Add useClusterDetection hook for detecting clusters by type, tags, or semantic similarity - Add ClusterBoundaries component rendering dotted spheres around clusters - Integrate clustering config through GraphCanvas to Scene - Support three clustering modes: type (memory type), tags (first tag), semantic (connected components) - Boundaries gently rotate for visual interest - Fibonacci sphere point distribution for even dotted effect
- Add SelectionHighlight component with glowing ring around selected node - Add ConnectedPathsHighlight with flowing particles along edges - Pulsing animation on selection ring for visual feedback - Three-ring display (XY and XZ planes) for 3D depth perception - Particles flow from selected node to connected nodes - Track selected node layout position for accurate highlight placement
- Add useKeyboardNavigation hook for Obsidian-style keyboard shortcuts - Arrow keys navigate between nodes spatially (up/down/left/right) - Shift+Arrow up/down for Z-axis navigation (forward/backward) - Tab/Shift+Tab cycles through nodes sequentially - Escape to deselect, R to reheat, L to toggle labels - Comma to toggle settings panel - Question mark shows help in console - Ignores input when focus is in text fields
Added the 'ws' package to dependencies and updated npm scripts to use 'npx' for Vite commands in package.json for improved compatibility.
Introduces a UI toggle to switch between MediaPipe (webcam) and iPhone hand tracking sources, with initial source determined by URL parameters. Refactors tracking source state management, updates related props, and improves MediaPipe hand gesture cleanup to prevent errors during component unmount.
Revised iPhone hand landmark keys to match Vision framework abbreviations and updated all related calculations to use new keys. Added debug logging for tracking source changes, incoming iPhone messages, and landmark keys. Improved LiDAR depth normalization for MediaPipe compatibility and enhanced error handling and message logging in the WebSocket connection.
Refactors hand gesture controls to use displacement-based panning and depth for more intuitive graph manipulation, replacing velocity-based movement. Adds a 'Reset View' button to center the graph and reset rotation, with support for this callback throughout the app. Enhances hand overlay visuals with activation flash, improved opacity logic, and more accurate laser/pointing detection. Updates hand lock and grab logic for more robust pose detection and longer lock persistence.
Introduces several new interactive features to the graph viewer, including a bookmarks panel for saving and restoring camera positions, lasso selection for bulk node actions, animated edge particles, a pathfinding overlay, a timeline bar for time travel, a radial menu for node actions, and a tag cloud for filtering. Updates App.tsx and GraphCanvas.tsx to integrate these features and their state management, and adds supporting hooks and components. Also improves keyboard navigation and sound effects integration.
Introduces a new useHandCursor hook for simplified hand cursor tracking, replacing complex pointing and pinch logic in GraphCanvas. Refactors force layout logic in useForceLayout to use a module-level cache for improved stability and performance, and updates related components for more stable data references and improved event handling. Also improves lasso overlay usability and hand overlay depth feedback.
Simplifies hand gesture controls to only support fist grab for panning the graph. Removes hand cursor, laser pointer, expanded node selection, and related overlays and hooks. Updates overlays to provide only basic visual hand feedback without lasers or node selection.
Enhances hand tracking by refining hand lock acquisition, adding depth-aware pointing and pinch detection, and improving overlay visualization for both MediaPipe and iPhone LiDAR sources. The commit introduces more intentional hand lock gating, depth-based pointing heuristics, and visual feedback for hand state and depth. It also fixes candidate hand tracking, improves inertial panning, and updates the debug overlay to show world Z in meters.
Introduces hooks for recording and replaying hand gesture data, enabling automated testing and playback of hand interactions. Updates gesture state to include pinchPoint for direct pinch selection, refactors selection logic to use pinchPoint, and adds visual feedback for pinch pre-select. UI now displays recording and playback indicators, and exposes global automation APIs in test mode.
Introduces two-hand pinch gesture for pan/zoom/rotate in the graph viewer, with visual feedback and bimanual state tracking. Updates gesture handling hooks to support bimanual pinch, selection clearing via open palm, and consistent coordinate mirroring for both MediaPipe and iPhone hand tracking. Also updates UI overlays and node dimming for improved clarity. Adds ESLint and Flake8 config files for code quality.
|
No actionable comments were generated in the recent review. 🎉 📝 WalkthroughSummary by CodeRabbitRelease Notes
WalkthroughThis PR introduces a Graph Viewer integration that enables the API to forward viewer requests to a standalone graph viewer service. Changes include CORS support, environment variable configuration for the viewer URL and allowed origins, a viewer compatibility layer at Changes
Sequence DiagramsequenceDiagram
participant Client
participant AutoMem as AutoMem API
participant Viewer as Graph Viewer Service
Client->>AutoMem: GET /viewer/my-graph
Note over AutoMem: Check ENABLE_GRAPH_VIEWER
AutoMem->>AutoMem: Build bootstrap HTML<br/>(preserve hash, add server param)
AutoMem->>Client: 200 OK + HTML bootstrap
Client->>Client: Execute redirect JS<br/>(to Graph Viewer with params)
Client->>Viewer: GET /my-graph?server=<automem-origin>#token=...
Viewer->>Client: 200 OK (graph data)
Client->>AutoMem: GET /viewer/assets/index.js
AutoMem->>Client: 302 Redirect
Client->>Viewer: GET /assets/index.js
Viewer->>Client: 200 OK (asset)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (19)
packages/graph-viewer/src/components/RadialMenu.tsx-94-99 (1)
94-99:⚠️ Potential issue | 🟡 MinorHandle clipboard API errors gracefully.
navigator.clipboard.writeText()returns a Promise that can reject (e.g., in non-secure contexts or if permission is denied). The current implementation ignores failures silently, leaving users without feedback.🛡️ Proposed fix to add error handling
// Copy ID to clipboard - const handleCopyId = useCallback(() => { - navigator.clipboard.writeText(node.id) - onCopyId?.(node.id) - onClose() + const handleCopyId = useCallback(async () => { + try { + await navigator.clipboard.writeText(node.id) + onCopyId?.(node.id) + } catch (err) { + console.error('Failed to copy ID to clipboard:', err) + } + onClose() }, [node.id, onCopyId, onClose])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/components/RadialMenu.tsx` around lines 94 - 99, The handleCopyId handler calls navigator.clipboard.writeText(node.id) but doesn't handle rejected Promises; wrap the clipboard call in an async try/catch (or use .then/.catch) inside handleCopyId so failures are handled: await navigator.clipboard.writeText(node.id) inside try, call onCopyId?.(node.id) and onClose() only on success, and in the catch branch log the error (console.error) and surface user feedback (e.g., call an optional onCopyError callback or fallback to document.execCommand('copy') and/or show a UI toast). Update the handleCopyId definition accordingly to properly reference handleCopyId, navigator.clipboard.writeText, onCopyId, onClose (and add an onCopyError prop if you choose to report errors).packages/graph-viewer/src/hooks/useLassoSelection.ts-188-195 (1)
188-195:⚠️ Potential issue | 🟡 MinorDuplicate
onSelectionChangecallback invocation.
clearSelectioncallsonSelectionChange?.([])directly on line 194, but lines 217-224 also invokeonSelectionChangeduring render when selection changes. This causes the callback to fire twice when clearing selection.Remove the direct call here and let the render-time detection handle it consistently with other mutations like
toggleNodeSelection.Proposed fix
// Clear all selected nodes const clearSelection = useCallback(() => { setState((prev) => ({ ...prev, selectedIds: new Set(), })) - onSelectionChange?.([]) - }, [onSelectionChange]) + }, [])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/hooks/useLassoSelection.ts` around lines 188 - 195, The clearSelection callback currently both updates state and directly calls onSelectionChange, causing duplicate callbacks; remove the direct onSelectionChange?.([]) invocation from the clearSelection function (leave setState((prev) => ({ ...prev, selectedIds: new Set() })) intact) so that the existing render-time detection/effect that watches selectedIds (the logic that invokes onSelectionChange when selection changes) is the single place that notifies listeners. Ensure clearSelection still uses the useCallback and its dependency array remains correct (it should not include onSelectionChange since it will no longer call it directly).packages/graph-viewer/src/components/SelectionActions.tsx-94-104 (1)
94-104:⚠️ Potential issue | 🟡 MinorCSV escaping is incomplete for edge cases.
The current CSV generation only escapes double quotes in
content. Ifcontentcontains newlines,idcontains commas, ortagscontain quotes, the CSV will be malformed. Consider escaping all fields consistently.🔧 Proposed fix with consistent escaping
+ // Helper to escape CSV field + const escapeCSV = (value: string | number) => { + const str = String(value) + if (str.includes(',') || str.includes('"') || str.includes('\n')) { + return `"${str.replace(/"/g, '""')}"` + } + return str + } + // Export selection as CSV const handleExportCSV = () => { if (onExportSelection) { onExportSelection(selectedNodes, 'csv') } else { const headers = ['id', 'content', 'type', 'tags', 'importance', 'timestamp'] const rows = selectedNodes.map((n) => [ - n.id, - `"${n.content.replace(/"/g, '""')}"`, - n.type, - `"${n.tags.join(', ')}"`, - n.importance, - n.timestamp, + escapeCSV(n.id), + escapeCSV(n.content), + escapeCSV(n.type), + escapeCSV(n.tags.join(', ')), + escapeCSV(n.importance), + escapeCSV(n.timestamp), ].join(',') )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/components/SelectionActions.tsx` around lines 94 - 104, The CSV generation currently only escapes double quotes in content and misses other edge cases; update SelectionActions.tsx to consistently escape every field: implement a small helper function (e.g., escapeCsv(value)) that converts undefined/null to empty string, stringifies the value, replaces any double quotes with two double quotes, and wraps the result in double quotes if the value contains commas, quotes or newlines (or simply always wrap all fields in quotes). Then use this helper when building headers and when mapping selectedNodes to rows (replace the current inline escaping for n.content and the raw usage of n.id, n.tags, etc.) so id, content, type, tags, importance, and timestamp are all escaped consistently.packages/graph-viewer/src/components/settings/SettingsSection.tsx-15-28 (1)
15-28:⚠️ Potential issue | 🟡 MinorAdd
type="button",aria-expanded, andaria-controlsattributes for accessibility and form safety.The button lacks an explicit type attribute (defaults to
type="submit"), should expose its expanded state viaaria-expanded, and establish a control relationship with the content viaaria-controls. The content div requires anidto be referenced.🔧 Suggested fix
-import { useState, ReactNode } from 'react' +import { useState, ReactNode, useId } from 'react' import { ChevronDown } from 'lucide-react' interface SettingsSectionProps { title: string defaultOpen?: boolean children: ReactNode } export function SettingsSection({ title, defaultOpen = true, children }: SettingsSectionProps) { const [isOpen, setIsOpen] = useState(defaultOpen) + const contentId = useId() return ( <div className="border-b border-white/5 last:border-b-0"> <button onClick={() => setIsOpen(!isOpen)} + type="button" + aria-expanded={isOpen} + aria-controls={contentId} className="w-full flex items-center justify-between px-4 py-3 hover:bg-white/5 transition-colors" > <span className="text-sm font-medium text-slate-300">{title}</span> <ChevronDown className={`w-4 h-4 text-slate-500 transition-transform duration-200 ${ isOpen ? 'rotate-0' : '-rotate-90' }`} /> </button> {isOpen && ( - <div className="px-4 pb-4 space-y-3"> + <div id={contentId} className="px-4 pb-4 space-y-3"> {children} </div> )} </div> ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/components/settings/SettingsSection.tsx` around lines 15 - 28, The SettingsSection button toggle currently omits type and ARIA attributes; update the <button> in the SettingsSection component (the one that calls setIsOpen and reads isOpen, renders title and ChevronDown) to include type="button", aria-expanded={isOpen}, and aria-controls pointing to the content container, and give the content <div> (the one that wraps children) a stable id; generate the id using React's useId() or accept a prop (e.g., sectionId) so aria-controls matches that id and remains unique/stable across renders.packages/graph-viewer/scripts/hand-tracking-server.js-205-207 (1)
205-207:⚠️ Potential issue | 🟡 MinorFix incorrect port in user-facing hint.
The HTML displays port 8765, but
PHONE_PORTdefaults to 8768. This will confuse users trying to connect their iPhone.🐛 Proposed fix
<div class="info"> - iPhone should connect to: ws://<your-mac-ip>:8765 + iPhone should connect to: ws://<your-mac-ip>:${PHONE_PORT} </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/scripts/hand-tracking-server.js` around lines 205 - 207, The user-facing hint in the HTML (the <div class="info"> text) shows port 8765 but the server default constant PHONE_PORT (in hand-tracking-server.js) is 8768, so update the hint to use the PHONE_PORT value (or change the hardcoded 8765 to match PHONE_PORT) so they stay in sync; locate the string in the HTML rendering and either interpolate PHONE_PORT into that message or replace the literal "8765" with the PHONE_PORT constant to avoid future drift.packages/graph-viewer/src/hooks/useForceLayout.ts-38-42 (1)
38-42:⚠️ Potential issue | 🟡 MinorData signature may produce false cache hits.
The signature only considers
length, first ID, and last ID. If nodes in the middle are replaced while first/last IDs and count remain unchanged, the cache will incorrectly return stale positions.Consider using a hash of all node IDs or including a version/timestamp from the data source.
🛡️ Proposed fix
function createDataSignature(nodes: GraphNode[]): string { if (nodes.length === 0) return '' - return `${nodes.length}-${nodes[0]?.id}-${nodes[nodes.length - 1]?.id}` + // Include all IDs for accurate cache invalidation + return nodes.map(n => n.id).join(',') }If this becomes expensive for large graphs, consider a hash function or including a data version from the API response.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/hooks/useForceLayout.ts` around lines 38 - 42, The createDataSignature function currently only uses nodes.length, nodes[0].id and nodes[last].id which can produce false cache hits; update createDataSignature to incorporate all node identities (e.g., concatenate or compute a hash over nodes.map(n => n.id).join(',')) or include a data-version/timestamp from the source so any middle-node changes invalidate the cache; ensure you reference createDataSignature and GraphNode and replace the simple triple-field string with a full-ID-derived signature or hashed value to avoid stale position reuse.packages/graph-viewer/src/utils/OneEuroFilter.ts-254-261 (1)
254-261:⚠️ Potential issue | 🟡 MinorGuard against landmark array length mismatch.
If
landmarks.lengthexceeds 21,this.filters[i]will beundefinedfor indices ≥ 21, causing a runtime error when calling.filter(). Consider validating or slicing the input.🛡️ Proposed fix
filter( landmarks: Array<{ x: number; y: number; z: number; visibility?: number }>, timestamp?: number ): Array<{ x: number; y: number; z: number; visibility?: number }> { - return landmarks.map((lm, i) => ({ - ...this.filters[i].filter(lm, timestamp), + return landmarks.slice(0, 21).map((lm, i) => ({ + ...this.filters[i]!.filter(lm, timestamp), visibility: lm.visibility, })) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/utils/OneEuroFilter.ts` around lines 254 - 261, The filter method in OneEuroFilter assumes landmarks.length matches this.filters.length and will throw if landmarks has >21 entries; guard against length mismatches by limiting the input or iterating against this.filters length: in the filter(...) method, either slice the incoming landmarks array to this.filters.length or map over this.filters (e.g., use this.filters.map((f, i) => ... ) and safely handle missing landmark entries), ensuring you reference this.filters and the filter(...) call on each filter; also preserve visibility from the original landmark only when that landmark exists.packages/graph-viewer/src/hooks/useCameraState.ts-67-105 (1)
67-105:⚠️ Potential issue | 🟡 MinorCancel pending animation on unmount to prevent memory leaks.
If the component unmounts while an animation is in progress, the
requestAnimationFramecallback continues executing and may reference a stalecameraorcontrolsobject. Add a cleanup effect.🛡️ Proposed fix
export function useCameraNavigation() { const { camera, controls } = useThree() const animationRef = useRef<number | null>(null) + // Cleanup on unmount + useEffect(() => { + return () => { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current) + } + } + }, []) + const navigateTo = useCallback((targetX: number, targetY: number, duration = 500) => { // ... }, [camera, controls]) return { navigateTo } }You'll also need to add
useEffectto the imports on line 7.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/hooks/useCameraState.ts` around lines 67 - 105, Add a cleanup effect to useCameraNavigation that cancels any pending requestAnimationFrame and clears animationRef on unmount to avoid referencing stale camera/controls; specifically import useEffect, add a useEffect(() => { return () => { if (animationRef.current) cancelAnimationFrame(animationRef.current); animationRef.current = null; } }, []) inside the same scope as navigateTo so any in-flight animation is canceled when the component using useCameraNavigation unmounts, referencing animationRef, cancelAnimationFrame, and navigateTo for context.packages/graph-viewer/src/components/TimelineBar.tsx-105-142 (1)
105-142:⚠️ Potential issue | 🟡 MinorMissing
preventDefault()on arrow keys allows unintended node navigation during time travel.When time travel is active, pressing
ArrowLeft/ArrowRighttriggers both timeline stepping (TimelineBar) and spatial node navigation (useKeyboardNavigation) simultaneously, since neither callspreventDefault()on these keys. TimelineBar already callspreventDefault()for the Space key, so add the same for arrow keys to prevent the conflicting behavior.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/components/TimelineBar.tsx` around lines 105 - 142, The keyboard handler in the TimelineBar component's useEffect (handleKeyDown) doesn't call e.preventDefault() for 'ArrowLeft' and 'ArrowRight', which allows competing handlers (e.g., useKeyboardNavigation) to run; update handleKeyDown so that when handling both ArrowLeft and ArrowRight (including their shiftKey variants that call onStepBackward/onStepForward/onGoToStart/onGoToEnd) you call e.preventDefault() before invoking those callbacks to stop default/spatial navigation and ensure only the timeline actions run.packages/graph-viewer/src/api/client.ts-37-45 (1)
37-45:⚠️ Potential issue | 🟡 MinorToken exposed in URL query parameter may leak via referrer headers and browser history.
The
getTokenFromQuery()function reads tokens from URL query parameters. While convenient for development, query parameters are logged in server access logs, browser history, and can leak viaRefererheaders. The hash-based approach (getTokenFromHash) is more secure since fragments aren't sent to the server.Consider documenting this security trade-off or restricting query param tokens to development mode only.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/api/client.ts` around lines 37 - 45, getTokenFromQuery reads sensitive tokens from URL query params which can leak via referrer headers and history; update getToken so it only falls back to getTokenFromQuery in non-production/dev environments (e.g., check NODE_ENV or import.meta.env.MODE) or remove the query-param branch entirely and prefer getTokenFromHash() and localStorage.getItem('automem_token'); also add a short comment near getTokenFromQuery/getToken noting the security trade-off and that query tokens are allowed only for development/testing.packages/graph-viewer/src/hooks/useFocusMode.ts-10-10 (1)
10-10:⚠️ Potential issue | 🟡 MinorMinor doc inconsistency: comment says 10% but constant is 8%.
Line 10 states "Beyond: 10% opacity" but
DEFAULT_OPACITYon line 35 is0.08(8%). Consider updating the comment to match the actual value.📝 Proposed fix
- * - Beyond: 10% opacity + * - Beyond: 8% opacity🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/hooks/useFocusMode.ts` at line 10, The doc comment in useFocusMode.ts incorrectly states "Beyond: 10% opacity" while the actual constant DEFAULT_OPACITY is 0.08 (8%); update the comment to match the code (or change DEFAULT_OPACITY to 0.10 if you prefer 10%) so they are consistent—locate DEFAULT_OPACITY in useFocusMode.ts and either change the comment text to "Beyond: 8% opacity" or set DEFAULT_OPACITY = 0.10 and adjust any related logic accordingly.packages/graph-viewer/src/api/client.ts-112-123 (1)
112-123:⚠️ Potential issue | 🟡 MinorFalsy check may skip valid zero values for
limitandminImportance.The truthy checks (
if (params.limit)) will not include these parameters when their values are0. Iflimit=0orminImportance=0are valid API values, they won't be sent. Consider using explicit!== undefinedchecks if zero is meaningful.📝 Proposed fix (if zero values are valid)
- if (params.limit) searchParams.set('limit', String(params.limit)) - if (params.minImportance) searchParams.set('min_importance', String(params.minImportance)) + if (params.limit !== undefined) searchParams.set('limit', String(params.limit)) + if (params.minImportance !== undefined) searchParams.set('min_importance', String(params.minImportance))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/api/client.ts` around lines 112 - 123, The fetchGraphSnapshot function currently uses truthy checks for params.limit and params.minImportance which will omit valid zero values; update the conditionals in fetchGraphSnapshot to test explicitly for undefined (e.g., params.limit !== undefined and params.minImportance !== undefined) so that 0 is serialized and sent to the API, leaving the checks for params.types and params.since unchanged; ensure SnapshotParams handling and the URLSearchParams logic still work with the explicit checks.packages/graph-viewer/src/components/MiniMap.tsx-76-98 (1)
76-98:⚠️ Potential issue | 🟡 MinorGuard against zero-sized bounds to prevent NaN coordinates.
If all nodes share the same x/y,
rangeXandrangeYbecome 0, soscaleis 0 and Lines 82 and 94 divide by zero. Add a fallback scale.🛠️ Suggested fix
- const scale = Math.max(rangeX, rangeY) + const scale = Math.max(rangeX, rangeY) || 1- const scale = Math.max(rangeX, rangeY) + const scale = Math.max(rangeX, rangeY) || 1🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/components/MiniMap.tsx` around lines 76 - 98, worldToCanvas and canvasToWorld can divide by zero when bounds are degenerate; guard against zero-sized bounds by providing a fallback scale (e.g., use Math.max(rangeX, rangeY) and if that is 0 use 1 or a small epsilon) and use that fallback in both functions (worldToCanvas and canvasToWorld) so the division never produces NaN; update the scale computation in both useCallback blocks to use the fallback and keep the rest of the coordinate math unchanged.packages/graph-viewer/src/hooks/useHandGestures.ts-319-387 (1)
319-387:⚠️ Potential issue | 🟡 MinorEffect re-runs on
onResultschange causing expensive re-initialization.The initialization effect depends on
onResults, which is recreated wheneversmoothingoronGestureChangechanges. This triggers full MediaPipe re-initialization (stopping camera, closing hands, recreating). Consider using a ref for the callback to avoid this.🐛 Use ref to avoid re-initialization
+ const onResultsRef = useRef(onResults) + useEffect(() => { + onResultsRef.current = onResults + }, [onResults]) // Initialize MediaPipe useEffect(() => { if (!enabled || isInitializedRef.current) return // ... initialization code ... - hands.onResults(onResults) + hands.onResults((results) => onResultsRef.current(results)) // ... rest of initialization ... - }, [enabled, onResults]) + }, [enabled])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/hooks/useHandGestures.ts` around lines 319 - 387, The effect initializing MediaPipe re-runs whenever the onResults callback changes (it’s recreated when smoothing or onGestureChange change), causing expensive teardown/re-init; fix by storing the dynamic callback in a ref (e.g., onResultsRef) and having the effect depend only on enabled (not onResults), set onResultsRef.current = onResults wherever onResults is recreated (or inside a small useEffect watching smoothing/onGestureChange), and register a stable wrapper with hands.onResults that forwards to onResultsRef.current; keep initializeHands, isInitializedRef, handsRef, cameraRef, videoRef logic but remove onResults from the effect dependency list so initialization only runs when enabled changes.packages/graph-viewer/src/components/Hand2DOverlay.tsx-332-334 (1)
332-334:⚠️ Potential issue | 🟡 Minor
handIdbased on position may collide between hands.Using
Math.round(points[0].x * 10)for gradient IDs could cause collisions when both hands are at similar X positions, leading to gradient/filter conflicts. Consider using a stable identifier based on hand side.🐛 Use hand side for stable ID
function GhostHand({ landmarks, color: _color, gradientId: _gradientId, isGhost = false, opacityMultiplier = 1, + side = 'left', }: GhostHandProps) { // ... - const handId = Math.round(points[0].x * 10) + const handId = sideThen pass
side="left"orside="right"from the parent.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/components/Hand2DOverlay.tsx` around lines 332 - 334, The gradient/filter ID generation uses a position-derived value (const handId = Math.round(points[0].x * 10)) which can collide; change Hand2DOverlay to accept a stable side prop (e.g., side: "left" | "right") and compute handId from that (or combine side with a unique index) instead of points[0].x so gradient/filter IDs are deterministic per hand; update the parent to pass side="left" or side="right" and replace uses of handId computed from points with the new side-based identifier.packages/graph-viewer/src/components/SelectionHighlight.tsx-182-192 (1)
182-192:⚠️ Potential issue | 🟡 MinorGeometry clone in render causes memory leak.
ringGeometry.clone()is called on every render, creating new Three.js geometry objects that aren't disposed. This will leak memory over time.🐛 Proposed fix using a second memoized geometry
// Ring geometry const ringGeometry = useMemo(() => { return new THREE.RingGeometry(innerRadius, outerRadius, 32) }, [innerRadius, outerRadius]) + + // Second ring for XZ plane + const ringGeometryXZ = useMemo(() => { + return new THREE.RingGeometry(innerRadius, outerRadius, 32) + }, [innerRadius, outerRadius]) // ... later in JSX ... {/* Selection ring - XZ plane */} <mesh rotation={[Math.PI / 2, 0, 0]}> - <primitive object={ringGeometry.clone()} /> + <primitive object={ringGeometryXZ} /> <meshBasicMaterial🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/components/SelectionHighlight.tsx` around lines 182 - 192, The render currently calls ringGeometry.clone() inside SelectionHighlight's JSX which allocates a new Three.js Geometry each render and leaks memory; change this to create and reuse a cloned geometry (e.g., with useMemo or useRef) such as memoizedRingGeometry = useMemo(() => ringGeometry.clone(), [ringGeometry]) and use that in the <primitive> instead of calling clone() inline, and on component cleanup dispose of the cloned geometry (call dispose() in a useEffect cleanup) so the cloned resource is released. Reference: SelectionHighlight component, ringGeometry variable, and the <primitive> element.packages/graph-viewer/src/App.tsx-319-322 (1)
319-322:⚠️ Potential issue | 🟡 MinorRemove unused
nodesparameter or fix its type.The
nodes as anycast bypasses type checking for a parameter that is never used in the hook.SimulationNode[]cannot acceptGraphNode[]without casting because it expects additional properties (fx,fy,fz). Either remove the unusednodesparameter from theusePathfindingcall, or align the type signature to acceptGraphNode[]directly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/App.tsx` around lines 319 - 322, The call to usePathfinding passes nodes as "nodes as any" to avoid the type mismatch between GraphNode[] and SimulationNode[] (which expects fx/fy/fz) even though usePathfinding does not use those SimulationNode-specific props; either remove the nodes parameter from the usePathfinding invocation (call usePathfinding({ edges }) and update the hook signature if necessary) or change the hook signature to accept GraphNode[] (update usePathfinding's param type from SimulationNode[] to GraphNode[] and remove the cast), ensuring references to usePathfinding, nodes, SimulationNode[], and GraphNode[] are updated accordingly.packages/graph-viewer/src/hooks/useHandPlayback.ts-436-436 (1)
436-436:⚠️ Potential issue | 🟡 Minor
hasRecordingreads ref directly and won't update UI.
recordingRef.current !== nullis evaluated at render time but won't cause re-renders whenloadRecordingis called since refs don't trigger updates. TherecordingNamefrom state can be used instead.🛠️ Proposed fix
- hasRecording: recordingRef.current !== null, + hasRecording: state.recordingName !== '',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/hooks/useHandPlayback.ts` at line 436, The hasRecording flag currently reads recordingRef.current directly so it won't trigger re-renders; change it to derive from the component state instead (e.g., use recordingName). Replace hasRecording: recordingRef.current !== null with something like hasRecording: Boolean(recordingName) or hasRecording: recordingName !== '' in the return/object provided by useHandPlayback so UI updates when loadRecording updates recordingName; keep recordingRef and loadRecording as-is for imperative access.packages/graph-viewer/src/components/GraphCanvas.tsx-1448-1454 (1)
1448-1454:⚠️ Potential issue | 🟡 MinorObject allocation inside render loop for lasso-selected nodes.
A new
THREE.Coloris created inside the useFrame loop for each lasso-selected node. This should use the existingtempColorobject and a separate pre-allocated color for lerping.🛠️ Proposed fix
+ // Add near other temp objects (around line 1322) + const blueColor = useMemo(() => new THREE.Color('#3b82f6'), []) // Inside useFrame, replace: } else if (isLassoSelected) { tempColor.set(node.color) - const blueColor = new THREE.Color('#3b82f6') tempColor.lerp(blueColor, 0.35) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/graph-viewer/src/components/GraphCanvas.tsx` around lines 1448 - 1454, The lasso branch in the useFrame loop (isLassoSelected) allocates a new THREE.Color each frame; instead pre-allocate a reusable color (e.g., lassoBlue) outside the render loop and reuse tempColor for operations. Replace the inline new THREE.Color('#3b82f6') with the preallocated lassoBlue and call tempColor.set(node.color) then tempColor.lerp(lassoBlue, 0.35) inside the isLassoSelected branch (update where tempColor and isLassoSelected are used in GraphCanvas.tsx).
🤖 I have created a release *beep* *boop* --- ## [0.13.0](v0.12.0...v0.13.0) (2026-03-02) ### Features * **bench:** benchmark testing infrastructure for rapid iteration ([#97](#97)) ([80a6f93](80a6f93)) * **recall:** add min_score threshold and adaptive floor filtering ([#73](#73)) ([#101](#101)) ([8df3c08](8df3c08)) * **viewer:** add standalone graph-viewer runtime files ([5bcb6db](5bcb6db)) * **viewer:** consolidate stable core and split-ready compatibility ([#94](#94)) ([958da72](958da72)) * **viewer:** externalize visualizer with /viewer compatibility routes ([29bafcf](29bafcf)) * **viewer:** merge visualizer stable core branch ([96b27bf](96b27bf)) ### Bug Fixes * FalkorDB data not persisting across restarts ([3bbc834](3bbc834)) * FalkorDB data not persisting across restarts ([#99](#99)) ([8490d36](8490d36)) * **mcp-sse:** sync tool schemas for SSE/MCP parity ([#104](#104)) ([d99b86d](d99b86d)) ### Documentation * **bench:** add PR [#73](#73), [#80](#80), and [#87](#87) experiment results ([#103](#103)) ([8533fac](8533fac)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Summary
feat/visualizer-stable-core(stable core only)/viewerfrom in-process static SPA serving to standalone compatibility redirect/bootstrap usingGRAPH_VIEWER_URL/viewerand/viewer/*compatibility routes while keeping hash token handling client-sideVIEWER_ALLOWED_ORIGINSCORS allowlist support and preflight handlingVITE_ENABLE_HAND_CONTROLS=falsedefaultpackages/graph-viewer/.github/workflows/ci.yml.viteartifacts and stale visualizer simplification docConfig Changes
AutoMem env vars:
ENABLE_GRAPH_VIEWER=trueGRAPH_VIEWER_URL=https://<viewer-domain>VIEWER_ALLOWED_ORIGINS=https://<viewer-domain>[,https://... ]Viewer env var:
VITE_ENABLE_HAND_CONTROLS=false(opt-in)Verification
make lint✅AUTOMEM_API_TOKEN=test-token ADMIN_API_TOKEN=test-admin-token EMBEDDING_PROVIDER=auto make test✅ (152 passed, 1 skipped, 10 deselected)cd packages/graph-viewer && npm run build✅Curl checks (local API on :8012)
curl -i http://localhost:8012/graph/snapshot?limit=10 -H 'X-API-Key: test-token'→200 OKcurl -i http://localhost:8012/viewer/?foo=bar→200 OK(bootstrap HTML)curl -i http://localhost:8012/viewer/assets/index.js?v=1→302 FOUNDtohttps://viewer.example.com/assets/index.js?v=1curl -i -X OPTIONS http://localhost:8012/graph/snapshot -H 'Origin: https://viewer.example.com' -H 'Access-Control-Request-Method: GET' -H 'Access-Control-Request-Headers: X-API-Key, Content-Type'→200 OK,Access-Control-Allow-Origin: https://viewer.example.comNotes
make buildtarget does not exist in this repo; verification used lint + tests per current Makefile.git subtree split --prefix=packages/graph-viewer -b split/automem-graph-viewer.