@sleep2agi/agent-network-dashboard 0.4.6-preview.121 → 0.4.6-preview.123

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/diagnostics/route-bundle-stats.json +4 -4
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/prerender-manifest.json +3 -3
  6. package/.next/server/app/_global-error.html +1 -1
  7. package/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_not-found.html +2 -2
  15. package/.next/server/app/_not-found.rsc +2 -2
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  17. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  20. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  21. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/admin.html +2 -2
  24. package/.next/server/app/admin.rsc +2 -2
  25. package/.next/server/app/admin.segments/_full.segment.rsc +2 -2
  26. package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
  27. package/.next/server/app/admin.segments/_index.segment.rsc +2 -2
  28. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  29. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
  30. package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
  31. package/.next/server/app/index.html +2 -2
  32. package/.next/server/app/index.rsc +3 -3
  33. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  34. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  35. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  36. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  37. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  38. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  39. package/.next/server/app/login.html +2 -2
  40. package/.next/server/app/login.rsc +3 -3
  41. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  42. package/.next/server/app/login.segments/_head.segment.rsc +1 -1
  43. package/.next/server/app/login.segments/_index.segment.rsc +2 -2
  44. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  45. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  46. package/.next/server/app/login.segments/login.segment.rsc +1 -1
  47. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  48. package/.next/server/app/logs.html +2 -2
  49. package/.next/server/app/logs.rsc +2 -2
  50. package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
  51. package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
  52. package/.next/server/app/logs.segments/_index.segment.rsc +2 -2
  53. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  54. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
  55. package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
  56. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  57. package/.next/server/app/messages.html +2 -2
  58. package/.next/server/app/messages.rsc +2 -2
  59. package/.next/server/app/messages.segments/_full.segment.rsc +2 -2
  60. package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
  61. package/.next/server/app/messages.segments/_index.segment.rsc +2 -2
  62. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  63. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
  64. package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
  65. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  66. package/.next/server/app/node.html +2 -2
  67. package/.next/server/app/node.rsc +2 -2
  68. package/.next/server/app/node.segments/_full.segment.rsc +2 -2
  69. package/.next/server/app/node.segments/_head.segment.rsc +1 -1
  70. package/.next/server/app/node.segments/_index.segment.rsc +2 -2
  71. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  72. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +1 -1
  73. package/.next/server/app/node.segments/node.segment.rsc +1 -1
  74. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app/nodes.html +2 -2
  76. package/.next/server/app/nodes.rsc +2 -2
  77. package/.next/server/app/nodes.segments/_full.segment.rsc +2 -2
  78. package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
  79. package/.next/server/app/nodes.segments/_index.segment.rsc +2 -2
  80. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  81. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +1 -1
  82. package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
  83. package/.next/server/app/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/server-logs.html +2 -2
  86. package/.next/server/app/server-logs.rsc +2 -2
  87. package/.next/server/app/server-logs.segments/_full.segment.rsc +2 -2
  88. package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
  89. package/.next/server/app/server-logs.segments/_index.segment.rsc +2 -2
  90. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  91. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +1 -1
  92. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
  93. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  94. package/.next/server/app/settings/networks.html +2 -2
  95. package/.next/server/app/settings/networks.rsc +2 -2
  96. package/.next/server/app/settings/networks.segments/_full.segment.rsc +2 -2
  97. package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
  98. package/.next/server/app/settings/networks.segments/_index.segment.rsc +2 -2
  99. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  100. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
  101. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
  102. package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
  103. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  104. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  105. package/.next/server/app/settings/tokens.html +2 -2
  106. package/.next/server/app/settings/tokens.rsc +2 -2
  107. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +2 -2
  108. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
  109. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +2 -2
  110. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  111. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
  112. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
  113. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
  114. package/.next/server/app/settings.html +2 -2
  115. package/.next/server/app/settings.rsc +3 -3
  116. package/.next/server/app/settings.segments/_full.segment.rsc +3 -3
  117. package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  118. package/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  119. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  120. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
  121. package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  122. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  123. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app/tasks.html +2 -2
  125. package/.next/server/app/tasks.rsc +2 -2
  126. package/.next/server/app/tasks.segments/_full.segment.rsc +2 -2
  127. package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
  128. package/.next/server/app/tasks.segments/_index.segment.rsc +2 -2
  129. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  130. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
  131. package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
  132. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -3
  133. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  134. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  135. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  136. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  137. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  138. package/.next/server/middleware-build-manifest.js +3 -3
  139. package/.next/server/pages/404.html +2 -2
  140. package/.next/server/pages/500.html +1 -1
  141. package/.next/server/server-reference-manifest.js +1 -1
  142. package/.next/server/server-reference-manifest.json +1 -1
  143. package/.next/static/chunks/056qnpaojmgob.js +4 -0
  144. package/.next/static/chunks/{0a-6zp8m0ux5..js → 13q82hspm0t83.js} +1 -1
  145. package/.next/static/chunks/1450fa_~v9qpc.css +2 -0
  146. package/.next/static/chunks/{0ekib0be3t9i5.js → 155clh0u_d7o~.js} +1 -1
  147. package/.next/trace +2 -2
  148. package/.next/trace-build +1 -1
  149. package/app/components/TopoGraph.tsx +78 -17
  150. package/package.json +1 -1
  151. package/scripts/topo-hub-hover-ring-test.mjs +82 -0
  152. package/scripts/topo-recent-row-pin-test.mjs +126 -0
  153. package/.next/static/chunks/001t1axy5kvz8.css +0 -2
  154. package/.next/static/chunks/106~a5kttxze-.js +0 -4
  155. /package/.next/static/{fh5pUqgTFNt162QarYePi → 1T3W_wqzriDiRMGvMe9qe}/_buildManifest.js +0 -0
  156. /package/.next/static/{fh5pUqgTFNt162QarYePi → 1T3W_wqzriDiRMGvMe9qe}/_clientMiddlewareManifest.js +0 -0
  157. /package/.next/static/{fh5pUqgTFNt162QarYePi → 1T3W_wqzriDiRMGvMe9qe}/_ssgManifest.js +0 -0
@@ -821,12 +821,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
821
821
  // visible without reading the tooltip. The set is the link's two aliases
822
822
  // (null when no edge hovered); node opacity composes this after inFocus.
823
823
  const [hoveredEdgeKey, setHoveredEdgeKey] = useState<string | null>(null);
824
+ // Round 116 / Loop: sticky variant of hoveredEdgeKey. R56 made the
825
+ // recent-signal rows brighten the matching edge on hover, but the
826
+ // filter released on mouseleave — no way to lock a flow for a
827
+ // closer look. Click-to-pin closes that gap, matching the
828
+ // established hover→pin idiom (R60 status, R61 legend, R63 group,
829
+ // R88 vendor). activeEdgeKey = hoveredEdgeKey ?? pinnedEdgeKey
830
+ // below so hover still wins for spot-comparison while a pin is set.
831
+ const [pinnedEdgeKey, setPinnedEdgeKey] = useState<string | null>(null);
832
+ const activeEdgeKey = hoveredEdgeKey ?? pinnedEdgeKey;
824
833
  // Round 77 / Loop: hovering the "N active links" header chip globally
825
834
  // brightens every flow edge (1.5× opacity). Transient affordance — the
826
835
  // chip already says HOW MANY active flows exist; this answers WHERE
827
836
  // they are in one glance, without the user scanning the canvas for
828
837
  // moving particles. Reset on mouse leave.
829
838
  const [hoveredActiveLinks, setHoveredActiveLinks] = useState(false);
839
+ // Round 115 / Loop: hub-center hover state. R52 made the hub
840
+ // clickable (fit view) + cursor:pointer, but there was no
841
+ // hover-time visual feedback — the click target felt guessed at
842
+ // rather than confirmed. This state drives a subtle hint ring on
843
+ // hover so users see the affordance before committing the click.
844
+ const [hoveredHub, setHoveredHub] = useState(false);
830
845
  // Round 80 / Loop: vendor-letter hover in the distribution chip. The
831
846
  // chip already names vendor mix (`C:5 G:3 ?:1`); hover a letter and
832
847
  // every node from OTHER vendors dims. Surfaces the breakdown spatially
@@ -940,10 +955,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
940
955
  // R74 listener for layout + view palette commands lives below
941
956
  // fitView's declaration — see further down in the file.
