Skip to content

feat(extension): graph UX overhaul — live layout, focused card view, radial-on-select, layout fixes - #169

Merged
dgtalbug merged 11 commits into
mainfrom
feature/radial-on-select
Jun 7, 2026
Merged

feat(extension): graph UX overhaul — live layout, focused card view, radial-on-select, layout fixes#169
dgtalbug merged 11 commits into
mainfrom
feature/radial-on-select

Conversation

@dgtalbug

@dgtalbug dgtalbug commented Jun 7, 2026

Copy link
Copy Markdown
Owner

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).

Bundled intentionally (early setup phase). Each feature has its own OpenSpec change under openspec/changes/ (radial-on-select, focused-graph-view, live-layout).

What's in it

1. Live force-layout settle (8a00c4e) — the "lively" fix. The default graph used a one-shot frozen forceAtlas2.assign; now a continuous FA2 worker simulation settles nodes on mount (~2.5s) then auto-stops to spare CPU. LiveLayout wrapper (6 unit tests) guards the auto-stop; degrades to the seed layout where no Worker exists (jsdom).

2. Focused (React Flow) boxed-card view (df5201aec1bfbb, 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/react confined to webview/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 bugfixes1dd01db ForceAtlas2 now runs the anti-collision pass (default view de-overlaps); 2676723 Circular/Hierarchical "graph vanished" fixed (camera was framed in graph space, not Sigma's normalized space → now uses animatedReset).

Affected surfaces

  • packages/extension (webview only)

Test plan

  • pnpm check green: core 282 / exporters 161 / extension 667
  • New unit tests: focused model/layout/edge-styles, SymbolNode card, LiveLayout lifecycle (auto-stop, dispose, large-graph cap)
  • ESLint two-engine boundary active: @xyflow/react only under blast-radius/; reactflow legacy banned
  • No core/host/exporters/contract change; no new host message

Notes / honest caveats

  • Bundle: @xyflow/react ships in the single webview IIFE — React.lazy defers construction, not bytes (the webview can't code-split). Documented in-code.
  • On-select uses the radial snap (clean rings); the live sim deliberately does NOT re-energize on select (would fight the rings). "Lively" lives on mount + layout-reset.
  • Visual confirmation pending: logic is built + tested, but the FA2 worker / focused canvas couldn't be verified in a live IDE here; if the webview CSP blocks the Worker, live-layout degrades silently to the seed layout.

🤖 Generated with Claude Code

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
@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 18242166-df9e-48f0-869f-2ed8cfdc2d3c

📥 Commits

Reviewing files that changed from the base of the PR and between 5b4ac15 and 8d3729c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • eslint.config.js
  • packages/extension/package.json
  • packages/extension/src/webview/blast-radius/FocusedGraphView.module.css
  • packages/extension/src/webview/blast-radius/FocusedGraphView.tsx
  • packages/extension/src/webview/blast-radius/LazyFocusedGraphView.tsx
  • packages/extension/src/webview/blast-radius/SymbolNode.module.css
  • packages/extension/src/webview/blast-radius/SymbolNode.test.tsx
  • packages/extension/src/webview/blast-radius/SymbolNode.tsx
  • packages/extension/src/webview/blast-radius/edgeStyles.ts
  • packages/extension/src/webview/blast-radius/focusedGraph.test.ts
  • packages/extension/src/webview/blast-radius/focusedGraphModel.ts
  • packages/extension/src/webview/blast-radius/focusedLayout.ts
  • packages/extension/src/webview/components/GraphView.module.css
  • packages/extension/src/webview/components/GraphView.tsx
  • packages/extension/src/webview/components/SigmaController.test.tsx
  • packages/extension/src/webview/components/SigmaController.ts
  • packages/extension/src/webview/components/cameraFit.ts
  • packages/extension/src/webview/components/fitCameraToNodes.test.tsx
  • packages/extension/src/webview/components/graphLayoutPresets.test.ts
  • packages/extension/src/webview/components/graphLayoutPresets.ts
  • packages/extension/src/webview/components/graphTraversal.test.ts
  • packages/extension/src/webview/components/graphTraversal.ts
  • packages/extension/src/webview/components/graphViewTypes.ts
  • packages/extension/src/webview/components/liveLayout.test.tsx
  • packages/extension/src/webview/components/liveLayout.ts

📝 Walkthrough

User-Visible & Reviewer-Relevant Changes

  • Focused card view overlay: Alt/Cmd+double-click opens a boxed-card React Flow canvas with the selected node centered, neighbourhood fanned into concentric rings (2-hop max), routed smoothstep edges with arrowheads, and a truncation notice if >60 nodes are capped.
  • Radial neighbourhood layout on selection: Selecting a node repositions its direct neighbours (ring 1) and 2-hop nodes (ring 2) into concentric circles via assignRadialPositions, with snapshot/restore to recover the prior layout on selection clear or graph change.
  • Live ForceAtlas2 layout settle: Replaces one-shot forceAtlas2.assign with a LiveLayout wrapper that runs the worker simulation on mount (~2.5s), auto-stops to save CPU, and can be restarted after layout preset changes; degrades gracefully when Worker is unavailable.
  • Camera framing fix: Circular/Hierarchical layout presets now use Sigma's animatedReset() for proper camera framing instead of manual bounds calculation, preventing graph vanishing on preset reapplication.

Architecture & Package Boundaries

  • New ESLint Flat Config boundary enforces two-engine separation: bans legacy reactflow package and restricts @xyflow/react imports to packages/extension/src/webview/blast-radius/** (new area added). Non-boundary violation messages keyed to RULE-ARCH-002.
  • @xyflow/react bundled as part of single webview IIFE; React.lazy defers component construction/rendering work (not byte reduction).
  • Core models are framework-agnostic (FocusedGraphModel, computeFocusedLayout, focusedEdgeStyle); React Flow binding confined to blast-radius/ with SymbolNode custom node type.

Risks & Known Limitations

  • Live layout simulation does not re-energize when nodes are reselected (by design; only runs on mount and after explicit preset reapplication).
  • Focused view caps neighbourhood at 60 nodes by importance ranking; truncation is reported but UI can become crowded near the cap.
  • Worker availability is required for live layout; graceful fallback uses seed layout only (no async fallback simulation).
  • Large graph run time is clamped (LARGE_GRAPH_NODES cap) to avoid unbounded layout settle delays.

Schema & Contract Changes

  • SelectionTraversal interface gains nodeLayers: string[][] (BFS-grouped node IDs by depth for ring placement); existing hopLayers (edge grouping) semantics unchanged.
  • FitCameraTarget interface replaced animate() with animatedReset() and added optional refresh() method; NodeBoundsGraph narrowed to require only order (for empty-graph check).
  • SigmaMountCallbacks gains onFocus: (nodeId: string) => void callback; SigmaController adds public restartLiveLayout() method.

Deferred Validation

  • No re-energization of live layout on radial reposition; interaction model for force-directed graph evolution under dynamic selection not tested at scale.
  • Bundle size and React Flow tree-shaking behavior in single IIFE not measured; confirmation that dead code elimination is effective pending.

Walkthrough

This 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.


Changes

Focused Neighborhood Graph View with Radial Selection Layout

Layer / File(s) Summary
Architecture Boundary & Dependencies
eslint.config.js, packages/extension/package.json
ESLint enforcement of @xyflow/react scoping to blast-radius subgraph only; adds @xyflow/react@^12.11.0 dependency.
Focused Graph Data Model & Layout
packages/extension/src/webview/blast-radius/focusedGraphModel.ts, focusedLayout.ts, edgeStyles.ts, focusedGraph.test.ts
Framework-agnostic focused-view model with node ring depths, edge directions, importance-based truncation cap (60 nodes default), and deterministic concentric-ring layout algorithm; comprehensive test coverage for model building, layout positioning, and edge styling.
SymbolNode React Component
packages/extension/src/webview/blast-radius/SymbolNode.module.css, SymbolNode.tsx, SymbolNode.test.tsx
React Flow node component rendering symbols with Codicon icons, labels, kind badges, and focus/core styling variants; styled card layout with connection handles.
FocusedGraphView React Component
packages/extension/src/webview/blast-radius/FocusedGraphView.module.css, FocusedGraphView.tsx, LazyFocusedGraphView.tsx
React Flow canvas component building focused neighborhood from model/layout, transforming to RF nodes/edges with styled arrowheads, and rendering with fixed interaction settings; lazy-loaded via Suspense to defer React Flow construction.
Selection Traversal & Radial Data
packages/extension/src/webview/components/graphTraversal.ts, graphViewTypes.ts, graphTraversal.test.ts
Extends computeSelection to derive nodeLayers (per-depth node groupings) enabling ring-based placement; validates layer structure in tests.
Radial Selection Layout
packages/extension/src/webview/components/graphLayoutPresets.ts, graphLayoutPresets.test.ts
New assignRadialPositions function repositioning selected node at origin with neighbors on concentric rings derived from BFS layers; includes snapshot/restore helpers and comprehensive layout distribution tests.
Camera Fit & Layout Refactoring
packages/extension/src/webview/components/cameraFit.ts, fitCameraToNodes.test.tsx
Delegates camera framing to Sigma's native animatedReset() instead of manual bounds computation; removes per-node iteration and simplifies interface contracts (FitCameraTarget, NodeBoundsGraph).
Live Layout Supervisor
packages/extension/src/webview/components/liveLayout.ts, liveLayout.test.tsx
Manages bounded-duration force-layout supervisor with auto-stop timer scaled by graph size; supports idempotent stop/dispose with injectable timers for testing.
SigmaController Integration
packages/extension/src/webview/components/SigmaController.ts, SigmaController.test.tsx
Adds live ForceAtlas2 startup/restart, Alt/Cmd+double-click gesture triggering onFocus(nodeId) callback, consolidated FORCE_ATLAS2_SETTINGS, and live layout cleanup in dispose(); updates camera interface and test fixtures.
GraphView Integration & Focused Overlay
packages/extension/src/webview/components/GraphView.tsx, GraphView.module.css
Integrates focused-view state, radial repositioning on selection (snapshot/restore), focused overlay rendering, and controller callback wiring; adds CSS overlay positioning and close button.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


Possibly related issues

  • dgtalbug/dextree#168: Radial on-select neighborhood layout feature directly addresses this issue's core objective.

Possibly related PRs

  • dgtalbug/dextree#154: Both modify SigmaController/GraphView callback wiring and camera integration; main PR builds on the refactoring foundation.
  • dgtalbug/dextree#141: Both modify GraphView stack (cameraFit.ts, theme contracts, layout presets); main PR extends layout capabilities.
  • dgtalbug/dextree#155: Both wire focus callbacks into GraphView/SigmaController for node-focused UI state management.

Suggested labels

area:extension, type:feat


Poem

A node glows at the center stage,
Rings expand outward, age by age,
Alt-click to focus, drift away,
React Flow dances, radial display. 🎯


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

dgtalbug added 4 commits June 7, 2026 20:33
…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.
dgtalbug added 4 commits June 7, 2026 22:25
… (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
dgtalbug added 2 commits June 7, 2026 23:34
…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.
@dgtalbug dgtalbug changed the title feat(extension): radial neighbourhood layout on node select feat(extension): graph UX overhaul — live layout, focused card view, radial-on-select, layout fixes Jun 7, 2026
@dgtalbug
dgtalbug marked this pull request as ready for review June 7, 2026 18:08
@dgtalbug
dgtalbug merged commit 56dd4ed into main Jun 7, 2026
12 checks passed
@dgtalbug
dgtalbug deleted the feature/radial-on-select branch June 7, 2026 18:09
@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Jun 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant