@sleep2agi/agent-network-dashboard 0.5.1-preview.99 → 0.5.2-preview.0

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 (203) 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 +32 -32
  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 +12 -12
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  17. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  18. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  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 +14 -14
  25. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  26. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  27. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  28. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  29. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  30. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  31. package/.next/server/app/index.html +2 -2
  32. package/.next/server/app/index.rsc +14 -14
  33. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  34. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  35. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  36. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  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 +14 -14
  41. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  42. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  43. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  44. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  45. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  46. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  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 +14 -14
  50. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  51. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  52. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  53. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  54. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  55. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  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 +14 -14
  59. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  60. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  61. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  62. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  63. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  64. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  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 +14 -14
  68. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  69. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  70. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  71. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  72. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  73. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  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 +14 -14
  77. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  78. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  79. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  80. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  81. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  82. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  83. package/.next/server/app/page.js.nft.json +1 -1
  84. package/.next/server/app/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  86. package/.next/server/app/server-logs.html +2 -2
  87. package/.next/server/app/server-logs.rsc +14 -14
  88. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  89. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  90. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  91. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  92. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  93. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  94. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  95. package/.next/server/app/settings/networks.html +2 -2
  96. package/.next/server/app/settings/networks.rsc +14 -14
  97. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  98. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  99. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  100. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  101. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  102. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  103. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  104. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  105. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  106. package/.next/server/app/settings/tokens.html +2 -2
  107. package/.next/server/app/settings/tokens.rsc +14 -14
  108. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  109. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  110. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  111. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  112. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  113. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  114. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  115. package/.next/server/app/settings.html +2 -2
  116. package/.next/server/app/settings.rsc +14 -14
  117. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  118. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  119. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  120. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  121. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  122. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  123. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  125. package/.next/server/app/tasks.html +2 -2
  126. package/.next/server/app/tasks.rsc +14 -14
  127. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  128. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  129. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  130. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  131. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  132. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  133. package/.next/server/chunks/ssr/{[root-of-the-server]__03b.f76._.js → [root-of-the-server]__0sv~g.o._.js} +2 -2
  134. package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js.map +1 -0
  135. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -3
  136. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  137. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  138. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  139. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  140. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  141. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +1 -1
  142. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -1
  143. package/.next/server/middleware-build-manifest.js +3 -3
  144. package/.next/server/pages/404.html +2 -2
  145. package/.next/server/pages/500.html +1 -1
  146. package/.next/server/server-reference-manifest.js +1 -1
  147. package/.next/server/server-reference-manifest.json +1 -1
  148. package/.next/static/chunks/{0l_~q07bhpkcx.js → 03a4--7ncekmk.js} +1 -1
  149. package/.next/static/chunks/0a4hmfvj-81x5.css +2 -0
  150. package/.next/static/chunks/0caxil0dw-oe9.js +4 -0
  151. package/.next/static/chunks/{03~~oirxz7~vc.js → 0p8xwrfjtykvn.js} +1 -1
  152. package/.next/static/chunks/0x.m3vy8e5iit.js +1 -0
  153. package/.next/static/chunks/12gq1w9k_7v06.js +1 -0
  154. package/.next/trace +2 -2
  155. package/.next/trace-build +1 -1
  156. package/app/components/ServersDrawer.tsx +17 -0
  157. package/app/components/TopoGraph.tsx +770 -76
  158. package/package.json +1 -1
  159. package/screenshots/v0.10.2-disk-verify/disk-render-full.png +0 -0
  160. package/screenshots/v0.10.2-disk-verify/disk-render.png +0 -0
  161. package/screenshots/v0.11.0-147/after.png +0 -0
  162. package/scripts/p0-147-screenshot.mjs +53 -0
  163. package/scripts/p0-servers-drawer-screenshot.mjs +2 -0
  164. package/scripts/topo-any-hover-attr-test.mjs +83 -0
  165. package/scripts/topo-any-pinned-attr-test.mjs +86 -0
  166. package/scripts/topo-chrome-fullscreen-icon-sw-test.mjs +92 -0
  167. package/scripts/topo-chrome-reset-icon-sw-test.mjs +80 -0
  168. package/scripts/topo-chrome-zoom-icon-sw-test.mjs +90 -0
  169. package/scripts/topo-dashboard-version-attr-test.mjs +69 -0
  170. package/scripts/topo-dense-alias-opacity-test.mjs +68 -0
  171. package/scripts/topo-edge-particle-hover-r-test.mjs +113 -0
  172. package/scripts/topo-endpoint-ring-r-hover-test.mjs +89 -0
  173. package/scripts/topo-fleet-count-attrs-test.mjs +87 -0
  174. package/scripts/topo-group-box-geom-transition-test.mjs +110 -0
  175. package/scripts/topo-group-box-rx-pin-test.mjs +103 -0
  176. package/scripts/topo-group-label-count-fw-test.mjs +100 -0
  177. package/scripts/topo-group-label-fw-pin-test.mjs +99 -0
  178. package/scripts/topo-group-label-tint-geom-test.mjs +94 -0
  179. package/scripts/topo-group-label-tint-transition-test.mjs +97 -0
  180. package/scripts/topo-group-pip-fontsize-test.mjs +106 -0
  181. package/scripts/topo-group-tier-attr-test.mjs +84 -0
  182. package/scripts/topo-group-tint-rx-pin-test.mjs +107 -0
  183. package/scripts/topo-hub-core-fill-hover-test.mjs +85 -0
  184. package/scripts/topo-hub-halo-r-hover-test.mjs +82 -0
  185. package/scripts/topo-legend-count-active-opacity-test.mjs +102 -0
  186. package/scripts/topo-legend-count-pin-fw-test.mjs +90 -0
  187. package/scripts/topo-minimap-viewport-opacity-test.mjs +96 -0
  188. package/scripts/topo-node-halo-hover-opacity-test.mjs +104 -0
  189. package/scripts/topo-node-halo-light-offline-test.mjs +80 -0
  190. package/scripts/topo-node-sub-text-fw-test.mjs +75 -0
  191. package/scripts/topo-overlap-stale-build-guard-test.mjs +66 -0
  192. package/scripts/topo-overlap-test.mjs +42 -1
  193. package/scripts/topo-recent-row-count-pin-fw-test.mjs +106 -0
  194. package/scripts/topo-recent-row-pip-hover-r-test.mjs +104 -0
  195. package/scripts/topo-runtime-icon-hover-test.mjs +96 -0
  196. package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js.map +0 -1
  197. package/.next/static/chunks/0.sf46gnv4wwm.js +0 -1
  198. package/.next/static/chunks/017hq2-5l~_98.css +0 -2
  199. package/.next/static/chunks/0_igeywsok2_-.js +0 -1
  200. package/.next/static/chunks/0igz0bww16uvc.js +0 -4
  201. /package/.next/static/{4rHxTzLwe2XC9M1rN-MpJ → 5IMzNtH5S5Oqe-FCw1CX4}/_buildManifest.js +0 -0
  202. /package/.next/static/{4rHxTzLwe2XC9M1rN-MpJ → 5IMzNtH5S5Oqe-FCw1CX4}/_clientMiddlewareManifest.js +0 -0
  203. /package/.next/static/{4rHxTzLwe2XC9M1rN-MpJ → 5IMzNtH5S5Oqe-FCw1CX4}/_ssgManifest.js +0 -0
@@ -8,6 +8,7 @@ import { aliasAvatarColors, aliasInitial } from './AliasAvatar';
8
8
  import { ChatPopover } from './ChatPopover';
9
9
  import { vendorForModel, runtimeIdentity, identityLine } from '../lib/vendorIdentity';
10
10
  import { parseHubTime, relativeAgo } from '../lib/time';
11
+ import { DASHBOARD_VERSION } from '../lib/version';
11
12
 
12
13
  /** v0.10.0 Hero 1+2 / §3.F server-health hook — fetches the normalized
13
14
  * /api/hub/servers payload (preview.370 unblocked real-data via the
@@ -3457,6 +3458,108 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3457
3458
  return `Agent network topology — ${parts.join(' · ')}. Tab to navigate nodes, double-click canvas to reset view.`;
3458
3459
  })()}
3459
3460
  data-topo-canvas-aria
3461
+ /* Round 469 / Loop — fleet-split numeric attrs on the root
3462
+ svg. The aria-label already encodes online/working/offline
3463
+ /flow counts in text form (R7 origin) but DOM probes had
3464
+ to PARSE the label string to extract the numbers. R469
3465
+ surfaces them as 4 numeric data-attrs alongside the R462
3466
+ dashboard-version + R466 any-hover + R467 any-pinned set
3467
+ that already live on the root svg:
3468
+ data-topo-online-count total online sessions
3469
+ data-topo-working-count subset currently working
3470
+ data-topo-offline-count offline / ghost-purged
3471
+ data-topo-flow-count active flow links
3472
+ Use cases:
3473
+ - Playwright: one-line `svg.getAttribute('data-topo-
3474
+ working-count')` instead of parsing aria-label
3475
+ - external CSS: data-attribute selectors for empty
3476
+ vs populated states (`[data-topo-online-count='0']`)
3477
+ - a11y enrichment: screen-reader scripts can read the
3478
+ numeric attrs directly
3479
+ - hub-aria parity: the hub-center text already shows
3480
+ `workingCount` digit (R130); R469 puts the same scalar
3481
+ on the canvas root for non-visual consumers.
3482
+ Composed from existing onlineNodes / workingCount /
3483
+ offlineNodes / flowLinks — no new state. */
3484
+ data-topo-online-count={onlineNodes.length}
3485
+ data-topo-working-count={workingCount}
3486
+ data-topo-offline-count={offlineNodes.length}
3487
+ data-topo-flow-count={flowLinks.length}
3488
+ /* Round 466 / Loop — aggregate hover signal on the root SVG.
3489
+ Exposes a single boolean `data-topo-any-hover` that
3490
+ reflects whether ANY hover state in the topology is
3491
+ active. Composed from the existing per-surface hover
3492
+ vars; doesn't introduce new state. Useful for:
3493
+ - Playwright tests asserting "topology entered a hover
3494
+ mode" without enumerating per-surface attrs
3495
+ - external CSS hooks targeting `[data-topo-any-hover=
3496
+ "true"]` to dim adjacent UI (e.g. chrome strip)
3497
+ while the user is inspecting the canvas
3498
+ - debug overlays that visualise hover dwell-time
3499
+ The 6 hover sources contributing:
3500
+ hoveredAlias (node circle / card / alias text)
3501
+ hoveredHub (hub center, halo, ring)
3502
+ hoveredEdgeKey (flow link path / particle / endpoint)
3503
+ hoveredGroupLabel (cluster name / count / pips)
3504
+ hoveredStatus (legend row)
3505
+ hoveredVendor (vendor chip in chip row)
3506
+ Read-only computed attr — zero re-render cost beyond the
3507
+ React update that already fires when any of those state
3508
+ vars flips. Geometry / paint untouched. */
3509
+ data-topo-any-hover={
3510
+ (hoveredAlias || hoveredHub || hoveredEdgeKey || hoveredGroupLabel ||
3511
+ hoveredStatus || hoveredVendor) ? 'true' : 'false'
3512
+ }
3513
+ /* Round 467 / Loop — pin-aggregate sibling to R466 hover-
3514
+ aggregate. Exposes `data-topo-any-pinned` reflecting
3515
+ whether ANY sticky inspection mode is active. Composed
3516
+ from the 4 pinned state vars:
3517
+ pinnedStatus (legend row click → status filter)
3518
+ pinnedGroup (group label click → cluster lock)
3519
+ pinnedVendor (vendor chip click → vendor filter)
3520
+ pinnedEdgeKey (edge click → edge focus)
3521
+ Together with R466 the root svg now carries a 2-bit
3522
+ inspection-mode surface:
3523
+ data-topo-any-hover — transient (mouse hover)
3524
+ data-topo-any-pinned — sticky (click-to-lock)
3525
+ Useful for:
3526
+ - Playwright tests: one-line query for either mode
3527
+ - external CSS hooks: render a persistent "filter
3528
+ active" badge when pinned, distinct from the
3529
+ transient hover dim
3530
+ - Esc-handler tests: assert all 4 pins clear after
3531
+ the universal-cancel Escape press (R62/R63/R88/
3532
+ R116 — single Esc collapses every pin)
3533
+ Read-only computed disjunction; no new state, zero
3534
+ re-render cost beyond the React pin-flip updates. */
3535
+ data-topo-any-pinned={
3536
+ (pinnedStatus || pinnedGroup || pinnedVendor || pinnedEdgeKey) ? 'true' : 'false'
3537
+ }
3538
+ /* Round 462 / Loop — surface DASHBOARD_VERSION on the root SVG
3539
+ element as `data-dashboard-version`. Directly closes the
3540
+ feedback_dash_zombie_port_3000.md memory rule: "verify ships
3541
+ via SVG DOM, not tmux 'Ready' — zombie next-servers + stale
3542
+ global installs silently serve old code". Pre-R462 the only
3543
+ ways to know which preview the dash was serving were:
3544
+ 1. parse the npm registry for the latest tag (network)
3545
+ 2. fetch /api/dashboard/version (API surface, no DOM)
3546
+ 3. inspect the /login footer or /settings page (off-route)
3547
+ Test scripts that probe TopoGraph DOM (overlap, group-label
3548
+ tint, pip strip, etc.) couldn't tell whether the dash was
3549
+ actually serving the build they expected to verify. R462
3550
+ threads DASHBOARD_VERSION through to the root <svg> so:
3551
+ - Playwright probes can read svg[data-dashboard-version]
3552
+ directly + fail-fast on stale-build mismatch
3553
+ - the memory rule's manual zombie check ("inspect SVG
3554
+ dom") becomes a one-attr probe
3555
+ - operators DOM-inspect to confirm the live version
3556
+ matches the npm tag without leaving the topology page
3557
+ Geometry/visual impact: ZERO (data-* attrs don't paint).
3558
+ The version string is build-time injected via the existing
3559
+ DASHBOARD_VERSION constant (R51 footer + R51 settings page
3560
+ already consume it from app/lib/version.ts → reads
3561
+ package.json pkg.version). No business logic added. */
3562
+ data-dashboard-version={DASHBOARD_VERSION}
3460
3563
  onPointerDown={onPointerDown}
3461
3564
  onPointerMove={onPointerMove}
3462
3565
  onPointerUp={onPointerUp}
@@ -4203,10 +4306,31 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4203
4306
  // reused on the data attribute + the inline custom property.
4204
4307
  const w = box.statuses.working;
4205
4308
  const marchDur = w >= 6 ? 8 : w >= 4 ? 10 : w >= 2 ? 12 : 14;
4309
+ // Round 468 / Loop — single-tier classifier. Surfaces the
4310
+ // semantic the R319 pip-strip already encodes implicitly:
4311
+ // a cluster where every member sits in one status tier
4312
+ // renders as `name · count` only (offending duplicate pip
4313
+ // dropped). Pre-R468 that "all members in tier X" fact
4314
+ // was visible to the eye (no pips) but not queryable from
4315
+ // the DOM. R468 attaches the classifier as
4316
+ // `data-group-tier`:
4317
+ // 'all-working' — w===count, fleet uniformly busy
4318
+ // 'all-idle' — i===count, fleet uniformly waiting
4319
+ // 'all-offline' — o===count, fleet uniformly down
4320
+ // 'mixed' — at least 2 tiers present
4321
+ // Sibling R466/R467 pattern — expose composed state as a
4322
+ // data-attr without changing paint. Use cases: Playwright
4323
+ // assertions, external CSS hooks, accessibility enrichment.
4324
+ const groupTier =
4325
+ box.statuses.working === box.count ? 'all-working' :
4326
+ box.statuses.idle === box.count ? 'all-idle' :
4327
+ box.statuses.offline === box.count ? 'all-offline' :
4328
+ 'mixed';
4206
4329
  return (
4207
4330
  <g
4208
4331
  key={`grp-${box.key}`}
4209
4332
  data-group={box.key}
4333
+ data-group-tier={groupTier}
4210
4334
  // Round 173 / Loop: group boxes pick up the first-paint
4211
4335
  // fade-in wave alongside R9 staggered nodes (0-540ms)
4212
4336
  // and R172 staggered edges (280-980ms). Pre-R173 the
@@ -4241,7 +4365,29 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4241
4365
  y={box.y}
4242
4366
  width={box.w}
4243
4367
  height={box.h}
4244
- rx="14"
4368
+ /* Round 464 / Loop: group-box rx 14 → 16 on isPinned.
4369
+ Geometric softening at the corner radius — locked
4370
+ groups read with subtly rounder shoulders than
4371
+ hovered/idle. +2px reads as a calm \"settled in\"
4372
+ posture (subtler than a fill or stroke bump but
4373
+ unmistakable across the whole cluster boundary).
4374
+ Pin signature on the group-box rect now spans 7
4375
+ axes:
4376
+ R63 text fill brighten
4377
+ R142 drop-shadow filter
4378
+ R432 text letter-spacing 0→0.5
4379
+ R444 count tspan fw 500→600
4380
+ R457 parent text fw 700→800
4381
+ codex p.125 text opacity 0.55→1
4382
+ R464 corner rx 14→16 (this round)
4383
+ transition list (R461) already covers x/y/width/
4384
+ height 200ms ease-out; appended `rx 200ms ease-
4385
+ out` so the rounding eases alongside the geometry
4386
+ axes. SVG2 CSS animation on rx: Chrome 95+ /
4387
+ Safari 16+ / FF 70+ (same matrix as x/y/w/h).
4388
+ data-group-box-rx exposes the resolved value. */
4389
+ rx={isPinned ? '16' : '14'}
4390
+ data-group-box-rx={isPinned ? '16' : '14'}
4245
4391
  fill={isLight ? '#0f172a' : '#a5b4fc'}
4246
4392
  // R68: 3-tier opacity + stroke ladder.
4247
4393
  // pinned → fill 0.08 / 0.13, stroke 3 px (locked)
@@ -4281,6 +4427,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4281
4427
  data-group-box-pinned={isPinned ? 'true' : 'false'}
4282
4428
  data-group-box-linecap="round"
4283
4429
  data-group-box-linejoin="round"
4430
+ data-group-box-geom-transition="x,y,width,height"
4284
4431
  // R85: ambient "marching ants" drift on the perimeter
4285
4432
  // when this group has at least one working member, and
4286
4433
  // neither pin nor hover is active (those treatments
@@ -4318,8 +4465,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4318
4465
  while stroke / fill-opacity / filter all eased.
4319
4466
  Closes the last theme-toggle snap on the group
4320
4467
  box surface — same idiom R246 + R247 used at
4321
- per-node label-card and side-panel scopes. */
4322
- transition: 'stroke 200ms ease-out, stroke-width 200ms ease-out, fill-opacity 200ms ease-out, filter 200ms ease-out, fill 200ms ease-out',
4468
+ per-node label-card and side-panel scopes.
4469
+ Round 461 / Loop: extend the transition list to
4470
+ all 4 geometry axes (x, y, width, height) so
4471
+ when a cluster grows / shrinks (member joins,
4472
+ leaves, prefix rebalance, dense toggle, status
4473
+ flip) the BIG outer container slides into the
4474
+ new bounds at the same 200ms cadence the R460
4475
+ inner hitbox tint rect now uses. Pre-R461 the
4476
+ outer 200×140 px box snap-jumped on cluster
4477
+ resize while the inner 160×18 hitbox slid —
4478
+ jarring two-rate motion at the same surface.
4479
+ R461 unifies both rects to slide as one, with
4480
+ the parent box driving the visual envelope and
4481
+ the inner hitbox tracking the bottom-edge tint.
4482
+ Hero D #147 motion-coherence at the FULL cluster
4483
+ container tier (not just the label tint).
4484
+ data-group-box-geom-transition attr exposed. */
4485
+ transition: 'stroke 200ms ease-out, stroke-width 200ms ease-out, fill-opacity 200ms ease-out, filter 200ms ease-out, fill 200ms ease-out, x 200ms ease-out, y 200ms ease-out, width 200ms ease-out, height 200ms ease-out, rx 200ms ease-out',
4323
4486
  pointerEvents: 'none',
4324
4487
  // CSS var consumed by `.anet-topo-groupbox-live`
4325
4488
  // (line 877 of globals.css). React's CSSProperties
@@ -4397,27 +4560,83 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4397
4560
  ].filter(Boolean).join('\n')}</title>
4398
4561
  );
4399
4562
  })()}
4563
+ {/* v0.11.0 #147 Hero D — Vincent 5401: "太大太丑,
4564
+ 都放到框的右下角的小字". First-cut bottom-right
4565
+ placement collided with bottom-row nodes (cluster
4566
+ geometry has no bottom padding; only GROUP_TOP=12
4567
+ top band). Pivoted to Option C from #147 spec:
4568
+ keep top-left anchor BUT shrink fontSize (13 → 9)
4569
+ and dim default opacity (1 → 0.55, hover/pin
4570
+ restore to 1). Satisfies "太大太丑" via the size +
4571
+ opacity axes while keeping the existing geometry
4572
+ contract that topo-overlap-test gates. Hitbox
4573
+ rect width tightens to min(box.w-12, 160) to
4574
+ track the narrower label render. */}
4575
+ {/* Round 465 / Loop — hitbox tint rect rx 4 → 5 on
4576
+ pinnedGroup match. Mirrors R464 (parent group-box
4577
+ rx 14 → 16 on isPinned) at the hitbox tier. The
4578
+ R460 hitbox carried fixed rx=4 since codex p.125
4579
+ pivoted it to the bottom-of-band position; the
4580
+ pin-state geometric softening was only on the BIG
4581
+ outer container, not the small hitbox underneath.
4582
+ R465 adds +1 px corner rounding on pin so the
4583
+ tint rect echoes the parent's locked posture at
4584
+ its own scale (8% relative bump matches R464's
4585
+ 14→16 ≈ 14% scaled to the smaller rect).
4586
+ Transition list (R460 fill/opacity/x/width 200ms
4587
+ ease-out) extends to include `rx 200ms ease-out`
4588
+ so the rounding eases under the same cadence.
4589
+ SVG2 CSS animation on rx: Chrome 95+ / Safari
4590
+ 16+ / FF 70+ (same matrix as x/y/w/h).
4591
+ data-group-label-tint-rx exposes the resolved
4592
+ value for tests. */}
4400
4593
  <rect
4401
4594
  x={box.x + 6}
4402
4595
  y={box.y + 2}
4403
- width={Math.min(box.w - 12, 240)}
4404
- height={20}
4405
- rx="4"
4406
- /* R107 / Loop: list-item tint extends to the SVG
4407
- group labels — same idiom R104 added to recent-
4408
- signal rows and R105 to the legend rows. The
4409
- tint colour is pal.legendAccent (cyan) since
4410
- groups don't carry an inherent swatch the way
4411
- legend rows do; this matches R68's group-box
4412
- isPinned/isHovered accent stroke for consistency.
4413
- hover < pinned opacity so locked vs preview is
4414
- discriminable at a glance. */
4596
+ width={Math.min(box.w - 12, 160)}
4597
+ height={18}
4598
+ rx={pinnedGroup === box.key ? '5' : '4'}
4599
+ data-group-label-tint-rx={pinnedGroup === box.key ? '5' : '4'}
4415
4600
  fill={pinnedGroup === box.key || hoveredGroupLabel === box.key ? pal.legendAccent : 'transparent'}
4416
4601
  opacity={pinnedGroup === box.key ? (isLight ? 0.16 : 0.20)
4417
4602
  : hoveredGroupLabel === box.key ? (isLight ? 0.09 : 0.13)
4418
4603
  : 1}
4419
4604
  data-group-label-tinted={pinnedGroup === box.key ? 'pinned' : hoveredGroupLabel === box.key ? 'hover' : 'none'}
4420
- style={{ transition: 'fill 150ms ease-out, opacity 150ms ease-out' }}
4605
+ /* Round 459 / Loop cadence-sync follow-on to codex
4606
+ preview.125 (Hero D #147). Codex's parent <text>
4607
+ transition list now reads:
4608
+ 'fill 200ms, letter-spacing 200ms,
4609
+ font-weight 200ms, opacity 200ms'
4610
+ — 200ms ease-out across every axis. The label
4611
+ hitbox tint rect underneath was still at 150ms
4612
+ (legacy R107 cadence), so the tint snapped in
4613
+ 50ms ahead of the parent label brightening —
4614
+ a small but perceivable mistimed cascade when
4615
+ hovering or clicking to pin a cluster. R459
4616
+ lifts both axes to 200ms to lock the tint
4617
+ under the label as one motion-coherent state
4618
+ flip. Hover/pin/unpin all feel as a single
4619
+ unified ease rather than "tint pops, label
4620
+ follows". data-group-label-tint-transition
4621
+ attr exposes the timing for tests. */
4622
+ /* Round 460 / Loop — extend the R459-200ms tint rect
4623
+ transition list to include `x` + `width` so the
4624
+ hitbox slides into place when a cluster grows or
4625
+ shrinks (member joins / leaves / status change
4626
+ re-pricing box.w). Pre-R460 every resize snap-
4627
+ jumped the hitbox bounds — a small but visible
4628
+ glitch right at the moment the operator's
4629
+ attention is on the cluster. SVG2 CSS animation
4630
+ on geometry attrs has shipped in Chrome 95+ /
4631
+ Safari 16+ / FF 70+; the runtime gracefully
4632
+ no-ops on older browsers. Sibling motion idiom
4633
+ to R134 / R141 / R142 (panel rect transitions)
4634
+ at the group-label hitbox tier.
4635
+ data-group-label-tint-geom-transition attr
4636
+ exposes the geometry-axis presence for tests. */
4637
+ data-group-label-tint-transition="200ms"
4638
+ data-group-label-tint-geom-transition="x,width,rx"
4639
+ style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out, x 200ms ease-out, width 200ms ease-out, rx 200ms ease-out' }}
4421
4640
  />
4422
4641
  {/* Round 218 / Loop: group label gains a letter-spacing
4423
4642
  transition on pin — the text subtly spaces out
@@ -4463,16 +4682,47 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4463
4682
  R432 group label text (this round)
4464
4683
  R218 transition list ('fill 200ms, letter-spacing
4465
4684
  200ms') untouched — additive conditional case. */}
4685
+ {/* Round 457 / Loop: group label parent text fontWeight
4686
+ 700 → 800 on isPinned. Adds typographic weight axis
4687
+ to the group-label parent text, sibling to R432
4688
+ letter-spacing tween at the same surface. Pre-R457
4689
+ pin lifted ls 0 → 0.5px (R218→R432 3-tier) but the
4690
+ fw stayed planted at R63's 700 — locked groups
4691
+ read as wider-but-same-weight. R457 adds the
4692
+ weight axis so pinned groups read as tightened
4693
+ AND wider, matching the R416/R424/R425/R426/R444/
4694
+ R445/R446 "data tightens under attention" idiom
4695
+ (now extended to the parent-text scope at the
4696
+ group-label tier). R63 fill brighten + R432
4697
+ letter-spacing 0/0.25/0.5 3-tier + R55 transition
4698
+ list all preserved; extends to include 'font-
4699
+ weight 200ms ease-out' so the bump eases under
4700
+ the same cadence. */}
4701
+ {/* v0.11.0 #147 Hero D — Vincent 5401 ask: "dash 网络
4702
+ 图里面这个工程的名字也太大了, 超级丑". Per Vincent
4703
+ screenshot 实测. Initial attempt moved label to
4704
+ bottom-right (#147 spec Option A); topo-overlap-test
4705
+ caught 7 grid collisions because cluster boxes have
4706
+ no bottom padding. Pivot to Option C: keep top-left
4707
+ anchor, shrink fontSize 13 → 9 (-31%, watermark
4708
+ register), dim default opacity 1 → 0.55 (hover/pin
4709
+ restore to 1). Net Twitter-grok improvement:
4710
+ cluster labels no longer dominate the canvas at
4711
+ rest; operator still hovers to find specific groups.
4712
+ Position unchanged to preserve the existing
4713
+ geometry that overlap-test gates. */}
4466
4714
  <text
4467
4715
  x={box.x + 12}
4468
- y={box.y + 14}
4716
+ y={box.y + 12}
4469
4717
  fill={isHovered ? pal.legendHeadline : pal.legendText}
