Skip to content

Impl agent memory graph second-brain UI - #294

Merged
mbakgun merged 24 commits into
mainfrom
update/memory-ui
Jul 7, 2026
Merged

Impl agent memory graph second-brain UI#294
mbakgun merged 24 commits into
mainfrom
update/memory-ui

Conversation

@mbakgun

@mbakgun mbakgun commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Searchable Source/Target relationship pickers (SearchableSelect) plus
an animated force-directed "second brain" graph view for the agent
memory dialog, replacing the top-down tree layout.

mbakgun added 24 commits July 7, 2026 17:08
Searchable Source/Target relationship pickers (SearchableSelect) plus
an animated force-directed "second brain" graph view for the agent
memory dialog, replacing the top-down tree layout.
10 tasks: searchable Source/Target pickers, pure-TS layout helpers,
a requestAnimationFrame force simulation hosted inside the existing
Vue Flow context, circle-rim edge routing, hover-dim, cluster legend,
a connections panel, and doc updates. Includes a position-preservation
fix (capture live positions before every graph reload) needed because
Vue Flow's node reconciliation overwrites position on every prop
change.
Replace the native Source/Target <select> elements in the "New
relationship" form with the SearchableSelect combobox already used
for node operations, so large entity lists are searchable.
Degree count, radius-by-degree, stable blue-family cluster color by
entity type, and deterministic cluster-anchored seed positions. Not
wired up yet.
Renderless AgentMemoryGraphForceSim runs a link-spring + repulsion +
centering + cluster-gravity + collision simulation via
requestAnimationFrame, ticking node.position through Vue Flow's own
updateNode (skips nodes currently being dragged). FlowPane hosts it
and forwards reheat()/snapshotPositions()/focusNode(id); dragging a
node reheats the sim so neighbors resettle. Not wired to the graph
data yet.
Nodes become colored circles (radius by connection count, color by
entity-type cluster) with captions below, driven by the force
simulation instead of the old top-down tree layout. Node positions
are captured before every reload so drag/settle state survives graph
refreshes. Edges route circle-rim to circle-rim with a fainter,
blue-tinted stroke. Hovering a node dims unrelated nodes and edges;
a cluster legend lists entity-type colors.
Selecting a node now shows its outgoing and incoming relationships in
the detail panel, each clickable to select+center that node. "Tidy
layout" and the label-visibility toggle no longer remount Vue Flow
(flowLayoutEpoch removed) — tidy now just reheats the force
simulation so the existing arrangement resettles instead of jumping
back to a fresh seed layout.
Root cause of the reported clutter: relationship labels have no
inter-label collision avoidance, so with several edges near each
other their curvature-offset label positions can coincidentally land
almost exactly on top of one another (verified: two labels measured
~1.5px apart in a 4-node repro) — confirmed independent of overall
node spacing (widening the force-sim distances made it worse, not
better, so that was reverted). Extending the existing hover-dim
mechanism to also key off the selected node means clicking a node now
fades everything not directly connected to it, which resolves the
practical case (the one the report screenshotted) and is also the
requested "fade out others on click" behavior.
…chain highlight

Per follow-up feedback on the second-brain view:
- Hover is now fully inert (removed the hover tooltip and hover-driven
  dim/select machinery); clicking a node already opens the full detail
  panel, so hover added a second, redundant interaction mode.
- Selecting a node now animates its directly-connected edges with a
  flowing stroke-dashoffset highlight in the primary accent color,
  instead of a static highlight — relationship labels are hidden by
  default and only rendered for that active chain, which also removes
  the labels that were competing for the same screen space.
- Circles are smaller (radius 11-22px, was 16-34px) and edge curvature
  is much flatter (0.02-0.15, was 0.12-0.4) so lines read as straight
  rather than bowed/spiral, and increased simulation damping (0.7,
  was 0.82) so nodes settle without oscillating.
Per follow-up feedback:
- Replace the looping "marching ants" chain highlight with a one-shot
  SVG line-draw animation: selecting a node makes its edges' purple
  overlay grow from the selected node outward (reversed for edges
  where the selection is the target), and drain back the same way on
  deselect. Root-caused an apparent "never stops" artifact during
  investigation to Playwright's getComputedStyle polling racing with
  Vue's render flush — verified via direct internal-state tracing that
  the actual animation is a clean one-shot 0->1 over ~450ms with no
  restart loop.