942
957
  const hoveredEdgeEndpoints = useMemo<Set<string> | null>(() => {
943
- if (!hoveredEdgeKey) return null;
944
- const link = flowLinks.find(l => l.key === hoveredEdgeKey);
958
+ // R116: compose hover ?? pin so pinning a row via click keeps the
959
+ // endpoint ring + edge ladder lit after mouseleave.
960
+ if (!activeEdgeKey) return null;
961
+ const link = flowLinks.find(l => l.key === activeEdgeKey);
945
962
  return link ? new Set([link.from, link.to]) : null;
946
- }, [hoveredEdgeKey, flowLinks]);
963
+ }, [activeEdgeKey, flowLinks]);
947
964
 
948
965
  // --- Round 103 (issue #81): fullscreen + zoom + pan interaction layer ---
949
966
  // DIY native (no d3 / svg-pan-zoom): wrap the topology content in a single
@@ -1204,13 +1221,14 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1204
1221
  // R63: extends to pinnedGroup too. Clears WHATEVER pin is active
1205
1222
  // (one Esc collapses all topology pins) so the keyboard escape
1206
1223
  // route stays a single key, not "Esc maybe Esc again".
1207
- else if (e.key === 'Escape' && !chatAlias && (pinnedStatus || pinnedGroup || pinnedVendor)) {
1208
- // R88: extend the universal-cancel to pinnedVendor too. One Esc
1209
- // collapses every topology pin (matches R62/R63's "single key,
1210
- // not Esc-maybe-Esc-again" promise).
1211
- if (pinnedStatus) setPinnedStatus(null);
1212
- if (pinnedGroup) setPinnedGroup(null);
1213
- if (pinnedVendor) setPinnedVendor(null);
1224
+ else if (e.key === 'Escape' && !chatAlias && (pinnedStatus || pinnedGroup || pinnedVendor || pinnedEdgeKey)) {
1225
+ // R88/R116: extend the universal-cancel to vendor + edge too.
1226
+ // One Esc collapses every topology pin (matches R62/R63's
1227
+ // "single key, not Esc-maybe-Esc-again" promise).
1228
+ if (pinnedStatus) setPinnedStatus(null);
1229
+ if (pinnedGroup) setPinnedGroup(null);
1230
+ if (pinnedVendor) setPinnedVendor(null);
1231
+ if (pinnedEdgeKey) setPinnedEdgeKey(null);
1214
1232
  e.preventDefault();
1215
1233
  }
1216
1234
  };
@@ -1221,7 +1239,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1221
1239
  // and pinnedStatus so the Escape branch reads fresh state (re-binding
1222
1240
  // the listener on these state changes is sub-ms — cheaper than refs).
1223
1241
  // eslint-disable-next-line react-hooks/exhaustive-deps
1224
- }, [fitView, chatAlias, pinnedStatus, pinnedGroup, pinnedVendor]);
1242
+ }, [fitView, chatAlias, pinnedStatus, pinnedGroup, pinnedVendor, pinnedEdgeKey]);
1225
1243
 
1226
1244
  const toggleFullscreen = () => {
1227
1245
  const el = containerRef.current;
@@ -2229,7 +2247,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2229
2247
  // background to the focus. Singletons fall through unchanged
2230
2248
  // (their group key is the alias itself, so bothInHoveredGroup
2231
2249
  // is impossible for a non-self-edge).
2232
- const isHoveredEdge = hoveredEdgeKey === link.key;
2250
+ // R116: composes hover ?? pin — a pinned edge stays "hot" after the cursor leaves.
2251
+ const isHoveredEdge = activeEdgeKey === link.key;
2233
2252
  const fromGroup = groupKeys[link.from] ?? link.from;
2234
2253
  const toGroup = groupKeys[link.to] ?? link.to;
2235
2254
  const bothInHoveredGroup = !!activeGroup && fromGroup === activeGroup && toGroup === activeGroup;
@@ -2242,7 +2261,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2242
2261
  // the edge-hover (2.0×) or node-hover (1.7×) priorities.
2243
2262
  const edgeOpacityMul = isHoveredEdge
2244
2263
  ? 2.0
2245
- : hoveredEdgeKey
2264
+ : activeEdgeKey
2246
2265
  ? 0.35
2247
2266
  : !hoveredAlias && !activeGroup
2248
2267
  ? (hoveredActiveLinks ? 1.5 : 1)
@@ -2432,11 +2451,14 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2432
2451
  dual-circle "lit lamp" core is unchanged. */}
2433
2452
  {layout === 'ring' && (<g
2434
2453
  data-topo-hub
2454
+ data-topo-hub-hovered={hoveredHub ? 'true' : 'false'}
2435
2455
  style={{ cursor: 'pointer' }}
2436
2456
  // Stop pointerdown from reaching the SVG pan handler — same
2437
2457
  // reason as the node <g>: a captured pointer makes Chromium
2438
2458
  // fire the follow-up click on the SVG, not this group.
2439
2459
  onPointerDown={(e) => e.stopPropagation()}
2460
+ onMouseEnter={() => setHoveredHub(true)}
2461
+ onMouseLeave={() => setHoveredHub(false)}
2440
2462
  // Round 52 / Loop: hub is the visual anchor but had no click
2441
2463
  // action — users discovered fit-to-content only via the `f`
2442
2464
  // key or the chrome button. Wire the hub to fitView() so the
@@ -2509,6 +2531,23 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2509
2531
  {/* core — 20px diameter, larger inner highlight reads as a "lit lamp" */}
2510
2532
  <circle cx={cx} cy={cy} r="10" fill={isLight ? '#059669' : '#10b981'} />
2511
2533
  <circle cx={cx} cy={cy} r="5" fill="#d1fae5" opacity="0.9" />
2534
+ {/* R115 / Loop: hover hint ring. Stroke-only circle at r=14
2535
+ that fades in when the hub is hovered — the same idea
2536
+ R44 used for node avatars (group-hover stroke). r=14
2537
+ sits comfortably outside the r=10 core and INSIDE the
2538
+ r=18 grounding halo, so the hover indicator is fully
2539
+ contained within the existing hub footprint (no bbox
2540
+ growth, overlap test unchanged). pointerEvents:none so
2541
+ the hint can't intercept the click that produced it. */}
2542
+ <circle
2543
+ cx={cx} cy={cy} r="14"
2544
+ fill="none"
2545
+ stroke={isLight ? '#059669' : '#10b981'}
2546
+ strokeWidth="1.5"
2547
+ opacity={hoveredHub ? (isLight ? 0.85 : 0.7) : 0}
2548
+ data-topo-hub-hover-ring
2549
+ style={{ pointerEvents: 'none', transition: 'opacity 180ms ease-out' }}
2550
+ />
2512
2551
  </g>)}
2513
2552
 
2514
2553
  {/* agent nodes */}
@@ -3102,14 +3141,32 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3102
3141
  const rawAt = link.last_at ? relativeAgo(link.last_at) : null;
3103
3142
  const lastAt = rawAt ? rawAt.replace(/\s+ago$/, '') : null;
3104
3143
  const isRowHovered = hoveredEdgeKey === link.key;
3144
+ const isRowPinned = pinnedEdgeKey === link.key;
3145
+ const isRowActive = isRowHovered || isRowPinned;
3105
3146
  return (
3106
3147
  <g
3107
3148
  key={link.key}
3108
3149
  data-recent-row={link.key}
3109
3150
  data-recent-row-hovered={isRowHovered ? 'true' : 'false'}
3151
+ data-recent-row-pinned={isRowPinned ? 'true' : 'false'}
3152
+ role="button"
3153
+ tabIndex={0}
3154
+ aria-pressed={isRowPinned}
3110
3155
  style={{ cursor: 'pointer' }}
3156
+ onPointerDown={(e) => e.stopPropagation()}
3111
3157
  onMouseEnter={() => setHoveredEdgeKey(link.key)}
3112
3158
  onMouseLeave={() => setHoveredEdgeKey(prev => prev === link.key ? null : prev)}
3159
+ // R116: click toggles pin. activeEdgeKey =
3160
+ // hoveredEdgeKey ?? pinnedEdgeKey so the matching
3161
+ // edge stays "hot" after mouseleave; click again
3162
+ // (or Esc) releases.
3163
+ onClick={() => setPinnedEdgeKey(prev => prev === link.key ? null : link.key)}
3164
+ onKeyDown={(e) => {
3165
+ if (e.key === 'Enter' || e.key === ' ') {
3166
+ e.preventDefault();
3167
+ setPinnedEdgeKey(prev => prev === link.key ? null : link.key);
3168
+ }
3169
+ }}
3113
3170
  >
3114
3171
  {/* R104: subtle row-background tint on hover. R56
3115
3172
  already brightens the matching edge on the canvas,
@@ -3118,17 +3175,21 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3118
3175
  Filling the rect at hover with `pal.legendAccent`
3119
3176
  at low alpha gives the row visual feedback at the
3120
3177
  source surface, mirroring the list-item idiom from
3121
- the chip-row pills. */}
3178
+ the chip-row pills. R116: pinned rows tint
3179
+ stronger than hovered ones so locked vs preview
3180
+ is discriminable. */}
3122
3181
  <rect