4470
- fontSize="13"
4718
+ fontSize="9"
4471
4719
  fontFamily="monospace"
4472
- fontWeight="700"
4720
+ fontWeight={isPinned ? '800' : '700'}
4721
+ opacity={isPinned || isHovered ? 1 : 0.55}
4473
4722
  data-group-label-hovered={isHovered && !isPinned ? 'true' : 'false'}
4723
+ data-group-label-font-weight={isPinned ? '800' : '700'}
4474
4724
  style={{
4475
- transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',
4725
+ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, opacity 200ms ease-out',
4476
4726
  letterSpacing: isPinned ? '0.5px' :
4477
4727
  isHovered ? '0.25px' : '0px',
4478
4728
  }}
@@ -4528,14 +4778,47 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4528
4778
  fill-inherit from parent label (hover-deepen-own-
4529
4779
  hue family) preserved. data-group-label-count-
4530
4780
  font-weight attr exposes the value for tests. */}
4781
+ {/* Round 444 / Loop: group label count tspan
4782
+ fontWeight 500 → 600 on isPinned. Extends the
4783
+ "data tightens under attention" typographic-
4784
+ weight pattern to a 5th anchor at the group-
4785
+ label-count scope:
4786
+ R416 chip-digit (chip hover)
4787
+ R424 panel-digit (panel hover)
4788
+ R425 hub-digit (hub hover)
4789
+ R426 edge-badge-digit (pin/hot)
4790
+ R444 group-label-count (pinned) ← this round
4791
+ Same idiom — when the group is locked, its
4792
+ member-count tightens typographically alongside
4793
+ the R432 letter-spacing spread (0 → 0.5px) on
4794
+ the parent label. Hover keeps rest fw (500) so
4795
+ the locked vs preview distinction at the type
4796
+ level stays intact — same gate R432 used.
4797
+ Monospace + R225 tabular-nums lock the digit
4798
+ width across fw changes; bbox unchanged; overlap-
4799
+ test invariants hold. transition list adds
4800
+ 'font-weight 200ms ease-out' matching R432
4801
+ letter-spacing cadence. R229 fill-inherit
4802
+ preserved (parent text fill still drives the
4803
+ hover/pin color). data-group-label-count-font-
4804
+ weight + -pinned attrs exposed for tests. */}
4805
+ {/* v0.11.0 #147 — count tspan tracks parent fontSize:
4806
+ 11 → 8 to match the new 9px label scale (parent
4807
+ dropped 13 → 9 with same -2px gap to the count
4808
+ suffix). dx="4" replaces dx="6" — the smaller
4809
+ glyph baseline doesn't need the wider gutter. */}
4531
4810
  <tspan
4532
- dx="6"
4533
- fontSize="11"
4534
- fontWeight="500"
4811
+ dx="4"
4812
+ fontSize="8"
4813
+ fontWeight={isPinned ? '600' : '500'}
4535
4814
  data-group-label-count={box.key}
4536
4815
  data-group-label-count-value={box.count}
4537
- data-group-label-count-font-weight="500"
4538
- style={{ fontVariantNumeric: 'tabular-nums' }}
4816
+ data-group-label-count-pinned={isPinned ? 'true' : 'false'}
4817
+ data-group-label-count-font-weight={isPinned ? '600' : '500'}
4818
+ style={{
4819
+ fontVariantNumeric: 'tabular-nums',
4820
+ transition: 'font-weight 200ms ease-out',
4821
+ }}
4539
4822
  >· {box.count}</tspan>
4540
4823
  {/* Round 58 / Loop: status mix pip strip. Compact text-
4541
4824
  based chips (e.g. "2w 1i") so the strip stays inside
@@ -4592,11 +4875,36 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4592
4875
  breakdown. Multi-tier groups (e.g. `alpha · 3
4593
4876
  2w 1i`) render unchanged — those pips genuinely
4594
4877
  add breakdown info that the total doesn't carry. */}
4878
+ {/* Round 458 / Loop — Hero D #147 finishing polish on top of
4879
+ N站牛/codex preview.125 (Option C: top-left label fontSize
4880
+ 13→9 + opacity 0.55 rest / 1 hover+pin, count tspan 11→8).
4881
+ That ship left the 3 status pips at fontSize=11 — visibly
4882
+ DOMINATING the now-9px parent label they trail. Result on
4883
+ a 5-member cluster: `alpha · 5 3w 2i` renders inside-out
4884
+ as "tiny name + tiny count + BIG bright pips" rather than
4885
+ a coherent right-tail of metadata. R458 scales the 3 pips
4886
+ to fontSize=8 (matches count tspan) and tightens dx 8/4/4
4887
+ → 6/3/3 (gutter ratio 0.73/0.36 glyph-widths @ 11px ≈
4888
+ 0.75/0.38 glyph-widths @ 8px — same visual rhythm at the
4889
+ smaller scale). The whole group-label bottom-right strip
4890
+ now reads as a unified 9/8/8/8 typographic ladder:
4891
+ name (parent <text>) fontSize 9 fw 700/800
4892
+ · count (1st tspan) fontSize 8 fw 500/600
4893
+ Nw (2nd tspan) fontSize 8 fw 600
4894
+ Ni (3rd tspan) fontSize 8 fw 600
4895
+ No (4th tspan) fontSize 8 fw 600
4896
+ Closes Vincent /goal 5401 ("太大太丑") at the pip-strip
4897
+ tier; with codex preview.125 the spec is fully realized.
4898
+ Geometry-only attribute changes — bbox tightens slightly
4899
+ (8px chars vs 11px chars stay inside the original 240px
4900
+ hitbox max) so topo-overlap-test invariants hold.
4901
+ tabular-nums + anet-fade-in + theme-eased fill 200ms
4902
+ preserved on every tspan. */}
4595
4903
  {box.statuses.working > 0 && box.statuses.working !== box.count && (
4596
4904
  <tspan
4597
- dx="8"
4905
+ dx="6"
4598
4906
  fill={isLight ? '#059669' : '#22c55e'}
4599
- fontSize="11"
4907
+ fontSize="8"
4600
4908
  fontWeight="600"
4601
4909
  className="anet-fade-in"
4602
4910
  data-group-pip="working"
@@ -4605,9 +4913,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4605
4913
  )}
4606
4914
  {box.statuses.idle > 0 && box.statuses.idle !== box.count && (
4607
4915
  <tspan
4608
- dx="4"
4916
+ dx="3"
4609
4917
  fill={isLight ? '#0d9488' : '#2dd4bf'}
4610
- fontSize="11"
4918
+ fontSize="8"
4611
4919
  fontWeight="600"
4612
4920
  className="anet-fade-in"
4613
4921
  data-group-pip="idle"
@@ -4616,9 +4924,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4616
4924
  )}