- Default (non-active) edges are now solid, thin, and subtle instead
  of dashed blue, matching the requested network-hairline look;
  removed vue-flow's `animated: true` flag, which was injecting its
  own default dashed marching-ants CSS that fought this styling.
…ox center

Root cause (found via precise DOM measurement, not guessing): Vue
Flow's computedPosition is the node box's top-left corner. The old
node template stacked the circle and its caption in a flex column, so
whenever a caption was wider than the circle (frequently, since
captions can be up to 110px and circles are only 22-44px), the node's
own box grew wider/taller than the circle to fit it — meaning
computedPosition-as-circle-center was off by up to half the caption's
width, consistently reproducing the reported "lines floating,
disconnected from the circles" look. Verified this was not a
timing/staleness issue: the gap persisted unchanged after 20 full
seconds of guaranteed quiescence, and did not correlate with node
position freshness, but disappeared entirely (down to floating-point
noise) once the node's own box was fixed to exactly the circle's
diameter.

Fix: flowNodes now sets each node's width/height to its circle's
diameter, so Vue Flow's box exactly matches the circle; the caption
is now an absolutely-positioned overlay below the box instead of a
flex sibling that grows it. Edges compute the circle's true center as
computedPosition + radius on both axes. Also removed a leftover
`width: auto !important` CSS override (needed for the old
content-sized layout) that would otherwise have collapsed the node's
new explicit width.

Also, per follow-up feedback:
- Selecting a node now toggles: clicking the same node again
  deselects it and plays the drain animation in reverse.
- Relationship labels only fade in once the fill animation reaches
  the target node, instead of appearing immediately on selection.
- Compact (labelsHidden) mode now also shrinks the circles themselves
  (0.55x scale, floor 4px), not just hiding captions, so pins read as
  small markers instead of full-size circles with nothing to fill
  the extra space. flowNodes now depends on labelsHidden, so
  toggleLabels() captures live positions first (same fix as
  loadGraph()) to avoid snapping nodes back to a stale layout.
- Hovering a node in compact mode shows a name/type/attributes
  popover, since there's no caption to identify a plain dot by;
  hover stays fully inert in normal mode, unchanged.
- Clicking an edge that's part of the selected node's active chain
  now deselects (playing the drain animation in reverse), mirroring
  clicking the selected node again — wired via a new edge-click event
  forwarded through AgentMemoryGraphFlowPane.
…er captions

Root cause of "deselect just instantly drops instead of animating
reversed": confirmed via mount/unmount instrumentation that Vue Flow
recreates the edge component on every selection change (its slot-based
custom-edge render path has early `return null` branches). The
previous fix attempted to persist reveal progress in a Map declared at
the top of <script setup> — but <script setup> content runs inside
setup(), so that Map was itself being recreated fresh on every
remount, exactly like the ref it was meant to protect. It's now
declared in a genuine module-scope sibling <script> block, so a
deselect correctly finds start=1 and animates down to 0 instead of
finding a fresh always-zero start with nothing to animate.

Also:
- Dimmed nodes/edges fade much further (0.25->0.1, 0.15->0.06) for a
  starker selected-vs-rest contrast.
- Node captions are bigger and bolder (10px medium -> 12px semibold)
  with a text-shadow for contrast against the dotted background, and
  the active-chain edge label similarly bumped, so the graph reads
  without zooming in.
… selection

- Deselecting a node now keeps everyone else faded for the same
  REVEAL_DURATION_MS the drain animation takes (via a new dimmingNodeId
  that lags behind selectedNodeId only on deselect), so other entities'
  color restores once the animation actually finishes instead of
  snapping back instantly underneath it. active/growFromEnd still track
  the real selection immediately so the animation itself isn't delayed.
  REVEAL_DURATION_MS is now exported from AgentMemoryGraphEdge.vue as
  the single source of truth for both the animation and this delay.
- Removed the top-right entity-type color legend chip entirely.
- Default fit-view padding on open tightened (0.18 -> 0.08) so the
  graph reads bigger by default while everything still fits.