3123
3182
  x="6" y={38 + index * 16 - 10}
3124
3183
  width="218" height="14" rx="3"
3125
- fill={isRowHovered ? pal.legendAccent : 'transparent'}
3126
- opacity={isRowHovered ? (isLight ? 0.10 : 0.14) : 1}
3184
+ fill={isRowActive ? pal.legendAccent : 'transparent'}
3185
+ opacity={isRowPinned ? (isLight ? 0.18 : 0.22)
3186
+ : isRowHovered ? (isLight ? 0.10 : 0.14)
3187
+ : 1}
3127
3188
  style={{ transition: 'fill 150ms ease-out, opacity 150ms ease-out' }}
3128
3189
  />
3129
3190
  <text
3130
3191
  x="13" y={38 + index * 16}
3131
- fill={hoveredEdgeKey === link.key ? pal.legendHeadline : pal.legendText}
3192
+ fill={isRowActive ? pal.legendHeadline : pal.legendText}
3132
3193
  fontSize="9"
3133
3194
  fontFamily="monospace"
3134
3195
  style={{ transition: 'fill 150ms ease-out' }}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.4.6-preview.121",
3
+ "version": "0.4.6-preview.123",
4
4
  "description": "Agent Network Dashboard — Web UI for managing AI Agent networks",
5
5
  "scripts": {
6
6
  "dev": "next dev",
@@ -0,0 +1,82 @@
1
+ /* Round 115 verification: hub center hover-state visual feedback.
2
+ * R52 made the hub clickable (fit view) + cursor:pointer, but
3
+ * hovering showed nothing. Now a thin r=14 stroke ring fades in on
4
+ * hover and out on leave. Sits inside the existing r=18 halo so
5
+ * overlap footprint unchanged.
6
+ *
7
+ * Probes:
8
+ * - baseline: ring opacity 0, data-topo-hub-hovered="false"
9
+ * - hover hub: ring opacity > 0, hovered="true"
10
+ * - mouseleave: opacity 0, hovered="false"
11
+ */
12
+ import { chromium } from 'playwright';
13
+ import { readFileSync } from 'node:fs';
14
+
15
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
16
+ const browser = await chromium.launch({ headless: true });
17
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1500 } });
18
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
19
+ await ctx.addInitScript(() => {
20
+ try {
21
+ localStorage.setItem('anet-theme', 'cyber');
22
+ localStorage.setItem('anet-topo-layout', 'ring');
23
+ sessionStorage.setItem('anet_v3_auth', '1');
24
+ } catch {}
25
+ });
26
+
27
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
28
+ await ctx.route('**/api/hub/status*', async (route) => {
29
+ const r = await route.fetch();
30
+ const b = await r.json();
31
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
32
+ const mk = (alias) => ({
33
+ alias, status: 'idle', model: 'claude-opus-4', runtime: 'cli-claude-code',
34
+ network_id: nid, project_dir: null,
35
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
36
+ });
37
+ await route.fulfill({ response: r, json: { ...b, sessions: [mk('alpha'), mk('beta'), mk('gamma')] } });
38
+ });
39
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [] } }));
40
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
41
+ const page = await ctx.newPage();
42
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
43
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 3, { timeout: 30000 });
44
+ await page.waitForSelector('[data-topo-hub]', { timeout: 10000 });
45
+ await page.waitForTimeout(400);
46
+
47
+ const read = () => page.evaluate(() => {
48
+ const hub = document.querySelector('[data-topo-hub]');
49
+ const ring = document.querySelector('[data-topo-hub-hover-ring]');
50
+ return {
51
+ hovered: hub?.getAttribute('data-topo-hub-hovered') || '',
52
+ ringOpacity: +(ring?.getAttribute('opacity') || '0'),
53
+ };
54
+ });
55
+
56
+ const before = await read();
57
+
58
+ await page.locator('[data-topo-hub]').hover();
59
+ await page.waitForTimeout(300);
60
+ const onHover = await read();
61
+
62
+ // Move pointer far away.
63
+ await page.mouse.move(10, 10);
64
+ await page.waitForTimeout(300);
65
+ const afterLeave = await read();
66
+
67
+ await browser.close();
68
+
69
+ const results = {
70
+ before_notHovered: before.hovered === 'false',
71
+ before_ringHidden: before.ringOpacity === 0,
72
+ hover_attrTrue: onHover.hovered === 'true',
73
+ hover_ringVisible: onHover.ringOpacity > 0.5,
74
+ leave_attrFalse: afterLeave.hovered === 'false',
75
+ leave_ringHidden: afterLeave.ringOpacity === 0,
76
+ };
77
+ const ok = Object.values(results).every(Boolean);
78
+ console.log(`${ok ? '✅' : '❌'} hub-hover ring:`, JSON.stringify(results),
79
+ `\n before=`, before,
80
+ `\n onHover=`, onHover,
81
+ `\n afterLeave=`, afterLeave);
82
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,126 @@
1
+ /* Round 116 verification: recent-signal row click pins the edge.
2
+ * R56 made hover transient; R116 makes click sticky — survives
3
+ * mouseleave, releases on second click or Esc. activeEdgeKey =
4
+ * hoveredEdgeKey ?? pinnedEdgeKey, so hover-while-pinned previews
5
+ * another edge transiently and mouseleave falls back to the pin.
6
+ *
7
+ * Drives 2 flow rows. Click row 0 → row's aria-pressed=true,
8
+ * data-recent-row-pinned=true, the edge stays bright after mouseleave
9
+ * (non-endpoints stay dim, endpoint ring stays). Click row 0 again
10
+ * → release. Pin row 0, hover row 1 → row-1 preview wins; leave →
11
+ * row-0 pin restores. Esc clears.
12
+ */
13
+ import { chromium } from 'playwright';
14
+ import { readFileSync } from 'node:fs';
15
+
16
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
17
+ const browser = await chromium.launch({ headless: true });
18
+
19
+ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1500 } });
20
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
21
+ await ctx.addInitScript(() => {
22
+ try { localStorage.setItem('anet-theme', 'cyber'); sessionStorage.setItem('anet_v3_auth', '1'); } catch {}
23
+ });
24
+
25
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
26
+ await ctx.route('**/api/hub/status*', async (route) => {
27
+ const r = await route.fetch();
28
+ const b = await r.json();
29
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
30
+ const mk = (alias) => ({
31
+ alias, status: 'idle', model: 'claude-opus-4', runtime: 'cli-claude-code',
32
+ network_id: nid, project_dir: null,
33
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
34
+ });
35
+ await route.fulfill({ response: r, json: { ...b, sessions: [mk('alpha'), mk('beta'), mk('gamma'), mk('delta')] } });
36
+ });
37
+
38
+ const now = Date.now();
39
+ const mkMsg = (id, from_alias, to_alias, ageMs) => ({
40
+ id, from_alias, to_alias, content: 'hi', network_id: 'default',
41
+ created_at: new Date(now - ageMs).toISOString(),
42
+ });
43
+ await ctx.route('**/api/hub/messages*', (route) => route.fulfill({ json: { messages: [
44
+ mkMsg('m1', 'alpha', 'beta', 30 * 1000),
45
+ mkMsg('m2', 'gamma', 'delta', 60 * 1000),
46
+ ] } }));
47
+ await ctx.route('**/api/hub/tasks*', (route) => route.fulfill({ json: { tasks: [] } }));
48
+ const page = await ctx.newPage();
49
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
50
+ await page.waitForFunction(() => document.querySelectorAll('g[data-node]').length === 4, { timeout: 30000 });
51
+ await page.waitForSelector('[data-recent-row]', { timeout: 10000 });
52
+ await page.waitForTimeout(400);
53
+
54
+ const rowState = (key) => page.evaluate((k) => {
55
+ const g = document.querySelector(`[data-recent-row="${k}"]`);
56
+ return {
57
+ pinned: g?.getAttribute('data-recent-row-pinned') || '',
58
+ aria: g?.getAttribute('aria-pressed') || '',
59
+ };
60
+ }, key);
61
+ const endpointRingFor = (alias) => page.evaluate((a) => {
62
+ const g = document.querySelector(`g[data-node="${a}"]`);
63
+ return !!g?.querySelector('[data-edge-endpoint-ring]');
64
+ }, alias);
65
+
66
+ const before = await rowState('alpha->beta');
67
+
68
+ // Click row 0 → pin.
69
+ await page.locator('[data-recent-row="alpha->beta"]').click();
70
+ await page.mouse.move(10, 10);
71
+ await page.waitForTimeout(300);
72
+ const afterPin = await rowState('alpha->beta');
73
+ const alphaRingPin = await endpointRingFor('alpha');
74
+ const betaRingPin = await endpointRingFor('beta');
75
+ const gammaRingPin = await endpointRingFor('gamma');
76
+
77
+ // Click again → release.
78
+ await page.locator('[data-recent-row="alpha->beta"]').click();
79
+ await page.mouse.move(10, 10);
80
+ await page.waitForTimeout(300);
81
+ const afterRelease = await rowState('alpha->beta');
82
+ const alphaRingRelease = await endpointRingFor('alpha');
83
+
84
+ // Pin row 0 again, hover row 1 — preview wins.
85
+ await page.locator('[data-recent-row="alpha->beta"]').click();
86
+ await page.mouse.move(10, 10);
87
+ await page.waitForTimeout(200);
88
+ await page.locator('[data-recent-row="gamma->delta"]').hover();
89
+ await page.waitForTimeout(300);
90
+ const alphaWhileHoverGamma = await endpointRingFor('alpha');
91
+ const gammaWhileHover = await endpointRingFor('gamma');
92
+
93
+ // Leave gamma row — row-0 pin restores.
94
+ await page.mouse.move(10, 10);
95
+ await page.waitForTimeout(300);
96
+ const alphaAfterLeave = await endpointRingFor('alpha');
97
+ const gammaAfterLeave = await endpointRingFor('gamma');
98
+
99
+ // Esc clears.
100
+ await page.keyboard.press('Escape');
101
+ await page.waitForTimeout(300);
102
+ const afterEsc = await rowState('alpha->beta');
103
+ const alphaAfterEsc = await endpointRingFor('alpha');
104
+
105
+ await browser.close();
106
+
107
+ const results = {
108
+ before_notPinned: before.pinned === 'false' && before.aria === 'false',
109
+ pin_attrTrue: afterPin.pinned === 'true' && afterPin.aria === 'true',
110
+ pin_endpointsLit: alphaRingPin === true && betaRingPin === true,
111
+ pin_nonEndpointDark: gammaRingPin === false,
112
+ releaseAfterReclick: afterRelease.pinned === 'false' && alphaRingRelease === false,
113
+ hoverPreview_gammaLit: gammaWhileHover === true,
114
+ hoverPreview_alphaDark: alphaWhileHoverGamma === false,
115
+ leave_pinRestored: alphaAfterLeave === true && gammaAfterLeave === false,
116
+ esc_clearsPin: afterEsc.pinned === 'false' && alphaAfterEsc === false,
117
+ };
118
+ const ok = Object.values(results).every(Boolean);
119
+ console.log(`${ok ? '✅' : '❌'} recent-row pin:`, JSON.stringify(results),
120
+ `\n afterPin=`, afterPin,
121
+ `\n rings@pin: alpha=`, alphaRingPin, ' beta=', betaRingPin, ' gamma=', gammaRingPin,
122
+ `\n afterRelease=`, afterRelease,
123
+ `\n hoverPreview: alpha=`, alphaWhileHoverGamma, ' gamma=', gammaWhileHover,
124
+ `\n afterLeave: alpha=`, alphaAfterLeave, ' gamma=', gammaAfterLeave,
125
+ `\n afterEsc=`, afterEsc);
126
+ process.exit(ok ? 0 : 1);