4617
4925
  {box.statuses.offline > 0 && box.statuses.offline !== box.count && (
4618
4926
  <tspan
4619
- dx="4"
4927
+ dx="3"
4620
4928
  fill={isLight ? '#94a3b8' : '#6b7280'}
4621
- fontSize="11"
4929
+ fontSize="8"
4622
4930
  fontWeight="600"
4623
4931
  className="anet-fade-in"
4624
4932
  data-group-pip="offline"
@@ -4972,25 +5280,29 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4972
5280
  all preserved. data-edge-particle-radius attr
4973
5281
  exposes the value for tests. */
4974
5282
  <circle
4975
- r="4.5"
5283
+ /* Round 439 / Loop: edge flow particle radius hover
5284
+ lift — r 4.5 → 5.5 on (isHoveredEdge ||
5285
+ isEndpointHoveredEdge). Continues edge paint-
5286
+ layer parity arc (R436 visible path sw / R437
5287
+ flow-rail sw / R439 particle r) so the whole
5288
+ edge surface — including the moving particle —
5289
+ lifts on hover, not just the static stripes.
5290
+ +1px radius gives ~50% area boost. Subtler than
5291
+ 1.4× sw bump on visible path because the
5292
+ particle is already small + motion-bright;
5293
+ +1px reads as "the dot caught attention"
5294
+ without overshadowing the path lift. R252
5295
+ transition list extends to include r 200ms so
5296
+ the size change eases under the same fill/
5297
+ opacity cadence. */
5298
+ r={(isHoveredEdge || isEndpointHoveredEdge) ? 5.5 : 4.5}
4976
5299
  fill={pal.flowParticle}
4977
5300
  filter={isLight ? undefined : 'url(#topo-glow)'}
4978
5301
  opacity={Math.min(1, fresh * edgeOpacityMul)}
4979
5302
  data-edge-particle={link.key}
4980
- data-edge-particle-radius="4.5"
4981
- /* Round 252 / Loop: particle picks up fill +
4982
- opacity transition for theme-toggle smoothing.
4983
- Pre-R252 pal.flowParticle (cyber #fef08a yellow
4984
- ↔ light #f59e0b amber) snapped on theme toggle
4985
- while every other edge element eased (R245
4986
- paths, R251 badge, R242 chat-target, R233
4987
- endpoint ring). opacity is freshness-driven so
4988
- it transitions per-frame as fresh decays anyway
4989
- — but adding opacity to the explicit transition
4990
- list also covers theme toggle (R3 className
4991
- transition-opacity duration-300 was previously
4992
- absent on this circle). */
4993
- style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out' }}
5303
+ data-edge-particle-radius={(isHoveredEdge || isEndpointHoveredEdge) ? 5.5 : 4.5}
5304
+ data-edge-particle-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
5305
+ style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out, r 200ms ease-out' }}
4994
5306
  >
4995
5307
  <animateMotion
4996
5308
  dur={`${duration}s`}
@@ -5722,13 +6034,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5722
6034
  // Mode='spline' + R245 ease-in-out keySplines all
5723
6035
  // preserved. data-topo-hub-halo-radius attr exposes
5724
6036
  // value for tests.
6037
+ /* Round 451 / Loop: hub center halo radius lift on
6038
+ hoveredHub — r 20 → 22 (+2px, ~21% area). Adds another
6039
+ geometric axis to the hub-hover signature stack
6040
+ alongside R177 ring radius lift + R209 digit scale +
6041
+ R425 digit fw + R370 halo opacity + R386 highlight
6042
+ opacity + R441 core fill chroma. Pre-R451 the halo
6043
+ r stayed planted at R408's 20px while the rest of
6044
+ the hub structure responded to hover. R451 makes
6045
+ the halo breath outward on hover so the focal pulse
6046
+ intensifies under attention. SMIL `<animate>` on
6047
+ opacity continues independently (animateAttr=
6048
+ 'opacity' vs CSS-property r — non-conflicting). R408
6049
+ base radius 20 preserved as rest; +2 hover delta
6050
+ keeps clearance from the R177 hub-hover-ring at
6051
+ r=17 hover (halo is BEHIND the ring, halo r=22 sits
6052
+ 5px beyond the ring's hover-r=17, still well within
6053
+ the hub canvas envelope). data-topo-hub-halo-radius
6054
+ attr now reports the dynamic value. */
6055
+ const isHaloHovered = !reducedMotion && hoveredHub;
6056
+ const haloR = isHaloHovered ? 22 : 20;
5725
6057
  return (
5726
6058
  <circle
5727
- cx={cx} cy={cy} r="20"
6059
+ cx={cx} cy={cy}
5728
6060
  fill={isLight ? '#d1fae5' : '#10b981'}
5729
6061
  opacity={isLight ? 0.42 : 0.12}
5730
6062
  data-hub-busyness={busy}
5731
- data-topo-hub-halo-radius="20"
6063
+ data-topo-hub-halo-radius={haloR}
6064
+ data-topo-hub-halo-hovered={isHaloHovered ? 'true' : 'false'}
5732
6065
  data-topo-hub-halo-trough={isLight ? troughLight : troughDark}
5733
6066
  data-topo-hub-halo-peak={isLight ? peakLight : peakDark}
5734
6067
  /* Round 253 / Loop: hub grounding halo fill transition
@@ -5737,8 +6070,14 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5737
6070
  animate on opacity continued running. CSS fill
5738
6071
  transition is independent of the SMIL animate
5739
6072
  (different attributes), so they compose without
5740
- conflict. */
5741
- style={{ transition: 'fill 200ms ease-out' }}
6073
+ conflict.
6074
+ R451: r as CSS property (R197/R198 idiom) so the
6075
+ hover-radius tween eases smoothly under the same
6076
+ 200ms cadence as fill. */
6077
+ style={{
6078
+ r: `${haloR}px`,
6079
+ transition: 'fill 200ms ease-out, r 200ms ease-out',
6080
+ } as React.CSSProperties}
5742
6081
  >
5743
6082
  {/* Round 244 / Loop: hub grounding halo breath gets
5744
6083
  ease-in-out keySplines, matching the active-node
@@ -5783,12 +6122,45 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5783
6122
  attr added for test introspection (the parent <g> at
5784
6123
  line 3587 has data-topo-hub but the core specifically
5785
6124
  is the canvas anchor). */}
5786
- <circle
5787
- cx={cx} cy={cy} r="10"
5788
- fill={isLight ? '#059669' : '#10b981'}
5789
- data-topo-hub-core
5790
- style={{ transition: 'fill 200ms ease-out' }}
5791
- />
6125
+ {(() => {
6126
+ /* Round 441 / Loop: hub center core fill brighten on
6127
+ hoveredHub. Pre-R441 the core was static (cyber
6128
+ emerald-500 #10b981 / light emerald-600 #059669) and
6129
+ the hub-hover gesture lifted ring radius (R177) +
6130
+ digit scale (R209) + digit fw (R425) + halo opacity
6131
+ (R370) + highlight opacity (R386) but the focal core
6132
+ ITSELF stayed planted at rest tone. R441 shifts the
6133
+ fill one emerald tier brighter on hover so the canvas
6134
+ anchor itself responds:
6135
+ cyber emerald-500 → emerald-400 (#10b981 → #34d399)
6136
+ light emerald-600 → emerald-500 (#059669 → #10b981)
6137
+ Same +100 step on the emerald scale across both themes.
6138
+ Pure paint axis; no geometry change. R248 fill 200ms
6139
+ transition already in the style list eases the shift.
6140
+ Closes the chroma axis on the hub-hover gesture stack:
6141
+ R177 ring radius lift geometry
6142
+ R209 digit scale 1.08 geometry
6143
+ R425 digit fw 700 → 800 typography
6144
+ R370 halo opacity 0.7 → 0.8 paint
6145
+ R386 highlight opacity paint
6146
+ R441 core fill brighten chroma ← this round
6147
+ data-topo-hub-core-hovered + -fill attrs exposed
6148
+ for tests. */
6149
+ const isCoreHovered = !reducedMotion && hoveredHub;
6150
+ const coreFill = isLight
6151
+ ? (isCoreHovered ? '#10b981' : '#059669')
6152
+ : (isCoreHovered ? '#34d399' : '#10b981');
6153
+ return (
6154
+ <circle
6155
+ cx={cx} cy={cy} r="10"
6156
+ fill={coreFill}
6157
+ data-topo-hub-core
6158
+ data-topo-hub-core-hovered={isCoreHovered ? 'true' : 'false'}
6159
+ data-topo-hub-core-fill={coreFill}
6160
+ style={{ transition: 'fill 200ms ease-out' }}
6161
+ />
6162
+ );
6163
+ })()}
5792
6164
  {/* R130 / Loop: when workingCount > 0, the decorative inner
5793
6165
  highlight gets replaced with the workingCount digit. The
5794
6166
  R84 busyness breath already encodes the same metric
@@ -6570,13 +6942,77 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6570
6942
  tion list ('fill,opacity' 300ms ease-out) unchanged.
6571
6943
  data-node-halo-offline-opacity attr exposes the
6572
6944
  resolved value for tests. */}
6945
+ {(() => {
6946
+ /* Round 440 / Loop: node halo opacity hover lift —
6947
+ lifts toward full on the matched node. Pure paint
6948
+ axis: rest values unchanged for un-hovered halos,
6949
+ hover state lifts the matched halo's alpha by
6950
+ +0.15 on each tier:
6951
+ online cyber 0.65 → 0.80
6952
+ online light 0.85 → 1.00 (capped)
6953
+ offline cyber 0.30 → 0.45
6954
+ offline light 0.45 → 0.60
6955
+ Same paint-only mental model as R430 hub-spoke
6956
+ opacity lift + R429 label-card body opacity lift,
6957
+ now at the per-node halo scope. No geometry
6958
+ change so R51 sentinels stay safe and the overlap-
6959
+ test invariant is unchanged (test runs at rest).
6960
+ Closes a chroma/presence axis on the per-node
6961
+ hover signature alongside the 12-layer cue stack
6962
+ (R26/R217/R142/R427/R428/R429 card + R430/R435/
6963
+ R436/R437/R94 link + R438 ring). R407 offline
6964
+ halo opacity floor (cyber 0.30 / light 0.45) is
6965
+ the rest branch unchanged. Existing transition-
6966
+ [fill,opacity] duration-300 className handles
6967
+ the easing. data-node-halo-hovered exposes the
6968
+ gate; data-node-halo-resolved-opacity exposes
6969
+ the four-state resolved value for tests. */
6970
+ const isHaloHovered = !reducedMotion && hoveredAlias === session.alias;
6971
+ /* Round 456 / Loop: light-theme offline node halo
6972
+ rest opacity 0.45 → 0.50. Stale-state legibility
6973
+ lift family extension (10th anchor) at the per-
6974
+ node halo light-theme scope:
6975
+ R317 subordinate-text gray-500 → gray-400
6976
+ R358 freshness floor 0.25 → 0.30
6977
+ R372 minimap offline-dot 0.5 → 0.6
6978
+ R404 hub-halo cyber trough 0.08 → 0.10
6979
+ R405 hub-halo light trough 0.32 → 0.34
6980
+ R406 edge freshness floor 0.35 → 0.40
6981
+ R407 node halo offline opacity
6982
+ cyber 0.25 → 0.30
6983
+ light 0.4 → 0.45
6984
+ R419 hub-spoke idle 0.45 → 0.50
6985
+ R452 dense alias rest 0.9 → 0.95
6986
+ R456 node halo offline LIGHT 0.45 → 0.50 ← this round
6987
+ Pre-R456 light-theme offline halo at 0.45 sat at
6988
+ the upper end of "near-floor" but read as soft-
6989
+ focus on the lighter canvas; +0.05 (~11 % opacity
6990
+ gain) lifts it to 0.50 — the midpoint between
6991
+ R407 rest 0.45 and R440 hover 0.60 — closing the
6992
+ gap so offline halos read more confidently as
6993
+ present-but-stale anchors. Cyber theme stays at
6994
+ R407's 0.30 (cyber backdrop is dark; the cyber
6995
+ offline halo against #080814 contains a stronger
6996
+ contrast envelope than light, so doesn't need
6997
+ the same lift). R440 hover 0.45→0.60 light + R12
6998
+ status.halo color + R407 transition list all
6999
+ preserved. */
7000
+ const haloOpacity = (() => {
7001
+ if (isOnline) {
7002
+ return isLight ? (isHaloHovered ? 1 : 0.85) : (isHaloHovered ? 0.80 : 0.65);
7003
+ }
7004
+ return isLight ? (isHaloHovered ? 0.60 : 0.50) : (isHaloHovered ? 0.45 : 0.30);
7005
+ })();
7006
+ return (
6573
7007
  <circle
6574
7008
  cx={pos.x}
6575
7009
  cy={pos.y}
6576
7010
  r={radius + 8}
6577
7011
  fill={status.halo}
6578
- opacity={isOnline ? (isLight ? 0.85 : 0.65) : (isLight ? 0.45 : 0.30)}
7012
+ opacity={haloOpacity}
6579
7013
  data-node-halo-offline-opacity={isOnline ? undefined : (isLight ? 0.45 : 0.30)}
7014
+ data-node-halo-hovered={isHaloHovered ? 'true' : 'false'}
7015
+ data-node-halo-resolved-opacity={haloOpacity}
6580
7016
  className="transition-[fill,opacity] duration-300 ease-out"
6581
7017
  data-node-halo-breath={!reducedMotion && session.status === 'working' ? 'on' : 'off'}
6582
7018
  data-node-halo-breath-offset={
@@ -6635,6 +7071,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6635
7071
  />
6636
7072
  )}
6637
7073
  </circle>
7074
+ );
7075
+ })()}
6638
7076
  {/* Round 111 / Loop: edge-endpoint emphasis ring. R49
6639
7077
  already keeps endpoint nodes at opacity 1 while
6640
7078
  others dim when an edge is hovered, but the
@@ -6691,11 +7129,37 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6691
7129
  ring stroke-width). data-edge-endpoint-ring-
6692
7130
  stroke-width attr surfaces the chosen value for
6693
7131
  test introspection. */
7132
+ /* Round 442 / Loop: endpoint emphasis ring radius
7133
+ hover lift — r=radius+7 → radius+8 on isEndpoint,
7134
+ closing a 3-axis hover-elevation parity at endpoint
7135
+ ring scope (r + sw + opacity):
7136
+ opacity R182 0 → 0.85/0.9
7137
+ sw R233 1.6 → 2.4
7138
+ r R442 +7 → +8 ← this round
7139
+ Mirrors the 3-axis trios already established at
7140
+ hub hover-ring (R177/R370/R385) and edge badge
7141
+ (R164/R394/R395). Pre-R442 the endpoint ring
7142
+ faded in + thickened on edge-hover but its radius
7143
+ stayed locked at radius+7 — only the paint/weight
7144
+ axes lifted while the GEOMETRY stayed unchanged.
7145
+ +1px (~radius+7 to radius+8) gives a subtle outward
7146
+ pulse on hover without crowding the status ring
7147
+ (which sits at radius from R438 sw3.5 hover) or
7148
+ the halo (radius+8 from R440 opacity hover — the
7149
+ endpoint ring sits at the SAME radius as the halo
7150
+ but with stroke=cyan vs fill=status.halo so they
7151
+ don't visually collide). The transition list
7152
+ extends to include 'r 180ms ease-out' so the new
7153
+ axis eases under the same R233 cadence. SVG `r`
7154
+ on a <circle> uses CSS-property syntax for inter-
7155
+ polation (same idiom R197/R198 used on the
7156
+ legend swatch). data-edge-endpoint-ring-radius
7157
+ attr exposes the resolved value for tests. */
7158
+ const endpointR = isEndpoint ? radius + 8 : radius + 7;
6694
7159
  return (
6695
7160
  <circle
6696
7161
  cx={pos.x}
6697
7162
  cy={pos.y}
6698
- r={radius + 7}
6699
7163
  fill="none"
6700
7164
  stroke={pal.flowEdge}
6701
7165
  strokeWidth={isEndpoint ? 2.4 : 1.6}
@@ -6703,7 +7167,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6703
7167
  data-edge-endpoint-ring
6704
7168
  data-edge-endpoint-active={isEndpoint ? 'true' : 'false'}
6705
7169
  data-edge-endpoint-ring-stroke-width={isEndpoint ? 2.4 : 1.6}
6706
- style={{ pointerEvents: 'none', transition: 'opacity 180ms ease-out, stroke-width 180ms ease-out' }}
7170
+ data-edge-endpoint-ring-radius={endpointR}
7171
+ style={{
7172
+ pointerEvents: 'none',
7173
+ r: `${endpointR}px`,
7174
+ transition: 'opacity 180ms ease-out, stroke-width 180ms ease-out, r 180ms ease-out',
7175
+ } as React.CSSProperties}
6707
7176
  />
6708
7177
  );
6709
7178
  })()}
@@ -6963,8 +7432,37 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6963
7432
  transition: 'r 150ms ease-out, stroke-width 150ms ease-out',
6964
7433
  } as React.CSSProperties}
6965
7434
  />
7435
+ {/* Round 443 / Loop: runtime badge inner-icon
7436
+ strokeWidth lift on node hover — 2.4 → 2.8 on
7437
+ isNodeActive. Pre-R443 the outer badge ring
7438
+ lifted (R208 r + sw both grow on hover) but
7439
+ the inner icon path stayed locked at sw=2.4.
7440
+ The two layers of the runtime badge were
7441
+ out of phase: ring thickened, icon stayed
7442
+ thin. R443 closes the 2-axis hover signature
7443
+ on the badge so both ring and icon lift
7444
+ together. +0.4 absolute delta matches the
7445
+ R208 ring's +0.5 sw delta (badge ring 1.5 →
7446
+ 2.0 absolute), proportional to the icon's
7447
+ heavier base of 2.4. Pure paint axis;
7448
+ strokeLinecap='round' + strokeLinejoin='round'
7449
+ preserved. transition list extends to include
7450
+ 'stroke-width 150ms ease-out' matching R208
7451
+ outer-ring cadence. data-runtime-badge-icon
7452
+ + -active attrs exposed for tests. */}
6966
7453
  <g transform={`translate(${bx - icon / 2} ${by - icon / 2}) scale(${icon / 24})`}>
6967
- <path d={rt.iconPath} fill="none" stroke={rt.color} strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
7454
+ <path
7455
+ d={rt.iconPath}
7456
+ fill="none"
7457
+ stroke={rt.color}
7458
+ strokeWidth={isNodeActive ? '2.8' : '2.4'}
7459
+ strokeLinecap="round"
7460
+ strokeLinejoin="round"
7461
+ data-runtime-badge-icon={session.alias}
7462
+ data-runtime-badge-icon-active={isNodeActive ? 'true' : 'false'}
7463
+ data-runtime-badge-icon-stroke-width={isNodeActive ? '2.8' : '2.4'}
7464
+ style={{ transition: 'stroke-width 150ms ease-out' }}
7465
+ />
6968
7466
  </g>
6969
7467
  </g>
6970
7468
  );
@@ -7269,12 +7767,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7269
7767
  hover scope. R211 fill 300ms transition
7270
7768
  preserved (additive letter-spacing branch
7271
7769
  + appended 'letter-spacing 200ms ease-out'). */}
7770
+ {/* Round 448 / Loop: node sub-text fontWeight
7771
+ 400 → 500 (font-medium). Sibling to R363
7772
+ (recent-row text fw 400→500) + R364 (legend-
7773
+ row label fw 400→500) — same "small mono
7774
+ text at fontSize=9-11 needs 500-tier weight
7775
+ for legibility" pattern, now applied to the
7776
+ per-node sub-text line. At fontSize=8-9
7777
+ monospace against the label-card chrome
7778
+ (pal.labelBox.fill cyber #020617 / light
7779
+ #ffffff), the default fw=400 sits at the
7780
+ legibility floor; fw=500 (font-medium) lifts
7781
+ it into a clearly readable band without
7782
+ changing geometry. R211 fill 300ms +
7783
+ R428 letter-spacing 0→0.2 hover + R427
7784
+ alias-text + R429 body opacity all preserved.
7785
+ Pure typography lift; no layout shift; the
7786
+ alias-text fw=700 (R427) still wins so the
7787
+ alias > status hierarchy holds at the type
7788
+ level. data-node-sub-text-font-weight attr
7789
+ exposes the value for tests. */}
7272
7790
  <text
7273
7791
  x="0" y={subY} textAnchor="middle"
7274
7792
  fill={status.primary}
7275
7793
  fontSize={subFs} fontFamily="monospace"
7794
+ fontWeight="500"
7276
7795
  data-node-sub-text={session.alias}
7277
7796
  data-node-sub-text-hovered={hoveredAlias === session.alias ? 'true' : 'false'}
7797
+ data-node-sub-text-font-weight="500"
7278
7798
  style={{
7279
7799
  transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',
7280
7800
  letterSpacing: hoveredAlias === session.alias ? '0.2px' : '0px',
@@ -7301,6 +7821,26 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7301
7821
  // CSS pseudo-class; only the transition-property
7302
7822
  // moves to inline. Big fleets benefit most — this is
7303
7823
  // the path users see when their dashboard is busiest.
7824
+ /* Round 452 / Loop: dense plain-text alias rest
7825
+ opacity 0.9 → 0.95. Closes the alpha gap on the
7826
+ dense fleet's per-node label, sibling to R449
7827
+ legend-count-active 0.95→1.0 and R450 minimap
7828
+ viewport rest 0.9→0.95 — same "close the
7829
+ active-presence alpha gap" idiom applied here
7830
+ to the dense-mode alias text at fontSize=9-10
7831
+ monospace. Pre-R452 dense aliases at α=0.9 sat
7832
+ just below full alpha; for un-hovered nodes in
7833
+ a busy >16-node fleet this is the only label
7834
+ readable, so the 10% alpha gap added a subtle
7835
+ "soft-focused chrome" feel where the labels
7836
+ should read as definitive. +0.05 lift makes
7837
+ them confidently present without erasing the
7838
+ status.text + R110 stroke halo + paintOrder
7839
+ layering. R26 group-hover translate + R212
7840
+ fill 300ms transition + R110 stroke=container-
7841
+ Bg halo all preserved. data-node-dense-alias-
7842
+ text-opacity attr exposes the resolved value
7843
+ for tests. */
7304
7844
  <text
7305
7845
  x={pos.x}
7306
7846
  y={pos.y + radius + denseDrop}
@@ -7309,9 +7849,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7309
7849
  fontSize={denseFs}
7310
7850
  fontFamily="monospace"
7311
7851
  fontWeight="700"
7312
- opacity={0.9}
7852
+ opacity={0.95}
7313
7853
  className="group-hover:-translate-y-[1.5px]"
7314
7854
  data-node-dense-alias-text={session.alias}
7855
+ data-node-dense-alias-text-opacity="0.95"
7315
7856
  style={{
7316
7857
  pointerEvents: 'none',
7317
7858
  paintOrder: 'stroke',
@@ -8333,13 +8874,37 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8333
8874
  R383 recent-row pip radius 1.8 → 2.0 (this round)
8334
8875
  data-recent-row-freshness-radius attr
8335
8876
  bumps to '2.0' for tests. */
8336
- r={2.0}
8877
+ /* Round 447 / Loop: recent-row freshness pip
8878
+ radius lift on (isRowHovered || isRowPinned)
8879
+ — r 2.0 → 2.5 (+0.5px, sibling to R442
8880
+ endpoint-ring r lift). Adds a geometric
8881
+ axis to the recent-row hover/pin gesture
8882
+ alongside R143 translateY + R104 row bg-
8883
+ tint + R434 letter-spacing + R445 count
8884
+ fw. Pre-R447 the pip stayed at r=2.0 always
8885
+ — the freshness alpha (R162) tracked
8886
+ recency but didn't telegraph "this row is
8887
+ in focus" geometrically. R447 lifts the
8888
+ pip outward by 25% area (π·2.5² / π·2.0²
8889
+ = 1.56) on attention, closing a 5-axis
8890
+ row-attention signature (geometry + paint
8891
+ + typography + spacing + position).
8892
+ SVG `r` as CSS property for interpolation
8893
+ (R197/R198 idiom). transition list extends
8894
+ to include 'r 200ms ease-out' matching the
8895
+ opacity cadence. data-recent-row-freshness-
8896
+ lifted attr exposes the gate for tests. */
8337
8897
  fill={pal.legendAccent}
8338
8898
  opacity={alpha}
8339
8899
  data-recent-row-freshness={link.key}
8340
8900
  data-recent-row-freshness-alpha={alpha.toFixed(2)}
8341
- data-recent-row-freshness-radius="2.0"
8342
- style={{ pointerEvents: 'none', transition: 'opacity 200ms ease-out' }}
8901
+ data-recent-row-freshness-radius={(isRowHovered || isRowPinned) ? 2.5 : 2.0}
8902
+ data-recent-row-freshness-lifted={(isRowHovered || isRowPinned) ? 'true' : 'false'}
8903
+ style={{
8904
+ pointerEvents: 'none',
8905
+ r: `${(isRowHovered || isRowPinned) ? 2.5 : 2.0}px`,
8906
+ transition: 'opacity 200ms ease-out, r 200ms ease-out',
8907
+ } as React.CSSProperties}
8343
8908
  />
8344
8909
  );
8345
8910
  })()}
@@ -8486,13 +9051,36 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8486
9051
  distinct, plus the fill flip from legendText
8487
9052
  → amber carries the dramatic part of the cue).
8488
9053
  Sibling treatment in the data-weight tier. */}
9054
+ {/* Round 445 / Loop: extend the R320 cold/hot fw
9055
+ binary (600/700) to ALSO fire on isRowPinned —
9056
+ pinned-cold now lifts to 700 alongside the
9057
+ existing hot-triggered lift. Sibling to R444
9058
+ group-label-count-pin (500→600) at the
9059
+ recent-row scope. Both panel-row counts now
9060
+ respond to pin with a typographic weight lift,
9061
+ part of the "data tightens under attention"
9062
+ family (R416/R424/R425/R426/R444/R445).
9063
+ Effective tiers:
9064
+ cold + un-pinned → fw 600
9065
+ cold + pinned → fw 700 ← this round
9066
+ hot (any pin state) → fw 700 (R320 preserved)
9067
+ hot is still amber-filled (R127); cold pin
9068
+ stays at the parent fill, so the two routes
9069
+ to fw=700 are visually distinct (color vs
9070
+ no color). transition list adds 'font-
9071
+ weight 200ms ease-out' so the lift eases
9072
+ under the same R320 fill cadence. data-
9073
+ recent-row-count-pinned attr exposes the
9074
+ pin gate for tests. */}
8489
9075
  <tspan
8490
9076
  fill={isHot ? hotStroke : undefined}
8491
- fontWeight={isHot ? '700' : '600'}
9077
+ fontWeight={(isHot || isRowPinned) ? '700' : '600'}
8492
9078
  data-recent-row-count
9079
+ data-recent-row-count-pinned={isRowPinned ? 'true' : 'false'}
9080
+ data-recent-row-count-font-weight={(isHot || isRowPinned) ? '700' : '600'}
8493
9081
  {...(isHot ? { 'data-recent-row-count-hot': 'true' } : {})}
8494
9082
  style={{
8495
- transition: 'fill 300ms ease-out',
9083
+ transition: 'fill 300ms ease-out, font-weight 200ms ease-out',
8496
9084
  fontVariantNumeric: 'tabular-nums',
8497
9085
  }}
8498
9086
  >
@@ -9318,20 +9906,60 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9318
9906
  labels to single status words, the count
9319
9907
  becomes proportionally more important; this
9320
9908
  round emphasizes that role typographically. */}
9909
+ {/* Round 446 / Loop: legend per-row count fontWeight
9910
+ lift 600 → 700 on isPinned. Mirror of R444 group-
9911
+ label-count + R445 recent-row-count at the
9912
+ legend-row scope. Closes the 3-panel-row family
9913
+ for the "data tightens under attention" pattern —
9914
+ every panel-row count now responds to pin with a
9915
+ typographic-weight bump:
9916
+ R444 group-label-count 500 → 600
9917
+ R445 recent-row-count 600 → 700 (cold-pin route)
9918
+ R446 legend-row-count 600 → 700 ← this round
9919
+ Hover gate (hoveredStatus===row.key) keeps rest
9920
+ fw=600 so the locked-vs-preview distinction at
9921
+ the type level stays intact — same gate R433 used
9922
+ on the parent <text> letter-spacing tween. R309
9923
+ fw=600 baseline + R204 empty-row opacity dim +
9924
+ R225 tabular-nums all preserved. transition list
9925
+ extends to include 'font-weight 150ms ease-out'
9926
+ matching R433 fill/letter-spacing cadence.
9927
+ data-legend-count-pinned + -font-weight attrs
9928
+ exposed for tests. */}
9321
9929
  <text
9322
9930
  x="215" y={row.y1}
9323
9931
  textAnchor="end"
9324
9932
  fill={row.count > 0 && (hoveredStatus === row.key || isPinned) ? row.fill : pal.legendText}
9325
9933
  fontSize="11"
9326
9934
  fontFamily="monospace"
9327
- fontWeight="600"
9935
+ fontWeight={isPinned ? '700' : '600'}
9936
+ /* Round 449 / Loop: legend-row count active-state
9937
+ opacity 0.95 → 1.0 on (hoveredStatus===row.key
9938
+ || isPinned). Pre-R449 R204 lifted populated-row
9939
+ active opacity from rest 0.65 to 0.95 — visibly
9940
+ brighter but kept a 5 pct alpha gap (1 - 0.95).
9941
+ R449 closes the gap to 1.0 so the active count
9942
+ reads as confidently present alongside the R446
9943
+ fw=600→700 + R433 letter-spacing tween. Theme-
9944
+ consistency / canvas-presence family extension
9945
+ (7th anchor on the active-presence lift sub-
9946
+ family): R370 hub hover-ring 0.7→0.8, R371 edge-
9947
+ badge rest 0.82→0.85, R372 minimap offline-dot
9948
+ 0.5→0.6, R386 hub-highlight idle 0.9→0.95, R387
9949
+ hover-detail panel 0.94→0.97, R429 label-card
9950
+ body 0.94→1.0, R449 legend-count active 0.95→1.0
9951
+ ← this round. Empty-row opacity (R204: 0.28
9952
+ light / 0.30 cyber) and idle 0.65 rest both
9953
+ preserved. */
9328
9954
  opacity={row.count === 0
9329
9955
  ? (isLight ? 0.28 : 0.30)
9330
- : (hoveredStatus === row.key || isPinned ? 0.95 : 0.65)}
9956
+ : (hoveredStatus === row.key || isPinned ? 1 : 0.65)}
9331
9957
  data-legend-count={row.key}
9332
9958
  data-legend-count-empty={row.count === 0 ? 'true' : 'false'}
9959
+ data-legend-count-pinned={isPinned ? 'true' : 'false'}
9960
+ data-legend-count-font-weight={isPinned ? '700' : '600'}
9333
9961
  data-legend-count-fill={row.count > 0 && (hoveredStatus === row.key || isPinned) ? 'tier' : 'neutral'}