- Selecting a node (canvas click or a Connections-panel row) now also
  fits+centers the camera on that node plus its direct neighbors
  (focusOnNode generalized to focusOnNodes(ids)), instead of leaving
  the camera wherever it was.
… to canvas, isolate hovered relation

Selecting a memory-graph node now widens its incident links' rest length in the
force simulation so crowded hub neighbors spread apart instead of relying on a
cosmetic label overlay. Node captions flip above the circle when the active
relationship label would otherwise land below it. All nodes are clamped to a
10% safe area inset from the canvas edges so the wider spacing can't push them
under the toolbar buttons. Hovering one active edge now hides sibling active
edges/labels so overlapping relationship labels near a hub can be read one at
a time.
…de labels

Hovering an edge only sets hoveredEdgeId when that edge actually belongs to
the current selection's chain, so hovering an unrelated background edge (or
hovering anything with nothing selected) no longer hides the selected hub's
labels. Hover also now hides only the relationship-label chip, not the
underlying hairline or purple reveal line, which stay visible.
Captions are absolutely positioned outside their node's own box (above/below
the circle) so they can reach into the space where connecting edges and
labels sit, and Vue Flow's node layer paints above the edges' SVG layer. With
no pointer-events-none, a caption sitting on top of a nearby edge silently ate
the hover, so the edge's mouseenter (and the hide-siblings behavior) never
fired — reproduced by sampling real points along each edge's curve and
checking elementFromPoint, which showed the caption intercepting the hit
right at the neighbor end of several edges.
Hovering a node's circle now shows the name/type/attributes popover
regardless of compact mode, giving a quick attribute peek without opening the
edit panel. Suppressed while any node is selected so it doesn't compete with
the selected chain's own highlighting, relation labels, and hover-isolate
behavior; the popover is also cleared immediately when a selection starts, so
one already open from the node just clicked can't linger.
…g leaf captions

Node hover now only shows the attributes popover in two cases: any node in
compact (pin) mode, since captions are hidden there entirely, or a selected
hub's leaf (neighbor) node in normal mode. Hovering the hub itself, an
unrelated node, or anything with no selection stays inert. Hovering a leaf
also hides the other leaves' name captions (not the hub's own), mirroring the
edge hover-isolate treatment of relationship labels, so a crowded hub's names
don't compete with the one being inspected.

Root-caused a bug in this pass: selectedNeighborIds intentionally includes
the hub itself (for the existing dim/fade behavior), so reusing it directly
for the leaf check made hovering the hub incorrectly trigger the popover and
hid the hub's own caption. Added selectedLeafIds (hub excluded) for this
feature instead.
…tions

Hovering a selected hub's leaf now reuses the same hoveredEdgeId mechanism as
hovering that leaf's edge directly (via the new edgeIdBetween lookup), so it
hides sibling relationship-label chips while leaving every node's own name
caption alone. The previous pass hid the other leaves' captions instead,
which wasn't the intended target — the crowding this addresses is in the
relation labels, not the node names.
Vue Flow's own DOM order paints .vue-flow__nodes (node captions included)
above .vue-flow__edge-labels, and neither layer sets a z-index — so a dimmed
background node's caption that happened to land on top of an active
relationship label would bleed through it instead of sitting behind it (e.g.
selecting Ada, whose "going_to" label landed on Volvo's caption). Both layers
are pointer-events:none, so raising the labels layer's z-index above nodes is
purely visual and doesn't affect clicking/dragging nodes underneath.
…show

A selected hub's leaf only pops up its attribute popover on hover when it has
2+ attributes — with 0 or 1, the popover would show little beyond what the
caption already says, so it's not worth it. The relationship-label isolation
still applies regardless of attribute count, since that's an unrelated
benefit. Compact mode is untouched: it always shows the popover for any node
since that's the only way to identify a pin there at all.
@mbakgun
mbakgun enabled auto-merge (squash) July 7, 2026 21:11
@mbakgun mbakgun changed the title docs: add design spec for agent memory graph second-brain UI Impl agent memory graph second-brain UI Jul 7, 2026
@mbakgun
mbakgun merged commit 33f7b14 into main Jul 7, 2026
2 checks passed
@mbakgun
mbakgun deleted the update/memory-ui branch July 7, 2026 21:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant