feat(extension): graph UX overhaul — live layout, focused card view, radial-on-select, layout fixes - #169
Conversation
Selecting a node now repositions its neighbourhood as concentric rings — the selected node at the origin, direct neighbours on an inner ring, 2-hop neighbours on an outer ring — so the selection's edges fan out with minimal overlap (the GitNexus-style 'clean edges on select'), instead of freezing positions and only dimming. The inner ring is a star, so it is crossing-free; the outer ring anchors each node near its inner-ring parent's angle to keep ring-to-ring edges radial. assignRadialPositions reads the selection's existing BFS hopLayers (no extra traversal). The prior layout is snapshotted once when a selection begins and restored exactly on deselect; a graph-data change discards the snapshot. Applies on canvas click and search/lens-row selection (which now flies the camera to the origin, where the selected node lands). Webview-only, additive — no core/schema/contract change. extension 648 (+5 radial layout tests: centre at origin, ring radii, distinct angles, no-op, restore). Refs: #168
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (25)
📝 WalkthroughUser-Visible & Reviewer-Relevant Changes
Architecture & Package Boundaries
Risks & Known Limitations
Schema & Contract Changes
Deferred Validation
WalkthroughThis PR implements a focused neighborhood graph visualization feature with two complementary interaction modes: radial repositioning on node selection and a dedicated Alt/Cmd+double-click overlay for exploring node neighborhoods. The implementation adds React Flow to the extension webview, refactors camera fitting, and introduces live layout supervision. ChangesFocused Neighborhood Graph View with Radial Selection Layout
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
Comment |
…g moved The on-select radial layout read SelectionTraversal.hopLayers as node layers, but hopLayers holds EDGE ids grouped by BFS depth (it feeds the trace overlay). So assignRadialPositions saw an edge id at hopLayers[0][0], graph.hasNode(edgeId) was false, and it returned immediately — no node ever moved. The type was string[][] for both, so the compiler couldn't catch the mismatch, and the unit test had encoded the same wrong assumption (hand-built node layers). Fix: computeSelection now also returns nodeLayers — NODE ids grouped by BFS depth, layer 0 = the selected node (built from the BFS nodesByDepth it already had). The radial layout consumes nodeLayers; hopLayers stays edge-based for the trace overlay. Added a graphTraversal test pinning the real contract (nodeLayers[0]= [selected], every entry is a node, never an edge). extension 649.
… confirmed) Logs which branch applyLayoutPreset took (applied / noop:already-active / noop:trivial-graph / rejected) + node count, to the Webview Developer Tools console. Helps diagnose 'Circular not working' from the running UI rather than speculatively. Remove once confirmed.
…ew de-overlaps) ForceAtlas2 — the default layout every graph opens with — was the only preset that skipped runReadabilityPass (noverlap), so the default view rendered nodes overlapping each other while circular/hierarchical did not. Run the same anti-collision pass after the force pass. This is the cheapest, highest-impact readability win toward the 'organized like a competitor' look; no engine change. extension 649 (updated the back-to-FA2 test: ranReadabilityPass is now true).
…in wrong coordinate space Root cause of 'only ForceAtlas2 comes into view': fitCameraToNodes computed a node bounding box in GRAPH coordinates and animated the camera to that center + ratio — but Sigma's camera operates in NORMALIZED space, not graph space. For the default FA2 cloud (near where the camera already sat) it looked fine; but Circular (ring at radius ~160), Hierarchical (origin-centred layers), and radial-on-select all relocate nodes to a different coordinate range, so the mis-aimed camera flew to empty space and the graph disappeared. Fix: delegate framing to Sigma's own camera.animatedReset() (after refresh() so bounds are recomputed), which fits the whole graph correctly for any layout. This replaces the hand-rolled graph-space math in cameraFit.ts; both zoomFit (Fit-to- view button) and the layout-preset handler now frame correctly. Widened the camera structural types to expose animatedReset. Updated tests that had encoded the bug as the spec (one was literally named 'reproduces the hierarchical-empty-page bug' and asserted raw-coordinate animation). extension 649.
… (focused-graph-view phase 0) Add @xyflow/react@^12.11.0 to the extension (rules-approved engine for focused subgraphs). Add the ESLint no-restricted-imports boundary the rules doc claimed existed but didn't: bans the legacy 'reactflow' package and confines '@xyflow/react' to webview/blast-radius/ — the main graph stays on Sigma. No code uses React Flow yet; this is the dependency + guardrail groundwork.
…graph-view phase 1) The framework-agnostic core of the React Flow focused view, with zero @xyflow/react import so the logic is unit-testable without the RF runtime: - focusedGraphModel.ts: builds FocusedNode/FocusedEdge from the in-memory graph + a node's SelectionTraversal; rings from nodeLayers; caps the neighbourhood to the top-N most-important nodes (focus always kept) and reports truncatedCount; drops edges with a truncated endpoint; classifies edge direction (caller/callee) - focusedLayout.ts: deterministic centre + concentric rings (mirrors the Sigma radial-on-select math) - edgeStyles.ts: per-relation routed (smoothstep) edge style, reusing edgeColor (shared with Sigma) + direction-aware CALLS emphasis All built from data already in the webview — no host round-trip, no core change. extension 658 (+9 tests).
…ew phase 2) The boxed-card render surface (the first + only @xyflow/react importers, confined to blast-radius/ by ESLint): - SymbolNode.tsx + .module.css: boxed-card custom node — name + kind badge + kind codicon INSIDE the box, source/target handles so edges route to the card edge. Focus/core emphasis via VS Code CSS vars. (This is the GitNexus-style card look.) - FocusedGraphView.tsx + .module.css: <ReactFlow> canvas; maps the pure model/ layout/edge-styles (phase 1) onto RF Node/Edge; fitView; routed (smoothstep) edges with arrowheads; truncation notice. Imports xyflow CSS. - LazyFocusedGraphView.tsx: React.lazy code-split so xyflow is not in the initial graph bundle (loads only when a node is focused). SymbolNode component tests (name, badge, file/class icons). Full-canvas RF render deferred — jsdom gives the canvas zero dimensions, making it flaky; the pure model/layout that decides what/where is already covered in phase 1. extension 661.
…t (focused-graph-view phase 3) Alt/Cmd + double-click a node opens the React Flow boxed-card focused view as a full-canvas overlay over the Sigma graph; a Close button dismisses it. Plain double-click keeps its existing go-to-source behaviour (the spec's plain-double- click trigger would have clobbered it — resolved to a modifier gesture). - SigmaController: new onFocus callback; doubleClickNode reads event.event.original and routes modifier+double-click → onFocus, else → onNavigate. - GraphView: focusedTraversal state; openFocusedView runs computeSelection at the CURRENT depth-control value (honours the depth slider) via a ref so the Sigma mount effect doesn't re-run on depth change; overlay renders LazyFocusedGraphView with in-memory nodes/edges + theme colours; close clears it (Sigma untouched). Honest correction shipped: the webview is a single IIFE (vite lib), so React.lazy can't code-split — xyflow ships in the one bundle; the lazy wrapper defers construction only. Comment + spec corrected. Webview-only; no core/host/contract change. Full gate green: core 282 / exporters 161 / extension 661. Refs: focused-graph-view
…n idles The default graph used a one-shot forceAtlas2.assign (frozen on first paint), which is why it looked static vs graph-DB explorers. Add a continuous force simulation that settles then auto-stops: - LiveLayout (components/liveLayout.ts): wraps a LayoutSupervisor (start/stop/ kill/isRunning); run(ms) starts it + arms an auto-stop timeout; stop clears; dispose stops+kills. Injectable timers; the run cap scales down for large graphs so big repos don't churn the CPU. 6 unit tests (fake supervisor+timers). - SigmaController: after the community seed + quick .assign, start a LiveLayout over FA2LayoutSupervisor so nodes settle live (~2.5s) then idle; dispose kills it; re-applying the ForceAtlas2 preset re-runs the settle. Guarded by typeof Worker so jsdom/tests degrade quietly to the seed layout. Deliberately NOT on select: selection keeps the clean radial snap — re-energizing the sim would drift those rings apart (design D3). Circular/Hierarchical stay fixed (structural). Webview-only; no core/host/contract change. extension 667.
The [layout] console diagnostic (bbf67df) served its purpose — the Circular/ Hierarchical 'not visible' bug was the camera-space issue, since fixed. Removing the temporary logging.
Summary
Graph UX overhaul — makes the graph view lively and readable, closing several gaps vs graph-DB explorers. Bundles four related webview features + the bugfixes found while building them. Webview-only; no core/host/public-contract change. Full suite green (core 282 / exporters 161 / extension 667).
What's in it
1. Live force-layout settle (
8a00c4e) — the "lively" fix. The default graph used a one-shot frozenforceAtlas2.assign; now a continuous FA2 worker simulation settles nodes on mount (~2.5s) then auto-stops to spare CPU.LiveLayoutwrapper (6 unit tests) guards the auto-stop; degrades to the seed layout where no Worker exists (jsdom).2. Focused (React Flow) boxed-card view (
df5201a→ec1bfbb, phases 0–3) — Alt/Cmd+double-click a node opens a React Flow overlay: boxed cards (name + type badge inside), routed smoothstep edges with arrowheads, focus node centred, neighbourhood in concentric rings at the current depth. Built from in-memory data (no host round-trip).@xyflow/reactconfined towebview/blast-radius/by a new ESLint boundary.3. Radial neighbourhood on select (
76ef614,240dcb3) — selecting a node fans its neighbourhood into concentric rings (inner ring crossing-free). Includes the fix for the edge-id-vs-node-id bug that made it do nothing.4. Layout bugfixes —
1dd01dbForceAtlas2 now runs the anti-collision pass (default view de-overlaps);2676723Circular/Hierarchical "graph vanished" fixed (camera was framed in graph space, not Sigma's normalized space → now usesanimatedReset).Affected surfaces
packages/extension(webview only)Test plan
pnpm checkgreen: core 282 / exporters 161 / extension 667@xyflow/reactonly underblast-radius/;reactflowlegacy bannedNotes / honest caveats
@xyflow/reactships in the single webview IIFE —React.lazydefers construction, not bytes (the webview can't code-split). Documented in-code.🤖 Generated with Claude Code