9334
- style={{ pointerEvents: 'none', transition: 'opacity 150ms ease-out, fill 150ms ease-out', fontVariantNumeric: 'tabular-nums' }}
9962
+ style={{ pointerEvents: 'none', transition: 'opacity 150ms ease-out, fill 150ms ease-out, font-weight 150ms ease-out', fontVariantNumeric: 'tabular-nums' }}
9335
9963
  >{row.count}</text>
9336
9964
  </g>
9337
9965
  );
@@ -9718,7 +10346,29 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9718
10346
  // R346: strokeWidth + opacity tween on container hover.
9719
10347
  strokeWidth={hoveredMinimap ? '1.75' : '1.5'}
9720
10348
  strokeLinejoin="round"
9721
- opacity={hoveredMinimap ? '1' : '0.9'}
10349
+ /* Round 450 / Loop · milestone: minimap viewport rest
10350
+ opacity 0.9 → 0.95. Closes half the alpha gap on
10351
+ the wayfinding indicator while preserving the
10352
+ R346 hover delta to 1.0. Pre-R450 the rest viewport
10353
+ sat at 0.9 (10 pct alpha gap) — adequate but
10354
+ under-confident for the user's primary "you are
10355
+ here" indicator on the minimap. R450 lifts to 0.95
10356
+ so the rest read is more present without erasing
10357
+ the hover lift cue (the +0.05 rest-to-hover delta
10358
+ is small but pairs with R346 sw 1.5→1.75 to keep
10359
+ hover clearly distinguishable). Sibling to R449
10360
+ legend-count active opacity 0.95→1.0 — same
10361
+ "close the active-presence alpha gap" idiom now
10362
+ applied to the REST tier of the wayfinding rect
10363
+ (the minimap viewport stays at canvas-presence
10364
+ register even when un-hovered since it's the
10365
+ spatial referent). Theme-consistency / canvas-
10366
+ presence family (8th anchor on the active-
10367
+ presence lift sub-arc).
10368
+ R287 strokeWidth=1.5 + R379 strokeLinejoin='round'
10369
+ + R346 hover-state tweens + R393 rx=2 + R199
10370
+ smoothView x/y/w/h transition all preserved. */
10371
+ opacity={hoveredMinimap ? '1' : '0.95'}
9722
10372
  data-topo-minimap-viewport
9723
10373
  data-topo-minimap-viewport-rx="2"
9724
10374
  data-topo-minimap-viewport-smooth={smoothView ? 'true' : 'false'}
@@ -9921,11 +10571,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9921
10571
  its own compositor layer for crisper edges during
9922
10572
  the scale tween. Sibling change on zoom-in icon
9923
10573
  below. */}
10574
+ {/* Round 454 / Loop: extend R453 chrome reset icon hover
10575
+ sw lift to zoom +/− icons via Tailwind arbitrary class
10576
+ group-hover:[stroke-width:2.8]. Chrome icon hover sw
10577
+ lift family now 5 anchors:
10578
+ R208 runtime badge outer ring 1.5 → 2
10579
+ R443 runtime badge inner icon 2.4 → 2.8
10580
+ R453 chrome reset icon 2.5 → 2.8
10581
+ R454 chrome zoom-out icon 2.5 → 2.8 ← this round
10582
+ R454 chrome zoom-in icon 2.5 → 2.8 ← this round
10583
+ Tailwind v4 arbitrary-value group-hover variant
10584
+ resolves [stroke-width:2.8] as a CSS property which
10585
+ overrides the static strokeWidth='2.5' attribute on
10586
+ hover. transition-[stroke-width] appended to the
10587
+ existing transition-transform list so the sw tween
10588
+ eases under the same 200ms cadence as R352 group-
10589
+ hover:scale-110. R186 anet-chrome-pop keyframe still
10590
+ owns transform during click via CSS-animation
10591
+ precedence over transition-transform. Sibling change
10592
+ on zoom-in icon below. */}
9924
10593
  <svg
9925
10594
  width="12" height="12" viewBox="0 0 24 24"
9926
10595
  fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
9927
10596
  aria-hidden
9928
- className={`transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu${chromePopping === 'zoom-out' ? ' anet-chrome-pop' : ''}`}
10597
+ className={`transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu${chromePopping === 'zoom-out' ? ' anet-chrome-pop' : ''}`}
9929
10598
  data-topo-chrome-zoom-out-icon
9930
10599
  ><path d="M5 12h14" /></svg>
9931
10600
  </button>
@@ -10030,11 +10699,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10030
10699
  {/* R352 sibling — zoom-in icon picks up the same
10031
10700
  group-hover:scale-110 family. Mirror change at
10032
10701
  the zoom-out icon above. */}
10702
+ {/* R454 sibling — zoom-in icon picks up the same
10703
+ group-hover:[stroke-width:2.8] family lift. */}
10033
10704
  <svg
10034
10705
  width="12" height="12" viewBox="0 0 24 24"
10035
10706
  fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
10036
10707
  aria-hidden
10037
- className={`transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu${chromePopping === 'zoom-in' ? ' anet-chrome-pop' : ''}`}
10708
+ className={`transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu${chromePopping === 'zoom-in' ? ' anet-chrome-pop' : ''}`}
10038
10709
  data-topo-chrome-zoom-in-icon
10039
10710
  ><path d="M12 5v14M5 12h14" /></svg>
10040
10711
  </button>
@@ -10091,13 +10762,28 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10091
10762
  (2.5) all share one weight. View-box (24×24) and
10092
10763
  display size (13×13) unchanged, so geometry stays
10093
10764
  pixel-stable — only the stroke deepens. */}
10765
+ {/* Round 453 / Loop: chrome reset icon strokeWidth hover
10766
+ lift — 2.5 → 2.8 on hoveredReset && !resetSpinning.
10767
+ Sibling to R443 runtime badge inner-icon sw lift
10768
+ (2.4→2.8) — both chrome icons now thicken on hover
10769
+ for tactile feedback. Pre-R453 reset hover was a
10770
+ rotate-only cue (R350); R453 adds a stroke-weight
10771
+ axis so the affordance reads with both motion (R350
10772
+ rotate -8°) AND geometry (R453 sw +0.3). Gated on
10773
+ !resetSpinning so the R184 spin keyframe owns paint
10774
+ during its 450ms run. 200ms stroke-width transition
10775
+ appended to the style list matches R350 transform
10776
+ cadence. data-topo-chrome-reset-icon-stroke-width
10777
+ attr exposes the resolved value for tests. */}
10094
10778
  <svg
10095
10779
  width="13" height="13" viewBox="0 0 24 24"
10096
- fill="none" stroke="currentColor" strokeWidth="2.5"
10780
+ fill="none" stroke="currentColor"
10781
+ strokeWidth={hoveredReset && !resetSpinning ? '2.8' : '2.5'}
10097
10782
  strokeLinecap="round" strokeLinejoin="round"
10098
10783
  aria-hidden
10099
10784
  className={resetSpinning ? 'anet-reset-spin' : undefined}
10100
10785
  data-topo-chrome-reset-icon
10786
+ data-topo-chrome-reset-icon-stroke-width={hoveredReset && !resetSpinning ? '2.8' : '2.5'}
10101
10787
  // R350: hover-rotate preview of the R184 click-spin.
10102
10788
  // Gated on !resetSpinning so the anet-reset-spin keyframe
10103
10789
  // owns transform during its 450ms run. transformOrigin
@@ -10106,7 +10792,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10106
10792
  style={{
10107
10793
  transform: hoveredReset && !resetSpinning ? 'rotate(-8deg)' : 'rotate(0deg)',
10108
10794
  transformOrigin: 'center',
10109
- transition: 'transform 200ms ease-out',
10795
+ transition: 'transform 200ms ease-out, stroke-width 200ms ease-out',
10110
10796
  }}
10111
10797
  data-topo-chrome-reset-icon-hover={hoveredReset && !resetSpinning ? 'true' : 'false'}
10112
10798
  >
@@ -10176,12 +10862,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10176
10862
  gpu hint promotes the svg to its own compositor layer
10177
10863
  for crisper edges during the scale tween. Closes the
10178
10864
  chrome-strip per-icon hover-affordance arc. */}
10865
+ {/* R455 — fullscreen ENTER + EXIT icons pick up the same
10866
+ group-hover:[stroke-width:2.8] family lift as the
10867
+ zoom +/− icons (R454) and chrome reset icon (R453).
10868
+ Chrome icon hover sw lift family now 6 anchors —
10869
+ R208/R443 runtime badge + R453/R454-zoom-out/zoom-in
10870
+ + R455 fullscreen (this round). transition-[transform,
10871
+ stroke-width] expands existing transition-transform
10872
+ so the sw lift eases under R352 scale-110 cadence. */}
10179
10873
  {isFullscreen ? (
10180
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu" data-topo-chrome-fullscreen-icon="exit">
10874
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu" data-topo-chrome-fullscreen-icon="exit">
10181
10875
  <path d="M8 3v4a1 1 0 0 1-1 1H3M21 8h-4a1 1 0 0 1-1-1V3M3 16h4a1 1 0 0 1 1 1v4M16 21v-4a1 1 0 0 1 1-1h4" />
10182
10876
  </svg>
10183
10877
  ) : (
10184
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu" data-topo-chrome-fullscreen-icon="enter">
10878
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu" data-topo-chrome-fullscreen-icon="enter">
10185
10879
  <path d="M3 8V5a2 2 0 0 1 2-2h3M21 8V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3M21 16v3a2 2 0 0 1-2 2h-3" />
10186
10880
  </svg>
10187
10881
  )}