@sleep2agi/agent-network-dashboard 0.4.5-preview.1 → 0.4.5-preview.11

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 (204) 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 +37 -37
  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.js.nft.json +1 -1
  57. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  58. package/.next/server/app/messages.html +2 -2
  59. package/.next/server/app/messages.rsc +14 -14
  60. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  61. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  62. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  63. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  64. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  65. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  66. package/.next/server/app/node/page.js.nft.json +1 -1
  67. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  68. package/.next/server/app/node.html +2 -2
  69. package/.next/server/app/node.rsc +14 -14
  70. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  71. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  72. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  73. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  74. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  75. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  76. package/.next/server/app/nodes/page.js.nft.json +1 -1
  77. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  78. package/.next/server/app/nodes.html +2 -2
  79. package/.next/server/app/nodes.rsc +14 -14
  80. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  81. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  82. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  83. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  84. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  85. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  86. package/.next/server/app/page.js.nft.json +1 -1
  87. package/.next/server/app/page_client-reference-manifest.js +1 -1
  88. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  89. package/.next/server/app/server-logs.html +2 -2
  90. package/.next/server/app/server-logs.rsc +14 -14
  91. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  92. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  93. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  94. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  95. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  96. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  97. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  98. package/.next/server/app/settings/networks.html +2 -2
  99. package/.next/server/app/settings/networks.rsc +14 -14
  100. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  101. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  102. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  103. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  104. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  105. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  106. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  107. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  108. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  109. package/.next/server/app/settings/tokens.html +2 -2
  110. package/.next/server/app/settings/tokens.rsc +14 -14
  111. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  112. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  113. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  114. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  115. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  116. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  117. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  118. package/.next/server/app/settings.html +2 -2
  119. package/.next/server/app/settings.rsc +14 -14
  120. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  121. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  122. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  123. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  124. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  125. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  126. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  127. package/.next/server/app/tasks/page.js.nft.json +1 -1
  128. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  129. package/.next/server/app/tasks.html +2 -2
  130. package/.next/server/app/tasks.rsc +14 -14
  131. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  132. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  133. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  134. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  135. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  136. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  137. package/.next/server/chunks/ssr/[root-of-the-server]__0epcm6p._.js +3 -0
  138. package/.next/server/chunks/ssr/[root-of-the-server]__0epcm6p._.js.map +1 -0
  139. package/.next/server/chunks/ssr/[root-of-the-server]__0to~xqw._.js +3 -0
  140. package/.next/server/chunks/ssr/[root-of-the-server]__0to~xqw._.js.map +1 -0
  141. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js +1 -1
  142. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js.map +1 -1
  143. package/.next/server/chunks/ssr/[root-of-the-server]__0xo8x6b._.js +3 -0
  144. package/.next/server/chunks/ssr/[root-of-the-server]__0xo8x6b._.js.map +1 -0
  145. package/.next/server/chunks/ssr/_13_v7-3._.js +3 -0
  146. package/.next/server/chunks/ssr/_13_v7-3._.js.map +1 -0
  147. package/.next/server/chunks/ssr/app_10b5blc._.js +9 -0
  148. package/.next/server/chunks/ssr/app_10b5blc._.js.map +1 -0
  149. package/.next/server/chunks/ssr/app_components_08ys5y3._.js +2 -2
  150. package/.next/server/chunks/ssr/app_components_08ys5y3._.js.map +1 -1
  151. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js +1 -1
  152. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js.map +1 -1
  153. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js +1 -1
  154. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js.map +1 -1
  155. package/.next/server/middleware-build-manifest.js +3 -3
  156. package/.next/server/pages/404.html +2 -2
  157. package/.next/server/pages/500.html +1 -1
  158. package/.next/server/server-reference-manifest.js +1 -1
  159. package/.next/server/server-reference-manifest.json +1 -1
  160. package/.next/static/chunks/03wwuo3wor.05.js +1 -0
  161. package/.next/static/chunks/09xm65~ucg19e.js +1 -0
  162. package/.next/static/chunks/0bbx5m_hcs39u.js +1 -0
  163. package/.next/static/chunks/0f0tgtns1m6zh.js +1 -0
  164. package/.next/static/chunks/103qydx_2k3a8.js +7 -0
  165. package/.next/static/chunks/12dcb86elsl-e.js +7 -0
  166. package/.next/static/chunks/12xk2ohuxsyo5.js +1 -0
  167. package/.next/static/chunks/132e__998_q9r.js +1 -0
  168. package/.next/static/chunks/15_cmg2jjx1aj.css +2 -0
  169. package/.next/trace +2 -2
  170. package/.next/trace-build +1 -1
  171. package/app/components/AgentCard.tsx +6 -2
  172. package/app/components/AliasAvatar.tsx +51 -0
  173. package/app/components/CommandPalette.tsx +83 -8
  174. package/app/components/Sidebar.tsx +40 -35
  175. package/app/components/TaskChatPanel.tsx +7 -5
  176. package/app/components/TopoGraph.tsx +16 -3
  177. package/app/globals.css +104 -0
  178. package/app/messages/page.tsx +79 -48
  179. package/app/nodes/page.tsx +21 -23
  180. package/app/page.tsx +9 -6
  181. package/app/tasks/page.tsx +100 -37
  182. package/package.json +1 -1
  183. package/.next/server/chunks/ssr/[root-of-the-server]__0_jnflc._.js +0 -3
  184. package/.next/server/chunks/ssr/[root-of-the-server]__0_jnflc._.js.map +0 -1
  185. package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js +0 -3
  186. package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js.map +0 -1
  187. package/.next/server/chunks/ssr/[root-of-the-server]__12mpoig._.js +0 -3
  188. package/.next/server/chunks/ssr/[root-of-the-server]__12mpoig._.js.map +0 -1
  189. package/.next/server/chunks/ssr/_0d-fuox._.js +0 -3
  190. package/.next/server/chunks/ssr/_0d-fuox._.js.map +0 -1
  191. package/.next/server/chunks/ssr/app_0ff~kjj._.js +0 -9
  192. package/.next/server/chunks/ssr/app_0ff~kjj._.js.map +0 -1
  193. package/.next/static/chunks/06t_vzei8s9_h.js +0 -1
  194. package/.next/static/chunks/07okhs5561y5y.js +0 -1
  195. package/.next/static/chunks/0a1wbl-spby3_.css +0 -2
  196. package/.next/static/chunks/0gs9-~h7-pkuu.js +0 -7
  197. package/.next/static/chunks/0j3s_1ee66awv.js +0 -1
  198. package/.next/static/chunks/0khpyxpdrp~ty.js +0 -1
  199. package/.next/static/chunks/0lo_g.q1fm-hr.js +0 -7
  200. package/.next/static/chunks/0ogeals5ufhho.js +0 -1
  201. package/.next/static/chunks/1031v6nw33ud3.js +0 -1
  202. /package/.next/static/{wcTnZXyx_R19_Ur1_cGMP → GRpiaBz0BNf38IvCL9T47}/_buildManifest.js +0 -0
  203. /package/.next/static/{wcTnZXyx_R19_Ur1_cGMP → GRpiaBz0BNf38IvCL9T47}/_clientMiddlewareManifest.js +0 -0
  204. /package/.next/static/{wcTnZXyx_R19_Ur1_cGMP → GRpiaBz0BNf38IvCL9T47}/_ssgManifest.js +0 -0
@@ -51,11 +51,12 @@ export function Sidebar() {
51
51
  href={item.href}
52
52
  prefetch={false}
53
53
  onClick={() => setMobileOpen(false)}
54
+ title={collapsed ? item.label : undefined}
54
55
  className={`relative flex items-center gap-3 px-3 py-3 rounded-lg text-sm transition-colors active:bg-[#1a1a3a] ${
55
56
  isActive(item.href)
56
57
  ? 'bg-cyan-500/10 text-cyan-300 border border-cyan-500/20 anet-nav-active'
57
58
  : 'text-gray-400 hover:text-gray-200 hover:bg-[#1a1a2a]'
58
- } ${collapsed ? 'justify-center' : ''}`}
59
+ } ${collapsed ? 'justify-center px-0' : ''}`}
59
60
  >
60
61
  <svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
61
62
  <path strokeLinecap="round" strokeLinejoin="round" d={item.icon} />
@@ -134,45 +135,49 @@ export function Sidebar() {
134
135
  {nav}
135
136
  </div>
136
137
 
137
- {/* Sign out + collapse */}
138
+ {/* Sign out + collapse — round 27: collapsed-state gets icon-only
139
+ variants so users still have Sign out / Quick search access at
140
+ 56px width, plus title= tooltips. */}
138
141
  <div className="absolute bottom-0 left-0 right-0 border-t border-[#2a2a4a] bg-[#0d0d1a]">
139
- {!collapsed && (
140
- <button
141
- onClick={async () => {
142
- await fetch('/api/auth/logout', { method: 'POST' }).catch(() => {});
143
- sessionStorage.removeItem('anet_v3_auth');
144
- window.location.assign('/login');
145
- }}
146
- className="w-full flex items-center gap-3 px-5 py-3 text-xs text-gray-500 hover:text-red-400 hover:bg-red-500/5 transition-colors"
147
- >
148
- <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
149
- <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
142
+ <button
143
+ onClick={() => {
144
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true, ctrlKey: true }));
145
+ }}
146
+ title={collapsed ? 'Quick search (⌘K)' : undefined}
147
+ className={`w-full flex items-center text-[11px] text-gray-600 hover:text-gray-400 hover:bg-[#1a1a2a] transition-colors ${
148
+ collapsed ? 'justify-center px-0 py-2.5' : 'justify-between gap-2 px-5 py-2'
149
+ }`}
150
+ aria-label="Open command palette"
151
+ >
152
+ <span className={`flex items-center ${collapsed ? '' : 'gap-2'}`}>
153
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
154
+ <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
150
155
  </svg>
151
- Sign out
152
- </button>
153
- )}
154
- {!collapsed && (
155
- <button
156
- onClick={() => {
157
- // Dispatch a synthetic keypress so the global Cmd+K listener fires
158
- window.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true, ctrlKey: true }));
159
- }}
160
- className="w-full flex items-center justify-between gap-2 px-5 py-2 text-[11px] text-gray-600 hover:text-gray-400 hover:bg-[#1a1a2a] transition-colors"
161
- aria-label="Open command palette"
162
- >
163
- <span className="flex items-center gap-2">
164
- <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
165
- <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
166
- </svg>
167
- Quick search
168
- </span>
169
- <kbd className="text-[10px] border border-current rounded px-1 py-0.5 opacity-60 font-mono">⌘K</kbd>
170
- </button>
171
- )}
172
- <div className={`flex items-center gap-2 px-3 py-2 ${collapsed ? 'justify-center' : 'justify-between'}`}>
156
+ {!collapsed && 'Quick search'}
157
+ </span>
158
+ {!collapsed && <kbd className="text-[10px] border border-current rounded px-1 py-0.5 opacity-60 font-mono">⌘K</kbd>}
159
+ </button>
160
+ <button
161
+ onClick={async () => {
162
+ await fetch('/api/auth/logout', { method: 'POST' }).catch(() => {});
163
+ sessionStorage.removeItem('anet_v3_auth');
164
+ window.location.assign('/login');
165
+ }}
166
+ title={collapsed ? 'Sign out' : undefined}
167
+ className={`w-full flex items-center text-xs text-gray-500 hover:text-red-400 hover:bg-red-500/5 transition-colors ${
168
+ collapsed ? 'justify-center px-0 py-2.5' : 'gap-3 px-5 py-3'
169
+ }`}
170
+ >
171
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
172
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
173
+ </svg>
174
+ {!collapsed && 'Sign out'}
175
+ </button>
176
+ <div className={`flex items-center gap-2 ${collapsed ? 'flex-col px-0 py-2' : 'px-3 py-2 justify-between'}`}>
173
177
  <ThemeSwitcher compact={collapsed} />
174
178
  <button
175
179
  onClick={() => setCollapsed(!collapsed)}
180
+ title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
176
181
  className="hidden lg:flex p-1.5 rounded text-gray-600 hover:text-gray-400 transition-colors"
177
182
  aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
178
183
  >
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useState, useEffect, useRef, useCallback } from 'react';
4
4
  import { timeAgo } from './utils';
5
+ import { AliasAvatar } from './AliasAvatar';
5
6
 
6
7
  interface ChatTask {
7
8
  task_id: string;
@@ -386,7 +387,10 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
386
387
  {m.result && (
387
388
  <div className="flex justify-start">
388
389
  <div className="max-w-[85%] bg-green-500/8 border border-green-500/15 rounded-2xl rounded-bl-md px-4 py-2.5 shadow-sm">
389
- <div className="text-[10px] text-green-400 font-medium mb-1.5">{m.to_name}</div>
390
+ <div className="flex items-center gap-1.5 mb-1.5">
391
+ {m.to_name && <AliasAvatar alias={m.to_name} size={14} />}
392
+ <span className="text-[10px] text-gray-200 font-medium">{m.to_name}</span>
393
+ </div>
390
394
  <div className="text-[13px] text-gray-200">
391
395
  <MarkdownContent text={m.result} />
392
396
  </div>
@@ -422,7 +426,7 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
422
426
  {filteredMentions.map(node => (
423
427
  <button key={node} onClick={() => insertMention(node)}
424
428
  className="w-full flex items-center gap-2 px-3 py-2 text-xs text-left hover:bg-cyan-500/10 transition-colors">
425
- <span className="w-2 h-2 rounded-full bg-cyan-400 shrink-0" />
429
+ <AliasAvatar alias={node} size={16} />
426
430
  <span className="text-gray-300">{node}</span>
427
431
  </button>
428
432
  ))}
@@ -476,9 +480,7 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
476
480
  {/* Header */}
477
481
  <div className="flex items-center justify-between px-4 py-3 border-b border-[#2a2a4a] bg-[#0d0d1a]">
478
482
  <div className="flex items-center gap-3">
479
- <div className="w-8 h-8 rounded-full bg-gradient-to-br from-cyan-500/30 to-blue-500/30 flex items-center justify-center text-xs font-bold text-cyan-300">
480
- {alias.slice(0, 2)}
481
- </div>
483
+ <AliasAvatar alias={alias} size={32} />
482
484
  <div>
483
485
  <div className="text-sm font-semibold text-white">{alias}</div>
484
486
  <div className="text-[10px] text-gray-500">{pollingIds.size > 0 ? 'Processing...' : 'Ready'}</div>
@@ -33,10 +33,14 @@ const cy = 330;
33
33
  const onlineRadius = 220;
34
34
  const offlineRadius = 315;
35
35
 
36
- function polarPoint(index: number, total: number, radius: number) {
36
+ /** Polar coordinate for a node on a ring. `rotateBy` lets the caller offset
37
+ * the whole ring so two stacked rings don't align radially (round 25: the
38
+ * offline ring gets a half-step rotation so its nodes sit in the gaps
39
+ * between online ones). */
40
+ function polarPoint(index: number, total: number, radius: number, rotateBy = 0) {
37
41
  const spread = total <= 2 ? Math.PI : Math.PI * 1.78;
38
42
  const start = -Math.PI / 2 - spread / 2;
39
- const angle = total <= 1 ? -Math.PI / 2 : start + (spread * index) / (total - 1);
43
+ const angle = total <= 1 ? -Math.PI / 2 : start + (spread * index) / (total - 1) + rotateBy;
40
44
  return {
41
45
  x: cx + radius * Math.cos(angle),
42
46
  y: cy + radius * Math.sin(angle),
@@ -248,8 +252,17 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
248
252
  positions[s.alias] = polarPoint(index, Math.max(online.length, 1), onlineRadius);
249
253
  });
250
254
 
255
+ // Offset the offline ring radially by half the online step so offline
256
+ // bubbles sit in the angular gaps between online bubbles instead of
257
+ // stacking directly behind them. Also push the outer ring further when
258
+ // there are many offline nodes so labels don't crowd the legend.
259
+ const spread = online.length <= 2 ? Math.PI : Math.PI * 1.78;
260
+ const onlineStep = online.length > 1 ? spread / (online.length - 1) : 0;
261
+ const offlineRotation = online.length > 0 ? onlineStep / 2 : 0;
262
+ const offlineR = offlineRadius + Math.max(0, offline.length - 4) * 6;
263
+
251
264
  offline.forEach((s, index) => {
252
- positions[s.alias] = polarPoint(index, Math.max(offline.length, 1), offlineRadius);
265
+ positions[s.alias] = polarPoint(index, Math.max(offline.length, 1), offlineR, offlineRotation);
253
266
  });
254
267
 
255
268
  const links = buildFlowLinks(messages, positions);
package/app/globals.css CHANGED
@@ -577,6 +577,30 @@ body {
577
577
  box-shadow: 0 6px 18px -10px rgba(15, 23, 42, 0.12);
578
578
  }
579
579
 
580
+ /* Task row hover bg (round 19) — dark hex would land on a white card and
581
+ look like a stripe. Re-map to a faint accent tint per theme. */
582
+ [data-theme="light"] .anet-task-row:hover {
583
+ background-color: #f5f7fb !important;
584
+ }
585
+ [data-theme="mint"] .anet-task-row:hover {
586
+ background-color: #ecf6f1 !important;
587
+ }
588
+ [data-theme="sunset"] .anet-task-row:hover {
589
+ background-color: #fff4ec !important;
590
+ }
591
+ /* Open state — slightly stronger fill so users can see "this one is the
592
+ open row" while scrolling. */
593
+ [data-theme="light"] .anet-task-row[aria-expanded="true"] {
594
+ background-color: #fafbfd !important;
595
+ box-shadow: 0 4px 14px -10px rgba(15, 23, 42, 0.14) !important;
596
+ }
597
+ [data-theme="mint"] .anet-task-row[aria-expanded="true"] {
598
+ background-color: #f3faf6 !important;
599
+ }
600
+ [data-theme="sunset"] .anet-task-row[aria-expanded="true"] {
601
+ background-color: #fff8f1 !important;
602
+ }
603
+
580
604
  /* ─────────────────────────────────────────────────────────────────────
581
605
  Login surface (round 1 polish)
582
606
  Restrained, minimal — first impression for new Agent Network users.
@@ -684,6 +708,45 @@ body {
684
708
  background-color: rgba(0, 158, 126, 0.08) !important;
685
709
  }
686
710
 
711
+ /* Round 20 — matched-char highlight inside cmdk titles/hints. Inherits
712
+ line color (`color`) instead of forcing one — keeps text readable on
713
+ both inactive (gray) and active (cyan-300) rows. <mark> default is a
714
+ garish yellow block, which we override. */
715
+ .anet-cmdk-mark {
716
+ background: transparent;
717
+ color: inherit;
718
+ font-weight: 600;
719
+ /* Subtle underline so the mark stays visible even on the active row
720
+ where the background tint matches the accent color. */
721
+ text-decoration: underline;
722
+ text-decoration-color: currentColor;
723
+ text-decoration-thickness: 1px;
724
+ text-underline-offset: 2px;
725
+ text-decoration-skip-ink: none;
726
+ }
727
+ /* Cyber: accent the matched glyph color so it pops on the dark bg. */
728
+ [data-theme="cyber"] .anet-cmdk-mark,
729
+ :root:not([data-theme]) .anet-cmdk-mark {
730
+ color: #22d3ee;
731
+ }
732
+ /* Light/mint: use the brand teal — undertone matches the active row. */
733
+ [data-theme="light"] .anet-cmdk-mark,
734
+ [data-theme="mint"] .anet-cmdk-mark {
735
+ color: #0d9488;
736
+ }
737
+ [data-theme="sunset"] .anet-cmdk-mark {
738
+ color: #ea580c;
739
+ }
740
+ /* On the active (cyan-tinted) row, the title is already cyan-300 — use
741
+ white-with-underline so the highlight reads as emphasis, not redundancy. */
742
+ .anet-cmdk-row.bg-cyan-500\/10 .anet-cmdk-mark {
743
+ color: #f0fdff;
744
+ }
745
+ [data-theme="light"] .anet-cmdk-row.bg-cyan-500\/10 .anet-cmdk-mark,
746
+ [data-theme="mint"] .anet-cmdk-row.bg-cyan-500\/10 .anet-cmdk-mark {
747
+ color: #064e3b;
748
+ }
749
+
687
750
  /* Help overlay — same white-card-with-shadow treatment as cmdk on light */
688
751
  [data-theme="light"] .anet-help > div:not([aria-hidden]),
689
752
  [data-theme="mint"] .anet-help > div:not([aria-hidden]) {
@@ -692,6 +755,47 @@ body {
692
755
  0 2px 6px -2px rgba(15, 23, 42, 0.10);
693
756
  }
694
757
 
758
+ /* Tasks tab strip horizontal scroll fade — gradient overlays at left/right
759
+ edges hint that there's more content when the row overflows. Only
760
+ applied on <sm (where the strip actually scrolls). The wrapper is
761
+ `position: relative` and the fades are absolute siblings. */
762
+ .anet-tabstrip-wrap {
763
+ position: relative;
764
+ }
765
+ .anet-tabstrip-wrap::before,
766
+ .anet-tabstrip-wrap::after {
767
+ content: '';
768
+ position: absolute;
769
+ top: 4px;
770
+ bottom: 4px;
771
+ width: 20px;
772
+ pointer-events: none;
773
+ z-index: 1;
774
+ }
775
+ .anet-tabstrip-wrap::before {
776
+ left: 0;
777
+ background: linear-gradient(to right, #111128, transparent);
778
+ border-radius: 8px 0 0 8px;
779
+ }
780
+ .anet-tabstrip-wrap::after {
781
+ right: 0;
782
+ background: linear-gradient(to left, #111128, transparent);
783
+ border-radius: 0 8px 8px 0;
784
+ }
785
+ @media (min-width: 640px) {
786
+ /* sm: stops scrolling, hide the fades */
787
+ .anet-tabstrip-wrap::before,
788
+ .anet-tabstrip-wrap::after { display: none; }
789
+ }
790
+ [data-theme="light"] .anet-tabstrip-wrap::before,
791
+ [data-theme="mint"] .anet-tabstrip-wrap::before {
792
+ background: linear-gradient(to right, var(--bg-secondary), transparent);
793
+ }
794
+ [data-theme="light"] .anet-tabstrip-wrap::after,
795
+ [data-theme="mint"] .anet-tabstrip-wrap::after {
796
+ background: linear-gradient(to left, var(--bg-secondary), transparent);
797
+ }
798
+
695
799
  /* P0-3 baseline: error / warning chip colors for light themes.
696
800
  The component-level copy (replacing raw JSON with human text) is fixed
697
801
  in the Logs / Server-Logs page components separately. */
@@ -4,6 +4,7 @@ import { useMemo, useState } from 'react';
4
4
  import { useMessages } from '../lib/hooks';
5
5
  import { timeAgo } from '../components/utils';
6
6
  import { EmptyState } from '../components/EmptyState';
7
+ import { AliasAvatar } from '../components/AliasAvatar';
7
8
 
8
9
  interface MessageItem {
9
10
  id: string;
@@ -40,6 +41,17 @@ function bubbleVariant(message: MessageItem) {
40
41
  return 'incoming';
41
42
  }
42
43
 
44
+ /** Render message content, highlighting the search match inline (skips the
45
+ * `from:` filter prefix since it filters by sender, not by content). */
46
+ function renderHighlighted(content: string | undefined, search: string) {
47
+ const text = content || '--';
48
+ const q = search.trim();
49
+ if (!q || q.startsWith('from:')) return text;
50
+ const idx = text.toLowerCase().indexOf(q.toLowerCase());
51
+ if (idx < 0) return text;
52
+ return <>{text.slice(0, idx)}<mark className="bg-yellow-500/30 text-yellow-200 rounded px-0.5">{text.slice(idx, idx + q.length)}</mark>{text.slice(idx + q.length)}</>;
53
+ }
54
+
43
55
  export default function MessagesPage() {
44
56
  const { messages, isLoading } = useMessages(200);
45
57
  const [filterType, setFilterType] = useState('');
@@ -213,13 +225,21 @@ export default function MessagesPage() {
213
225
  ))}
214
226
  </div>
215
227
  ) : (
216
- <div className="space-y-3">
228
+ <div className="space-y-1">
217
229
  {filtered.map((message: MessageItem, index) => {
218
230
  const previous = filtered[index - 1];
219
231
  const gapExceeded = previous
220
232
  ? normalizeDate(message.created_at) - normalizeDate(previous.created_at) > 5 * 60 * 1000
221
233
  : false;
222
234
  const variant = bubbleVariant(message);
235
+ // Cluster: same sender + same direction + same to_alias and no
236
+ // time-gap divider → tighter spacing and hide repeated header.
237
+ const samePrev = !!previous
238
+ && !gapExceeded
239
+ && bubbleVariant(previous) === variant
240
+ && (previous.from_alias || '') === (message.from_alias || '')
241
+ && (previous.to_alias || '') === (message.to_alias || '');
242
+ const fromAlias = message.from_alias || '?';
223
243
 
224
244
  return (
225
245
  <div key={message.id}>
@@ -231,60 +251,71 @@ export default function MessagesPage() {
231
251
  </div>
232
252
  )}
233
253
 
234
- <div className={
235
- variant === 'broadcast'
236
- ? ''
237
- : variant === 'outgoing'
238
- ? 'flex justify-end'
239
- : 'flex justify-start'
240
- }>
241
- <div className={`rounded-2xl border px-4 py-3 shadow-sm ${
242
- variant === 'broadcast'
243
- ? 'w-full border-purple-500/20 bg-purple-500/10'
244
- : variant === 'outgoing'
245
- ? 'max-w-3xl border-green-500/20 bg-green-500/10'
246
- : 'max-w-3xl border-blue-500/20 bg-blue-500/10'
247
- }`}>
248
- <div className="mb-2 flex flex-wrap items-center gap-2">
249
- <span className={`text-xs px-2 py-0.5 rounded-md border ${TYPE_COLORS[message.type || ''] || 'bg-gray-500/10 text-gray-400 border-gray-500/20'}`}>
250
- {message.type || 'unknown'}
251
- </span>
252
- <span className={`text-sm font-medium ${variant === 'outgoing' ? 'text-green-300' : variant === 'broadcast' ? 'text-purple-200' : 'text-blue-300'}`}>
253
- {message.from_alias || '--'}
254
- </span>
255
- {variant !== 'broadcast' && (
256
- <>
254
+ {/* Broadcasts span full width — no avatar gutter. */}
255
+ {variant === 'broadcast' ? (
256
+ <div className={samePrev ? 'mt-1' : 'mt-3'}>
257
+ <div className="rounded-2xl border border-purple-500/20 bg-purple-500/10 px-4 py-3 shadow-sm w-full">
258
+ <div className="mb-2 flex flex-wrap items-center gap-2">
259
+ <span className={`text-xs px-2 py-0.5 rounded-md border ${TYPE_COLORS[message.type || ''] || 'bg-gray-500/10 text-gray-400 border-gray-500/20'}`}>
260
+ {message.type || 'unknown'}
261
+ </span>
262
+ <span className="text-sm font-medium text-purple-200">{fromAlias}</span>
263
+ {message.to_alias && <span className="text-xs text-purple-200/80">to {message.to_alias}</span>}
264
+ {message.priority === 'high' && <span className="text-xs text-red-400">HIGH</span>}
265
+ <span className="ml-auto text-xs text-gray-500">{message.created_at ? timeAgo(message.created_at) : '--'}</span>
266
+ </div>
267
+ <div className="whitespace-pre-wrap text-sm leading-relaxed text-gray-200">
268
+ {renderHighlighted(message.content, search)}
269
+ </div>
270
+ </div>
271
+ </div>
272
+ ) : (
273
+ <div className={`${samePrev ? 'mt-1' : 'mt-3'} flex gap-2 ${variant === 'outgoing' ? 'flex-row-reverse' : 'flex-row'}`}>
274
+ {/* Avatar gutter — fixed width keeps bubble columns aligned even on streaks */}
275
+ <div className="w-8 shrink-0 pt-1">
276
+ {!samePrev && <AliasAvatar alias={fromAlias} size={32} />}
277
+ </div>
278
+ <div className={`min-w-0 max-w-3xl rounded-2xl border px-4 py-3 shadow-sm ${
279
+ variant === 'outgoing'
280
+ ? 'border-green-500/20 bg-green-500/10'
281
+ : 'border-blue-500/20 bg-blue-500/10'
282
+ }`}>
283
+ {!samePrev && (
284
+ <div className="mb-2 flex flex-wrap items-center gap-2">
285
+ <span className={`text-xs px-2 py-0.5 rounded-md border ${TYPE_COLORS[message.type || ''] || 'bg-gray-500/10 text-gray-400 border-gray-500/20'}`}>
286
+ {message.type || 'unknown'}
287
+ </span>
288
+ <span className={`text-sm font-medium ${variant === 'outgoing' ? 'text-green-300' : 'text-blue-300'}`}>
289
+ {fromAlias}
290
+ </span>
257
291
  <span className="text-xs text-gray-600">&rarr;</span>
258
292
  <span className="text-sm text-gray-300">{message.to_alias || '--'}</span>
259
- </>
293
+ {message.priority === 'high' && <span className="text-xs text-red-400">HIGH</span>}
294
+ <span className="ml-auto text-xs text-gray-500">{message.created_at ? timeAgo(message.created_at) : '--'}</span>
295
+ </div>
260
296
  )}
261
- {variant === 'broadcast' && message.to_alias && (
262
- <span className="text-xs text-purple-200/80">to {message.to_alias}</span>
297
+
298
+ <div className="whitespace-pre-wrap text-sm leading-relaxed text-gray-200">
299
+ {renderHighlighted(message.content, search)}
300
+ </div>
301
+
302
+ {/* When in a streak, surface the timestamp inline-right so users still know cadence */}
303
+ {samePrev && (
304
+ <div className="mt-1 text-[10px] text-gray-600 text-right">
305
+ {message.created_at ? timeAgo(message.created_at) : ''}
306
+ </div>
263
307
  )}
264
- {message.priority === 'high' && <span className="text-xs text-red-400">HIGH</span>}
265
- <span className="ml-auto text-xs text-gray-500">{message.created_at ? timeAgo(message.created_at) : '--'}</span>
266
- </div>
267
308
 
268
- <div className="whitespace-pre-wrap text-sm leading-relaxed text-gray-200">
269
- {(() => {
270
- const text = message.content || '--';
271
- const q = search.trim();
272
- if (!q || q.startsWith('from:')) return text;
273
- const idx = text.toLowerCase().indexOf(q.toLowerCase());
274
- if (idx < 0) return text;
275
- return <>{text.slice(0, idx)}<mark className="bg-yellow-500/30 text-yellow-200 rounded px-0.5">{text.slice(idx, idx + q.length)}</mark>{text.slice(idx + q.length)}</>;
276
- })()}
309
+ {debug && (
310
+ <div className="mt-3 border-t border-white/10 pt-3 text-xs text-gray-500 space-y-1">
311
+ <div>ID: {message.id}</div>
312
+ {message.task_id && <div>Task ID: {message.task_id}</div>}
313
+ <div>Created: {message.created_at}</div>
314
+ </div>
315
+ )}
277
316
  </div>
278
-
279
- {debug && (
280
- <div className="mt-3 border-t border-white/10 pt-3 text-xs text-gray-500 space-y-1">
281
- <div>ID: {message.id}</div>
282
- {message.task_id && <div>Task ID: {message.task_id}</div>}
283
- <div>Created: {message.created_at}</div>
284
- </div>
285
- )}
286
317
  </div>
287
- </div>
318
+ )}
288
319
  </div>
289
320
  );
290
321
  })}
@@ -6,6 +6,7 @@ import { timeAgo } from '../components/utils';
6
6
  import { useSessions, useHealth } from '../lib/hooks';
7
7
  import { TaskChatPanel } from '../components/TaskChatPanel';
8
8
  import { EmptyState } from '../components/EmptyState';
9
+ import { AliasAvatar } from '../components/AliasAvatar';
9
10
  import type { Session } from '../components/types';
10
11
 
11
12
  const STATUS_COLORS: Record<string, string> = {
@@ -188,14 +189,18 @@ export default function NodesPage() {
188
189
  </span>
189
190
  </div>
190
191
  )}
191
- <div className="flex items-start justify-between gap-3">
192
+ <div className="flex items-start gap-3">
193
+ <AliasAvatar alias={s.alias} size={36} />
192
194
  <Link href={`/node?alias=${encodeURIComponent(s.alias)}`} className="min-w-0 flex-1 hover:text-cyan-300">
193
195
  <div className="flex items-center gap-2">
194
196
  <span className="truncate text-base font-semibold text-white">{s.alias}</span>
195
- <span className={`shrink-0 rounded-md border px-2 py-0.5 text-[11px] ${STATUS_COLORS[statusKey] || STATUS_COLORS.offline}`}>
197
+ <span className={`shrink-0 rounded-md border px-2 py-0.5 text-[10px] uppercase tracking-wide ${STATUS_COLORS[statusKey] || STATUS_COLORS.offline}`}>
196
198
  {statusKey}
197
199
  </span>
198
200
  </div>
201
+ <div className="mt-0.5 truncate text-xs text-gray-500">
202
+ {(s.agent || '—')}<span className="text-gray-700 mx-1.5">·</span>{(s.server || '—')}
203
+ </div>
199
204
  </Link>
200
205
  <div className="flex gap-1.5 shrink-0">
201
206
  <button type="button" onClick={() => setChatAlias(s.alias)}
@@ -209,25 +214,16 @@ export default function NodesPage() {
209
214
  </div>
210
215
  </div>
211
216
 
212
- <div className="mt-3 space-y-2 text-xs text-gray-500">
213
- <div className="flex justify-between gap-3">
214
- <span>Agent</span>
215
- <span className="truncate text-gray-300">{s.agent || '--'}</span>
216
- </div>
217
- <div className="flex justify-between gap-3">
218
- <span>Server</span>
219
- <span className="truncate text-gray-300">{s.server || '--'}</span>
217
+ <div className="mt-3 rounded-lg border border-[#1a1a2a] bg-[#0a0a15] px-3 py-2 text-xs">
218
+ <div className="flex items-center justify-between gap-3">
219
+ <span className="text-[10px] uppercase tracking-wide text-gray-600">Current task</span>
220
+ <span className="text-[10px] text-gray-600">{timeAgo(s.last_seen_at || s.updated_at)}</span>
220
221
  </div>
221
- <div className="flex justify-between gap-3">
222
- <span>Updated</span>
223
- <span className="text-gray-300">{timeAgo(s.last_seen_at || s.updated_at)}</span>
222
+ <div className={`mt-1 line-clamp-2 ${s.task ? 'text-gray-300' : 'text-gray-600 italic'}`}>
223
+ {s.task || 'No current task'}
224
224
  </div>
225
225
  </div>
226
226
 
227
- <div className="mt-3 rounded-lg border border-[#1a1a2a] bg-[#0a0a15] px-3 py-2 text-xs text-gray-400">
228
- {s.task ? s.task : 'No current task'}
229
- </div>
230
-
231
227
  {progress > 0 && progress < 100 && (
232
228
  <div className="mt-3 h-1.5 overflow-hidden rounded-full bg-gray-800">
233
229
  <div className="h-1.5 rounded-full bg-cyan-400 transition-all" style={{ width: `${progress}%` }} />
@@ -297,8 +293,9 @@ export default function NodesPage() {
297
293
  </span>
298
294
  </div>
299
295
  <div className="col-span-2 min-w-0">
300
- <Link href={`/node?alias=${encodeURIComponent(s.alias)}`} className="truncate text-sm font-medium text-white hover:text-cyan-300">
301
- {s.alias}
296
+ <Link href={`/node?alias=${encodeURIComponent(s.alias)}`} className="flex items-center gap-2 min-w-0 hover:text-cyan-300">
297
+ <AliasAvatar alias={s.alias} size={20} />
298
+ <span className="truncate text-sm font-medium text-white">{s.alias}</span>
302
299
  </Link>
303
300
  </div>
304
301
  <div className="col-span-2 truncate text-xs text-gray-400">{s.agent || '--'}</div>
@@ -317,15 +314,16 @@ export default function NodesPage() {
317
314
  </div>
318
315
  </div>
319
316
  <div className="sm:hidden space-y-2">
320
- <div className="flex items-center justify-between gap-3">
321
- <Link href={`/node?alias=${encodeURIComponent(s.alias)}`} className="truncate text-sm font-medium text-white hover:text-cyan-300">
322
- {s.alias}
317
+ <div className="flex items-center gap-2.5">
318
+ <AliasAvatar alias={s.alias} size={28} />
319
+ <Link href={`/node?alias=${encodeURIComponent(s.alias)}`} className="min-w-0 flex-1 hover:text-cyan-300">
320
+ <div className="truncate text-sm font-medium text-white">{s.alias}</div>
321
+ <div className="truncate text-[10px] text-gray-500">{s.agent || '—'} · {timeAgo(s.last_seen_at || s.updated_at)}</div>
323
322
  </Link>
324
323
  <span className={`shrink-0 text-xs px-2 py-0.5 rounded-md border ${STATUS_COLORS[statusKey] || STATUS_COLORS.offline}`}>
325
324
  {statusKey}
326
325
  </span>
327
326
  </div>
328
- <div className="text-xs text-gray-500">{s.agent || '--'} · {timeAgo(s.last_seen_at || s.updated_at)}</div>
329
327
  {s.task && <div className="truncate text-xs text-gray-500">{s.task}</div>}
330
328
  <div className="flex gap-2">
331
329
  <button type="button" onClick={() => setChatAlias(s.alias)}
package/app/page.tsx CHANGED
@@ -10,6 +10,7 @@ import { AgentCard } from './components/AgentCard';
10
10
  import { InboxPanel } from './components/InboxPanel';
11
11
  import { LoadingSkeleton } from './components/LoadingSkeleton';
12
12
  import { NodesEmptyState as EmptyState } from './components/EmptyState';
13
+ import { AliasAvatar } from './components/AliasAvatar';
13
14
  import { UserBar } from './components/UserBar';
14
15
  import { CommandCenter, useCommandCenter } from './components/CommandCenter';
15
16
  import { DispatchPanel } from './components/DispatchPanel';
@@ -120,7 +121,7 @@ export default function Dashboard() {
120
121
  </div>
121
122
 
122
123
  {/* Dispatch + User Bar */}
123
- <div className="flex items-center gap-3 mb-2">
124
+ <div className="flex items-center gap-3 mb-4">
124
125
  <button onClick={() => setShowDispatch(true)}
125
126
  className="px-4 py-2 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-white text-sm font-medium rounded-xl shadow-lg shadow-cyan-500/10 transition-all active:scale-95 shrink-0">
126
127
  ⚡ Dispatch
@@ -189,9 +190,9 @@ export default function Dashboard() {
189
190
  {/* Quick Actions — split into two distinct intents:
190
191
  (1) Top: live stat cards (carry data, drill-in on click)
191
192
  (2) Bottom: pure nav rail (no number, icon + label)
192
- Previously these were mixed in one row of 6, mixing data cells
193
- ("0/0", "--", "0") with placeholder arrows ("→ Messages").
194
- Users couldn't tell which were stats vs which were navigation. */}
193
+ Round 24 — wrap both in a labelled block so the rhythm reads as
194
+ "here are the main jumps" instead of two disconnected strips. */}
195
+ <div className="text-[10px] uppercase tracking-[0.12em] text-gray-600 mb-2">Quick navigation</div>
195
196
  <section className="mb-3 grid grid-cols-3 gap-2 sm:gap-3">
196
197
  {(() => {
197
198
  // Build breakdown popover content per card. Pure data — pure CSS
@@ -314,9 +315,11 @@ export default function Dashboard() {
314
315
  <span className={`w-1.5 h-1.5 rounded-full shrink-0 ${
315
316
  t.status === 'replied' ? 'bg-purple-400' : t.status === 'running' ? 'bg-green-400' : t.status === 'failed' ? 'bg-red-400' : 'bg-blue-400'
316
317
  }`} />
317
- <span className="text-blue-400 shrink-0">{t.from_name || '?'}</span>
318
+ {t.from_name && <AliasAvatar alias={t.from_name} size={14} />}
319
+ <span className="text-gray-300 shrink-0 max-w-[20%] truncate">{t.from_name || '?'}</span>
318
320
  <span className="text-gray-600">&rarr;</span>
319
- <span className="text-cyan-400 shrink-0">{t.to_name || '?'}</span>
321
+ {t.to_name && <AliasAvatar alias={t.to_name} size={14} />}
322
+ <span className="text-gray-300 shrink-0 max-w-[20%] truncate">{t.to_name || '?'}</span>
320
323
  <span className="text-gray-500 truncate flex-1">{t.content?.slice(0, 40) || '--'}</span>
321
324
  <span className={`shrink-0 px-1.5 py-0.5 rounded text-[10px] border ${
322
325
  t.status === 'replied' ? 'text-purple-300 border-purple-500/20' : t.status === 'failed' ? 'text-red-300 border-red-500/20' : 'text-gray-500 border-gray-700/30'