@sleep2agi/agent-network-dashboard 0.4.5-preview.4 → 0.4.5-preview.40

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 (255) 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 +50 -50
  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 +4 -4
  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.js.nft.json +1 -1
  23. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  24. package/.next/server/app/admin.html +4 -4
  25. package/.next/server/app/admin.rsc +14 -14
  26. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  27. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  28. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  29. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  30. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  31. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  32. package/.next/server/app/index.html +4 -4
  33. package/.next/server/app/index.rsc +14 -14
  34. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  35. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  36. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  37. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  38. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  39. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  40. package/.next/server/app/login.html +2 -2
  41. package/.next/server/app/login.rsc +14 -14
  42. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  43. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  44. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  45. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  46. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  47. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  48. package/.next/server/app/logs/page.js.nft.json +1 -1
  49. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  50. package/.next/server/app/logs.html +4 -4
  51. package/.next/server/app/logs.rsc +14 -14
  52. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  53. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  54. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  55. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  56. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  57. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  58. package/.next/server/app/messages/page.js.nft.json +1 -1
  59. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  60. package/.next/server/app/messages.html +4 -4
  61. package/.next/server/app/messages.rsc +14 -14
  62. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  63. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  64. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  65. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  66. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  67. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  68. package/.next/server/app/node/page.js.nft.json +1 -1
  69. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  70. package/.next/server/app/node.html +4 -4
  71. package/.next/server/app/node.rsc +14 -14
  72. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  73. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  74. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  75. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  76. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  77. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  78. package/.next/server/app/nodes/page.js.nft.json +1 -1
  79. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  80. package/.next/server/app/nodes.html +4 -4
  81. package/.next/server/app/nodes.rsc +14 -14
  82. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  83. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  84. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  85. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  86. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  87. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  88. package/.next/server/app/page.js.nft.json +1 -1
  89. package/.next/server/app/page_client-reference-manifest.js +1 -1
  90. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  91. package/.next/server/app/server-logs.html +4 -4
  92. package/.next/server/app/server-logs.rsc +14 -14
  93. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  94. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  95. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  96. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  97. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  98. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  99. package/.next/server/app/settings/networks/page.js.nft.json +1 -1
  100. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  101. package/.next/server/app/settings/networks.html +4 -4
  102. package/.next/server/app/settings/networks.rsc +14 -14
  103. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  104. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  105. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  106. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  107. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  108. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  109. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  110. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  111. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  112. package/.next/server/app/settings/tokens.html +4 -4
  113. package/.next/server/app/settings/tokens.rsc +14 -14
  114. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  115. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  116. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  117. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  118. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  119. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  120. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  121. package/.next/server/app/settings.html +4 -4
  122. package/.next/server/app/settings.rsc +14 -14
  123. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  124. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  125. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  126. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  127. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  128. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  129. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  130. package/.next/server/app/tasks/page.js.nft.json +1 -1
  131. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  132. package/.next/server/app/tasks.html +4 -4
  133. package/.next/server/app/tasks.rsc +14 -14
  134. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  135. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  136. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  137. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  138. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  139. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  140. package/.next/server/chunks/ssr/[root-of-the-server]__02ee4km._.js +3 -0
  141. package/.next/server/chunks/ssr/[root-of-the-server]__02ee4km._.js.map +1 -0
  142. package/.next/server/chunks/ssr/[root-of-the-server]__0jjqrcf._.js +3 -0
  143. package/.next/server/chunks/ssr/[root-of-the-server]__0jjqrcf._.js.map +1 -0
  144. package/.next/server/chunks/ssr/[root-of-the-server]__0o8yj3c._.js +3 -0
  145. package/.next/server/chunks/ssr/[root-of-the-server]__0o8yj3c._.js.map +1 -0
  146. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js +1 -1
  147. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js.map +1 -1
  148. package/.next/server/chunks/ssr/_0nne7mz._.js +3 -0
  149. package/.next/server/chunks/ssr/_0nne7mz._.js.map +1 -0
  150. package/.next/server/chunks/ssr/app_0.zbtn5._.js +9 -0
  151. package/.next/server/chunks/ssr/app_0.zbtn5._.js.map +1 -0
  152. package/.next/server/chunks/ssr/app_04n78pr._.js +3 -0
  153. package/.next/server/chunks/ssr/app_04n78pr._.js.map +1 -0
  154. package/.next/server/chunks/ssr/app_0_agk0r._.js +3 -0
  155. package/.next/server/chunks/ssr/app_0_agk0r._.js.map +1 -0
  156. package/.next/server/chunks/ssr/app_0dk5wao._.js +1 -1
  157. package/.next/server/chunks/ssr/app_0dk5wao._.js.map +1 -1
  158. package/.next/server/chunks/ssr/app_0izi28j._.js +1 -1
  159. package/.next/server/chunks/ssr/app_0izi28j._.js.map +1 -1
  160. package/.next/server/chunks/ssr/app_0tncr1_._.js +3 -0
  161. package/.next/server/chunks/ssr/app_0tncr1_._.js.map +1 -0
  162. package/.next/server/chunks/ssr/app_components_08ys5y3._.js +5 -5
  163. package/.next/server/chunks/ssr/app_components_08ys5y3._.js.map +1 -1
  164. package/.next/server/chunks/ssr/app_components_0lhp_k.._.js +3 -0
  165. package/.next/server/chunks/ssr/app_components_0lhp_k.._.js.map +1 -0
  166. package/.next/server/chunks/ssr/app_login_page_tsx_0z71ad3._.js +1 -1
  167. package/.next/server/chunks/ssr/app_login_page_tsx_0z71ad3._.js.map +1 -1
  168. package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js +1 -1
  169. package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js.map +1 -1
  170. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js +1 -1
  171. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js.map +1 -1
  172. package/.next/server/middleware-build-manifest.js +3 -3
  173. package/.next/server/pages/404.html +4 -4
  174. package/.next/server/pages/500.html +1 -1
  175. package/.next/server/server-reference-manifest.js +1 -1
  176. package/.next/server/server-reference-manifest.json +1 -1
  177. package/.next/static/chunks/02oje7pf-g1v5.js +1 -0
  178. package/.next/static/chunks/06x7w8nwggx~~.js +1 -0
  179. package/.next/static/chunks/0943n0t.9c-o2.js +1 -0
  180. package/.next/static/chunks/{07okhs5561y5y.js → 0c1bp~g7.8ru..js} +1 -1
  181. package/.next/static/chunks/0csmku~3y472t.js +1 -0
  182. package/.next/static/chunks/0df0py1glk9xm.js +1 -0
  183. package/.next/static/chunks/0edt_z0rlxgfk.js +1 -0
  184. package/.next/static/chunks/0irkayokwc1fm.js +1 -0
  185. package/.next/static/chunks/0j8ee64jv6zb7.js +7 -0
  186. package/.next/static/chunks/0qe_pfeioih-5.css +2 -0
  187. package/.next/static/chunks/0qnpc7rbxn12u.js +1 -0
  188. package/.next/static/chunks/0xdl435t_mf1c.js +1 -0
  189. package/.next/static/chunks/0~n.cadw9_e8s.js +7 -0
  190. package/.next/static/chunks/15c9pv6gxir.5.js +1 -0
  191. package/.next/static/chunks/15nl80~3bo8ze.js +1 -0
  192. package/.next/static/chunks/17-5fpxo16w_n.js +1 -0
  193. package/.next/trace +2 -2
  194. package/.next/trace-build +1 -1
  195. package/app/admin/page.tsx +77 -25
  196. package/app/components/AgentCard.tsx +19 -5
  197. package/app/components/AliasAvatar.tsx +51 -0
  198. package/app/components/BroadcastBar.tsx +22 -4
  199. package/app/components/DispatchPanel.tsx +9 -5
  200. package/app/components/EmptyState.tsx +77 -4
  201. package/app/components/HealthBanner.tsx +1 -1
  202. package/app/components/HelpOverlay.tsx +50 -10
  203. package/app/components/Sidebar.tsx +49 -41
  204. package/app/components/TaskChatPanel.tsx +8 -10
  205. package/app/components/TaskDrawer.tsx +72 -28
  206. package/app/components/TopoGraph.tsx +118 -24
  207. package/app/globals.css +90 -0
  208. package/app/lib/version.ts +7 -0
  209. package/app/login/page.tsx +37 -7
  210. package/app/logs/page.tsx +72 -21
  211. package/app/messages/page.tsx +85 -51
  212. package/app/node/page.tsx +32 -15
  213. package/app/nodes/page.tsx +21 -23
  214. package/app/page.tsx +71 -13
  215. package/app/server-logs/page.tsx +37 -3
  216. package/app/settings/networks/page.tsx +37 -13
  217. package/app/settings/page.tsx +38 -10
  218. package/app/tasks/page.tsx +77 -41
  219. package/package.json +1 -1
  220. package/.next/server/chunks/ssr/[root-of-the-server]__0_jnflc._.js +0 -3
  221. package/.next/server/chunks/ssr/[root-of-the-server]__0_jnflc._.js.map +0 -1
  222. package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js +0 -3
  223. package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js.map +0 -1
  224. package/.next/server/chunks/ssr/[root-of-the-server]__12mpoig._.js +0 -3
  225. package/.next/server/chunks/ssr/[root-of-the-server]__12mpoig._.js.map +0 -1
  226. package/.next/server/chunks/ssr/_0d-fuox._.js +0 -3
  227. package/.next/server/chunks/ssr/_0d-fuox._.js.map +0 -1
  228. package/.next/server/chunks/ssr/app_0.0wpa~._.js +0 -3
  229. package/.next/server/chunks/ssr/app_0.0wpa~._.js.map +0 -1
  230. package/.next/server/chunks/ssr/app_0ap_oiq._.js +0 -3
  231. package/.next/server/chunks/ssr/app_0ap_oiq._.js.map +0 -1
  232. package/.next/server/chunks/ssr/app_0et5yua._.js +0 -3
  233. package/.next/server/chunks/ssr/app_0et5yua._.js.map +0 -1
  234. package/.next/server/chunks/ssr/app_0ff~kjj._.js +0 -9
  235. package/.next/server/chunks/ssr/app_0ff~kjj._.js.map +0 -1
  236. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js +0 -3
  237. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js.map +0 -1
  238. package/.next/static/chunks/02flrewxlf_m_.js +0 -1
  239. package/.next/static/chunks/03sm_xa792mxv.css +0 -2
  240. package/.next/static/chunks/051gslfjka1kj.js +0 -1
  241. package/.next/static/chunks/06t_vzei8s9_h.js +0 -1
  242. package/.next/static/chunks/07oobuohrpigy.js +0 -1
  243. package/.next/static/chunks/0e7c40p~wq665.js +0 -1
  244. package/.next/static/chunks/0g7m7zk.v7g6h.js +0 -1
  245. package/.next/static/chunks/0khpyxpdrp~ty.js +0 -1
  246. package/.next/static/chunks/0lo_g.q1fm-hr.js +0 -7
  247. package/.next/static/chunks/0m328uz90rp81.js +0 -1
  248. package/.next/static/chunks/0ogeals5ufhho.js +0 -1
  249. package/.next/static/chunks/1031v6nw33ud3.js +0 -1
  250. package/.next/static/chunks/10ir.g.n~hre0.js +0 -7
  251. package/.next/static/chunks/10njj7kn9ylkf.js +0 -1
  252. package/.next/static/chunks/13.dst7liziwo.js +0 -1
  253. /package/.next/static/{dvKGMaUCm52sSTROKhYVL → qyIMm3STzBMQg0Ho6J3Tr}/_buildManifest.js +0 -0
  254. /package/.next/static/{dvKGMaUCm52sSTROKhYVL → qyIMm3STzBMQg0Ho6J3Tr}/_clientMiddlewareManifest.js +0 -0
  255. /package/.next/static/{dvKGMaUCm52sSTROKhYVL → qyIMm3STzBMQg0Ho6J3Tr}/_ssgManifest.js +0 -0
@@ -0,0 +1,7 @@
1
+ /** Dashboard package version sourced from package.json at build time.
2
+ * Importing JSON requires `resolveJsonModule: true` in tsconfig, which
3
+ * Next sets by default. Bumping is automated by the publish flow, so this
4
+ * surfaces the real preview number in Settings → Dashboard. */
5
+ import pkg from '../../package.json' assert { type: 'json' };
6
+
7
+ export const DASHBOARD_VERSION: string = pkg.version;
@@ -6,6 +6,7 @@ export default function LoginPage() {
6
6
  const [mode, setMode] = useState<'login' | 'register'>('login');
7
7
  const [username, setUsername] = useState('');
8
8
  const [password, setPassword] = useState('');
9
+ const [showPassword, setShowPassword] = useState(false);
9
10
  const [error, setError] = useState('');
10
11
  const [pending, setPending] = useState(false);
11
12
 
@@ -68,7 +69,9 @@ export default function LoginPage() {
68
69
  </div>
69
70
  <h1 className="text-[22px] sm:text-2xl font-semibold text-white tracking-tight">Agent Network</h1>
70
71
  <p className="text-[13px] text-gray-500 mt-1.5 leading-snug">
71
- One control plane for your AI agents · <span className="text-gray-600">Tasks · Mesh · Messages</span>
72
+ {mode === 'login'
73
+ ? <>Welcome back · <span className="text-gray-600">Tasks · Mesh · Messages</span></>
74
+ : <>Create your account · <span className="text-gray-600">Free preview · no credit card</span></>}
72
75
  </p>
73
76
  </div>
74
77
 
@@ -93,17 +96,44 @@ export default function LoginPage() {
93
96
  <label htmlFor="password" className="block text-xs text-gray-500 mb-2 uppercase tracking-wider">
94
97
  Password
95
98
  </label>
96
- <input id="password" type="password" value={password} onChange={e => setPassword(e.target.value)}
97
- placeholder="Enter password"
98
- className="w-full bg-[#0a0a15] border border-[#2a2a4a] rounded-lg px-4 py-3 text-sm text-white placeholder-gray-600 focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/20 focus:outline-none transition-all" />
99
+ <div className="relative">
100
+ <input id="password" type={showPassword ? 'text' : 'password'} value={password} onChange={e => setPassword(e.target.value)}
101
+ placeholder="Enter password"
102
+ className="w-full bg-[#0a0a15] border border-[#2a2a4a] rounded-lg px-4 pr-11 py-3 text-sm text-white placeholder-gray-600 focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/20 focus:outline-none transition-all" />
103
+ <button
104
+ type="button"
105
+ onClick={() => setShowPassword(s => !s)}
106
+ tabIndex={-1}
107
+ aria-label={showPassword ? 'Hide password' : 'Show password'}
108
+ className="absolute inset-y-0 right-0 flex items-center px-3 text-gray-600 hover:text-gray-300 transition-colors"
109
+ >
110
+ {showPassword ? (
111
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
112
+ <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
113
+ <line x1="1" y1="1" x2="23" y2="23" />
114
+ </svg>
115
+ ) : (
116
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
117
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
118
+ <circle cx="12" cy="12" r="3" />
119
+ </svg>
120
+ )}
121
+ </button>
122
+ </div>
99
123
 
100
124
  {error && (
101
125
  <div className="mt-3 text-sm text-red-300 bg-red-500/5 border border-red-500/10 rounded-lg px-3 py-2">{error}</div>
102
126
  )}
103
127
 
104
- <button type="submit" disabled={pending || !password}
105
- className="mt-5 w-full px-4 py-3 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 disabled:from-gray-800 disabled:to-gray-800 disabled:text-gray-600 text-white text-sm font-medium rounded-lg transition-all shadow-lg shadow-cyan-500/10 hover:shadow-cyan-500/20 disabled:shadow-none">
106
- {pending ? (mode === 'login' ? 'Signing in...' : 'Registering...') : (mode === 'login' ? 'Sign in' : 'Create account')}
128
+ <button type="submit" disabled={pending || !password || !username.trim()}
129
+ className="mt-5 w-full flex items-center justify-center gap-2 px-4 py-3 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 disabled:from-gray-800 disabled:to-gray-800 disabled:text-gray-600 text-white text-sm font-medium rounded-lg transition-all shadow-lg shadow-cyan-500/10 hover:shadow-cyan-500/20 disabled:shadow-none">
130
+ {pending && (
131
+ <svg aria-hidden className="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none">
132
+ <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="3" opacity="0.25" />
133
+ <path d="M4 12a8 8 0 018-8" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
134
+ </svg>
135
+ )}
136
+ <span>{pending ? (mode === 'login' ? 'Signing in…' : 'Registering…') : (mode === 'login' ? 'Sign in' : 'Create account')}</span>
107
137
  </button>
108
138
  </form>
109
139
 
package/app/logs/page.tsx CHANGED
@@ -5,6 +5,7 @@ import Link from 'next/link';
5
5
  import { timeAgo } from '../components/utils';
6
6
  import { useNetworkId } from '../lib/network-context';
7
7
  import { EmptyState } from '../components/EmptyState';
8
+ import { AliasAvatar } from '../components/AliasAvatar';
8
9
 
9
10
  interface AuditLog {
10
11
  id: string;
@@ -26,6 +27,18 @@ const ACTION_COLORS: Record<string, string> = {
26
27
  send_task: 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20',
27
28
  };
28
29
 
30
+ /** 2px left-rail stripe color per audit action — reuses the r32 server-logs
31
+ * pattern so failed logins / token rotations spike out of a wall of
32
+ * registers and logins without users reading every chip. Inline hex
33
+ * prevents Tailwind v4 from purging dynamic class names. */
34
+ const ACTION_STRIPE: Record<string, string> = {
35
+ register: '#4ade80',
36
+ login: '#60a5fa',
37
+ login_failed: '#ef4444',
38
+ create_network: '#a78bfa',
39
+ send_task: '#22d3ee',
40
+ };
41
+
29
42
  export default function LogsPage() {
30
43
  const { networkId } = useNetworkId();
31
44
  const [logs, setLogs] = useState<AuditLog[]>([]);
@@ -64,25 +77,52 @@ export default function LogsPage() {
64
77
  return (
65
78
  <div className="min-h-screen bg-[#0a0a1a] text-gray-100 p-4 sm:p-6 font-mono">
66
79
  <div className="flex items-center gap-4 mb-6">
67
- <h1 className="text-2xl font-bold text-white lg:ml-0 ml-10">Logs</h1>
68
- <span className="text-xs bg-gray-900/30 text-gray-400 px-2 py-0.5 rounded-full border border-gray-800/30">
69
- Audit Log
70
- </span>
80
+ <h1 className="text-2xl font-bold text-white lg:ml-0 ml-10">Audit Log</h1>
81
+ {logs.length > 0 && (
82
+ <span className="text-xs bg-blue-900/30 text-blue-400 px-2 py-0.5 rounded-full border border-blue-800/30">
83
+ {logs.length}
84
+ </span>
85
+ )}
71
86
  </div>
72
87
 
73
- <div className="flex flex-wrap gap-3 mb-6">
74
- <select
75
- value={filterAction}
76
- onChange={e => setFilterAction(e.target.value)}
77
- className="bg-[#111128] border border-[#2a2a4a] rounded-lg px-3 py-2 text-sm text-white focus:border-blue-500/50 focus:outline-none"
78
- >
79
- <option value="">All actions</option>
80
- <option value="register">register</option>
81
- <option value="login">login</option>
82
- <option value="login_failed">login_failed</option>
83
- <option value="create_network">create_network</option>
84
- <option value="send_task">send_task</option>
85
- </select>
88
+ {/* Action filter chips (round 43) — same chip pattern as Tasks
89
+ status tabs / Overview agent filter. Each chip carries its
90
+ ACTION_STRIPE dot so the visual key matches the per-row left
91
+ rail. Counts come from the currently loaded log set. */}
92
+ <div className="mb-6 flex flex-wrap items-center gap-1 anet-tabstrip-wrap overflow-x-auto sm:overflow-x-visible">
93
+ {(() => {
94
+ const counts: Record<string, number> = {};
95
+ logs.forEach(l => { counts[l.action] = (counts[l.action] || 0) + 1; });
96
+ const chips: { key: string; label: string; dot?: string }[] = [
97
+ { key: '', label: 'All' },
98
+ { key: 'login', label: 'login', dot: ACTION_STRIPE.login },
99
+ { key: 'login_failed', label: 'login_failed', dot: ACTION_STRIPE.login_failed },
100
+ { key: 'register', label: 'register', dot: ACTION_STRIPE.register },
101
+ { key: 'create_network', label: 'create_network', dot: ACTION_STRIPE.create_network },
102
+ { key: 'send_task', label: 'send_task', dot: ACTION_STRIPE.send_task },
103
+ ];
104
+ return chips.map(c => {
105
+ const count = c.key === '' ? logs.length : counts[c.key] || 0;
106
+ const isActive = filterAction === c.key;
107
+ return (
108
+ <button
109
+ key={c.key || 'all'}
110
+ type="button"
111
+ onClick={() => setFilterAction(c.key)}
112
+ disabled={count === 0 && c.key !== ''}
113
+ className={`flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs transition-colors disabled:opacity-30 disabled:cursor-not-allowed shrink-0 whitespace-nowrap ${
114
+ isActive
115
+ ? 'bg-cyan-500/10 text-cyan-300 border border-cyan-500/30'
116
+ : 'text-gray-500 hover:text-gray-200 hover:bg-[#1a1a2a] border border-transparent'
117
+ }`}
118
+ >
119
+ {c.dot && <span aria-hidden className="inline-block w-1.5 h-1.5 rounded-full" style={{ backgroundColor: c.dot }} />}
120
+ <span>{c.label}</span>
121
+ <span className={`text-[10px] tabular-nums ${isActive ? 'text-cyan-400' : 'text-gray-600'}`}>{count}</span>
122
+ </button>
123
+ );
124
+ });
125
+ })()}
86
126
  </div>
87
127
 
88
128
  {error && (() => {
@@ -125,13 +165,23 @@ export default function LogsPage() {
125
165
  <EmptyState variant="logs" />
126
166
  ) : (
127
167
  <div className="space-y-2">
128
- {logs.map(log => (
129
- <div key={log.id} className="bg-[#111128] border border-[#2a2a4a] rounded-lg px-4 py-3 hover:border-[#3a3a5a] transition-colors">
168
+ {logs.map(log => {
169
+ const userName = log.username || log.user_id;
170
+ return (
171
+ <div key={log.id} className="relative bg-[#111128] border border-[#2a2a4a] rounded-lg pl-4 pr-4 py-3 hover:border-[#3a3a5a] transition-colors overflow-hidden">
172
+ {/* 2px left rail per action (round 33) — failed logins, token
173
+ rotations spike out of a wall of register/login rows. */}
174
+ <span
175
+ aria-hidden
176
+ className="absolute left-0 top-0 bottom-0 w-0.5"
177
+ style={{ backgroundColor: ACTION_STRIPE[log.action] || 'transparent' }}
178
+ />
130
179
  <div className="flex flex-wrap items-center gap-2 mb-1">
131
180
  <span className={`text-xs px-2 py-0.5 rounded-md border ${ACTION_COLORS[log.action] || 'bg-gray-500/10 text-gray-400 border-gray-500/20'}`}>
132
181
  {log.action}
133
182
  </span>
134
- <span className="text-sm text-blue-400 font-medium">{log.username || log.user_id}</span>
183
+ {userName && <AliasAvatar alias={userName} size={16} />}
184
+ <span className="text-sm text-gray-200 font-medium">{userName}</span>
135
185
  {log.target_type && (
136
186
  <>
137
187
  <span className="text-xs text-gray-600">&rarr;</span>
@@ -143,7 +193,8 @@ export default function LogsPage() {
143
193
  {log.detail && <div className="text-xs text-gray-500">{log.detail}</div>}
144
194
  {log.ip && log.ip !== 'unknown' && <div className="text-[10px] text-gray-700">IP: {log.ip}</div>}
145
195
  </div>
146
- ))}
196
+ );
197
+ })}
147
198
  </div>
148
199
  )}
149
200
  </div>
@@ -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('');
@@ -194,16 +206,19 @@ export default function MessagesPage() {
194
206
  <div key={ci} className="bg-[#111128] border border-[#2a2a4a] rounded-xl overflow-hidden">
195
207
  <div className="px-4 py-2.5 border-b border-[#2a2a4a] flex items-center justify-between">
196
208
  <div className="flex items-center gap-2">
197
- <span className="text-sm text-cyan-300 font-medium">{conv.participants[0]}</span>
209
+ <AliasAvatar alias={conv.participants[0]} size={16} />
210
+ <span className="text-sm text-gray-200 font-medium">{conv.participants[0]}</span>
198
211
  <span className="text-gray-600 text-xs">↔</span>
199
- <span className="text-sm text-green-300 font-medium">{conv.participants[1]}</span>
212
+ <AliasAvatar alias={conv.participants[1]} size={16} />
213
+ <span className="text-sm text-gray-200 font-medium">{conv.participants[1]}</span>
200
214
  </div>
201
215
  <span className="text-[10px] text-gray-600">{conv.messages.length} messages</span>
202
216
  </div>
203
217
  <div className="px-4 py-2 space-y-2 max-h-64 overflow-y-auto">
204
218
  {conv.messages.map(m => (
205
219
  <div key={m.id} className="flex items-start gap-2 text-xs py-1">
206
- <span className={`shrink-0 font-medium ${m.from_alias === conv.participants[0] ? 'text-cyan-400' : 'text-green-400'}`}>{m.from_alias}</span>
220
+ {m.from_alias && <AliasAvatar alias={m.from_alias} size={14} />}
221
+ <span className="shrink-0 font-medium text-gray-200">{m.from_alias}</span>
207
222
  <span className="text-gray-400 flex-1">{m.content?.slice(0, 120) || '--'}</span>
208
223
  <span className="text-[9px] text-gray-600 shrink-0">{timeAgo(m.created_at || '')}</span>
209
224
  </div>
@@ -213,13 +228,21 @@ export default function MessagesPage() {
213
228
  ))}
214
229
  </div>
215
230
  ) : (
216
- <div className="space-y-3">
231
+ <div className="space-y-1">
217
232
  {filtered.map((message: MessageItem, index) => {
218
233
  const previous = filtered[index - 1];
219
234
  const gapExceeded = previous
220
235
  ? normalizeDate(message.created_at) - normalizeDate(previous.created_at) > 5 * 60 * 1000
221
236
  : false;
222
237
  const variant = bubbleVariant(message);
238
+ // Cluster: same sender + same direction + same to_alias and no
239
+ // time-gap divider → tighter spacing and hide repeated header.
240
+ const samePrev = !!previous
241
+ && !gapExceeded
242
+ && bubbleVariant(previous) === variant
243
+ && (previous.from_alias || '') === (message.from_alias || '')
244
+ && (previous.to_alias || '') === (message.to_alias || '');
245
+ const fromAlias = message.from_alias || '?';
223
246
 
224
247
  return (
225
248
  <div key={message.id}>
@@ -231,60 +254,71 @@ export default function MessagesPage() {
231
254
  </div>
232
255
  )}
233
256
 
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
- <>
257
+ {/* Broadcasts span full width — no avatar gutter. */}
258
+ {variant === 'broadcast' ? (
259
+ <div className={samePrev ? 'mt-1' : 'mt-3'}>
260
+ <div className="rounded-2xl border border-purple-500/20 bg-purple-500/10 px-4 py-3 shadow-sm w-full">
261
+ <div className="mb-2 flex flex-wrap items-center gap-2">
262
+ <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'}`}>
263
+ {message.type || 'unknown'}
264
+ </span>
265
+ <span className="text-sm font-medium text-purple-200">{fromAlias}</span>
266
+ {message.to_alias && <span className="text-xs text-purple-200/80">to {message.to_alias}</span>}
267
+ {message.priority === 'high' && <span className="text-xs text-red-400">HIGH</span>}
268
+ <span className="ml-auto text-xs text-gray-500">{message.created_at ? timeAgo(message.created_at) : '--'}</span>
269
+ </div>
270
+ <div className="whitespace-pre-wrap text-sm leading-relaxed text-gray-200">
271
+ {renderHighlighted(message.content, search)}
272
+ </div>
273
+ </div>
274
+ </div>
275
+ ) : (
276
+ <div className={`${samePrev ? 'mt-1' : 'mt-3'} flex gap-2 ${variant === 'outgoing' ? 'flex-row-reverse' : 'flex-row'}`}>
277
+ {/* Avatar gutter — fixed width keeps bubble columns aligned even on streaks */}
278
+ <div className="w-8 shrink-0 pt-1">
279
+ {!samePrev && <AliasAvatar alias={fromAlias} size={32} />}
280
+ </div>
281
+ <div className={`min-w-0 max-w-3xl rounded-2xl border px-4 py-3 shadow-sm ${
282
+ variant === 'outgoing'
283
+ ? 'border-green-500/20 bg-green-500/10'
284
+ : 'border-blue-500/20 bg-blue-500/10'
285
+ }`}>
286
+ {!samePrev && (
287
+ <div className="mb-2 flex flex-wrap items-center gap-2">
288
+ <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'}`}>
289
+ {message.type || 'unknown'}
290
+ </span>
291
+ <span className={`text-sm font-medium ${variant === 'outgoing' ? 'text-green-300' : 'text-blue-300'}`}>
292
+ {fromAlias}
293
+ </span>
257
294
  <span className="text-xs text-gray-600">&rarr;</span>
258
295
  <span className="text-sm text-gray-300">{message.to_alias || '--'}</span>
259
- </>
296
+ {message.priority === 'high' && <span className="text-xs text-red-400">HIGH</span>}
297
+ <span className="ml-auto text-xs text-gray-500">{message.created_at ? timeAgo(message.created_at) : '--'}</span>
298
+ </div>
260
299
  )}
261
- {variant === 'broadcast' && message.to_alias && (
262
- <span className="text-xs text-purple-200/80">to {message.to_alias}</span>
300
+
301
+ <div className="whitespace-pre-wrap text-sm leading-relaxed text-gray-200">
302
+ {renderHighlighted(message.content, search)}
303
+ </div>
304
+
305
+ {/* When in a streak, surface the timestamp inline-right so users still know cadence */}
306
+ {samePrev && (
307
+ <div className="mt-1 text-[10px] text-gray-600 text-right">
308
+ {message.created_at ? timeAgo(message.created_at) : ''}
309
+ </div>
263
310
  )}
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
311
 
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
- })()}
312
+ {debug && (
313
+ <div className="mt-3 border-t border-white/10 pt-3 text-xs text-gray-500 space-y-1">
314
+ <div>ID: {message.id}</div>
315
+ {message.task_id && <div>Task ID: {message.task_id}</div>}
316
+ <div>Created: {message.created_at}</div>
317
+ </div>
318
+ )}
277
319
  </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
320
  </div>
287
- </div>
321
+ )}
288
322
  </div>
289
323
  );
290
324
  })}
package/app/node/page.tsx CHANGED
@@ -6,6 +6,7 @@ import Link from 'next/link';
6
6
  import { Suspense } from 'react';
7
7
  import { TaskChatPanel } from '../components/TaskChatPanel';
8
8
  import { timeAgo } from '../components/utils';
9
+ import { AliasAvatar } from '../components/AliasAvatar';
9
10
 
10
11
  interface SessionDetail {
11
12
  resume_id: string;
@@ -150,15 +151,23 @@ function NodeDetailContent() {
150
151
 
151
152
  return (
152
153
  <div className="min-h-screen bg-[#0a0a1a] text-gray-100 p-4 sm:p-6 font-mono">
153
- {/* Header */}
154
- <div className="flex items-center gap-4 mb-6">
155
- <Link href="/nodes" className="text-gray-500 hover:text-gray-300 text-sm lg:ml-0 ml-10">&larr; Nodes</Link>
156
- <div className="flex items-center gap-3">
157
- <span className={`inline-block w-3 h-3 rounded-full ${
158
- sse > 0 ? (session?.status === 'working' ? 'bg-green-500 animate-pulse' : 'bg-emerald-400') : 'bg-gray-500'
159
- }`} />
160
- <h1 className="text-2xl font-bold text-white">{alias}</h1>
161
- <span className={`text-sm ${statusColor}`}>{session?.status || 'unknown'}</span>
154
+ {/* Header — round 40: avatar joins the alias for cross-page hue
155
+ consistency; agent type lives in a subtitle so users know what
156
+ kind of node they're looking at. */}
157
+ <div className="flex items-center gap-3 mb-6 lg:ml-0 ml-10">
158
+ <Link href="/nodes" className="text-gray-500 hover:text-gray-300 text-sm shrink-0">&larr; Nodes</Link>
159
+ <AliasAvatar alias={alias} size={32} />
160
+ <div className="min-w-0 flex-1">
161
+ <div className="flex items-center gap-2">
162
+ <h1 className="text-xl sm:text-2xl font-bold text-white truncate">{alias}</h1>
163
+ <span className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${
164
+ sse > 0 ? (session?.status === 'working' ? 'bg-green-500 animate-pulse' : 'bg-emerald-400') : 'bg-gray-500'
165
+ }`} />
166
+ <span className={`text-[10px] uppercase tracking-wide font-semibold ${statusColor}`}>{session?.status || 'unknown'}</span>
167
+ </div>
168
+ {session?.agent && (
169
+ <div className="text-xs text-gray-500 truncate">{session.agent}{session.server ? <> <span className="text-gray-700 mx-1">·</span> {session.server}</> : null}</div>
170
+ )}
162
171
  </div>
163
172
  </div>
164
173
 
@@ -190,14 +199,22 @@ function NodeFullPanel({ alias, session, sse, sendMsg, setSendMsg, sending, send
190
199
 
191
200
  return (
192
201
  <div className="bg-[#0d0d1a] border border-[#2a2a4a] rounded-xl overflow-hidden h-[calc(100vh-140px)] flex flex-col">
193
- {/* Tab bar */}
202
+ {/* Tab bar — round 40: emoji icons (💬 📋 📊) replaced with stroke
203
+ SVG to drop the "AI generated" tell. */}
194
204
  <div className="flex border-b border-[#2a2a4a] bg-[#0a0a15] shrink-0">
195
- {(['chat', 'events', 'info'] as const).map(t => (
196
- <button key={t} onClick={() => setTab(t)}
197
- className={`px-5 py-3 text-sm font-medium border-b-2 transition-colors ${
198
- tab === t ? 'border-cyan-400 text-cyan-300' : 'border-transparent text-gray-500 hover:text-gray-300'
205
+ {([
206
+ { id: 'chat', label: 'Chat', icon: 'M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z' },
207
+ { id: 'events', label: 'Events', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z M14 2v6h6 M16 13H8 M16 17H8 M10 9H8' },
208
+ { id: 'info', label: 'Info', icon: 'M12 21a9 9 0 100-18 9 9 0 000 18z M12 8h.01 M11 12h1v4h1' },
209
+ ] as const).map(t => (
210
+ <button key={t.id} onClick={() => setTab(t.id)}
211
+ className={`flex items-center gap-2 px-5 py-3 text-sm font-medium border-b-2 transition-colors ${
212
+ tab === t.id ? 'border-cyan-400 text-cyan-300' : 'border-transparent text-gray-500 hover:text-gray-300'
199
213
  }`}>
200
- {t === 'chat' ? '💬 Chat' : t === 'events' ? '📋 Events' : '📊 Info'}
214
+ <svg className="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
215
+ <path d={t.icon} />
216
+ </svg>
217
+ <span>{t.label}</span>
201
218
  </button>
202
219
  ))}
203
220
  </div>
@@ -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)}