Skip to content

feat(viewer): consolidate stable core and split-ready compatibility#94

Merged
jack-arturo merged 45 commits intomainfrom
feat/visualizer-stable-core
Feb 20, 2026
Merged

feat(viewer): consolidate stable core and split-ready compatibility#94
jack-arturo merged 45 commits intomainfrom
feat/visualizer-stable-core

Conversation

@jack-arturo
Copy link
Copy Markdown
Member

Summary

  • Consolidates visualizer work into feat/visualizer-stable-core (stable core only)
  • Switches AutoMem /viewer from in-process static SPA serving to standalone compatibility redirect/bootstrap using GRAPH_VIEWER_URL
  • Preserves /viewer and /viewer/* compatibility routes while keeping hash token handling client-side
  • Adds VIEWER_ALLOWED_ORIGINS CORS allowlist support and preflight handling
  • Makes hand controls opt-in in graph viewer via VITE_ENABLE_HAND_CONTROLS=false default
  • Adds standalone viewer runtime files and split-repo CI under packages/graph-viewer/.github/workflows/ci.yml
  • Removes committed .vite artifacts and stale visualizer simplification doc
  • Drops frontend build stage from AutoMem root Dockerfile

Config Changes

AutoMem env vars:

  • ENABLE_GRAPH_VIEWER=true
  • GRAPH_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 OK
  • curl -i http://localhost:8012/viewer/?foo=bar200 OK (bootstrap HTML)
  • curl -i http://localhost:8012/viewer/assets/index.js?v=1302 FOUND to https://viewer.example.com/assets/index.js?v=1
  • curl -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.com

Notes

  • Python make build target does not exist in this repo; verification used lint + tests per current Makefile.
  • Split branch for standalone repo has been created with history preservation via git subtree split --prefix=packages/graph-viewer -b split/automem-graph-viewer.

jack-arturo and others added 30 commits December 23, 2025 21:10
- 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
claude and others added 14 commits December 23, 2025 21:59
- 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.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 20, 2026

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Integrated standalone Graph Viewer accessible through a /viewer endpoint with backward compatibility and URL data preservation.
    • Added CORS support for cross-origin API requests with configurable origin allowlisting.
  • Documentation

    • Updated environment variables and deployment documentation with Graph Viewer configuration options and setup guidance.

Walkthrough

This 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 /viewer, and associated tests and documentation.

Changes

Cohort / File(s) Summary
Configuration and Dependencies
.env.example, .flake8, .gitignore, requirements.txt, Dockerfile
Added environment variables for graph viewer configuration, Flake8 linting rules, dependency on flask-cors, and minor comment updates.
Core Viewer Integration
app.py, automem/api/viewer.py, automem/api/runtime_bootstrap.py
Implemented Flask-CORS configuration with dynamic origins, added new viewer blueprint module that forwards requests to standalone Graph Viewer service, conditionally registers viewer blueprint during app bootstrap, and handles CORS preflight and viewer route bypassing in request middleware.
Documentation
README.md, docs/ENVIRONMENT_VARIABLES.md, docs/RAILWAY_DEPLOYMENT.md
Added documentation for Graph Viewer service, configuration variables (ENABLE_GRAPH_VIEWER, GRAPH_VIEWER_URL, VIEWER_ALLOWED_ORIGINS), deployment guidance, and backward-compatibility details.
Tests
tests/contracts/test_routes_contract.py, tests/test_api_endpoints.py
Updated route contract to expect /viewer/ and /viewer/<path:path> endpoints; added tests for viewer bootstrap HTML, asset redirects, missing configuration handling, and CORS preflight responses.

Sequence Diagram

sequenceDiagram
    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)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: externalizing the graph viewer and switching from in-process serving to standalone compatibility mode, which represents the primary objective of consolidating visualizer work and implementing a split-ready architecture.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing the switch from in-process viewer serving to standalone redirect/bootstrap, environment variables, verification steps, and implementation notes that align with the file changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/visualizer-stable-core

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Handle 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 | 🟡 Minor

Duplicate onSelectionChange callback invocation.

clearSelection calls onSelectionChange?.([]) directly on line 194, but lines 217-224 also invoke onSelectionChange during 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 | 🟡 Minor

CSV escaping is incomplete for edge cases.

The current CSV generation only escapes double quotes in content. If content contains newlines, id contains commas, or tags contain 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 | 🟡 Minor

Add type="button", aria-expanded, and aria-controls attributes for accessibility and form safety.

The button lacks an explicit type attribute (defaults to type="submit"), should expose its expanded state via aria-expanded, and establish a control relationship with the content via aria-controls. The content div requires an id to 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 | 🟡 Minor

Fix incorrect port in user-facing hint.

The HTML displays port 8765, but PHONE_PORT defaults to 8768. This will confuse users trying to connect their iPhone.

🐛 Proposed fix
     <div class="info">
-        iPhone should connect to: ws://&lt;your-mac-ip&gt;:8765
+        iPhone should connect to: ws://&lt;your-mac-ip&gt;:${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 | 🟡 Minor

Data 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 | 🟡 Minor

Guard against landmark array length mismatch.

If landmarks.length exceeds 21, this.filters[i] will be undefined for 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 | 🟡 Minor

Cancel pending animation on unmount to prevent memory leaks.

If the component unmounts while an animation is in progress, the requestAnimationFrame callback continues executing and may reference a stale camera or controls object. 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 useEffect to 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 | 🟡 Minor

Missing preventDefault() on arrow keys allows unintended node navigation during time travel.

When time travel is active, pressing ArrowLeft/ArrowRight triggers both timeline stepping (TimelineBar) and spatial node navigation (useKeyboardNavigation) simultaneously, since neither calls preventDefault() on these keys. TimelineBar already calls preventDefault() 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 | 🟡 Minor

Token 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 via Referer headers. 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 | 🟡 Minor

Minor doc inconsistency: comment says 10% but constant is 8%.

Line 10 states "Beyond: 10% opacity" but DEFAULT_OPACITY on line 35 is 0.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 | 🟡 Minor

Falsy check may skip valid zero values for limit and minImportance.

The truthy checks (if (params.limit)) will not include these parameters when their values are 0. If limit=0 or minImportance=0 are valid API values, they won't be sent. Consider using explicit !== undefined checks 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 | 🟡 Minor

Guard against zero-sized bounds to prevent NaN coordinates.

If all nodes share the same x/y, rangeX and rangeY become 0, so scale is 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 | 🟡 Minor

Effect re-runs on onResults change causing expensive re-initialization.

The initialization effect depends on onResults, which is recreated whenever smoothing or onGestureChange changes. 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

handId based 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 = side

Then pass side="left" or side="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 | 🟡 Minor

Geometry 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 | 🟡 Minor

Remove unused nodes parameter or fix its type.

The nodes as any cast bypasses type checking for a parameter that is never used in the hook. SimulationNode[] cannot accept GraphNode[] without casting because it expects additional properties (fx, fy, fz). Either remove the unused nodes parameter from the usePathfinding call, or align the type signature to accept GraphNode[] 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

hasRecording reads ref directly and won't update UI.

recordingRef.current !== null is evaluated at render time but won't cause re-renders when loadRecording is called since refs don't trigger updates. The recordingName from 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 | 🟡 Minor

Object allocation inside render loop for lasso-selected nodes.

A new THREE.Color is created inside the useFrame loop for each lasso-selected node. This should use the existing tempColor object 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).

Comment thread automem/api/viewer.py
Comment thread packages/graph-viewer/src/components/BookmarksPanel.tsx Outdated
Comment thread packages/graph-viewer/src/components/EdgeParticles.tsx Outdated
Comment thread packages/graph-viewer/src/components/ExpandedNodeView.tsx Outdated
Comment thread packages/graph-viewer/src/components/GraphCanvas.tsx Outdated
Comment thread packages/graph-viewer/src/components/GraphCanvas.tsx Outdated
Comment thread packages/graph-viewer/src/components/settings/ToggleControl.tsx Outdated
Comment thread packages/graph-viewer/src/hooks/useLassoSelection.ts Outdated
Comment thread packages/graph-viewer/src/lib/OneEuroFilter.ts Outdated
Comment thread packages/graph-viewer/src/types/d3-force-3d.d.ts Outdated
@jack-arturo jack-arturo merged commit 958da72 into main Feb 20, 2026
7 checks passed
@jack-arturo jack-arturo deleted the feat/visualizer-stable-core branch February 20, 2026 20:40
jack-arturo added a commit that referenced this pull request Mar 2, 2026
🤖 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants