@sleep2agi/agent-network-dashboard 0.4.5-preview.2 → 0.4.5-preview.20

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 (233) 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 +41 -41
  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.js.nft.json +1 -1
  48. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  49. package/.next/server/app/logs.html +2 -2
  50. package/.next/server/app/logs.rsc +14 -14
  51. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  52. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  53. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  54. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  55. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  56. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  57. package/.next/server/app/messages/page.js.nft.json +1 -1
  58. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  59. package/.next/server/app/messages.html +2 -2
  60. package/.next/server/app/messages.rsc +14 -14
  61. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  62. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  63. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  64. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  65. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  66. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  67. package/.next/server/app/node/page.js.nft.json +1 -1
  68. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  69. package/.next/server/app/node.html +2 -2
  70. package/.next/server/app/node.rsc +14 -14
  71. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  72. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  73. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  74. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  75. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  76. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  77. package/.next/server/app/nodes/page.js.nft.json +1 -1
  78. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  79. package/.next/server/app/nodes.html +2 -2
  80. package/.next/server/app/nodes.rsc +14 -14
  81. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  82. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  83. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  84. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  85. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  86. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  87. package/.next/server/app/page.js.nft.json +1 -1
  88. package/.next/server/app/page_client-reference-manifest.js +1 -1
  89. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  90. package/.next/server/app/server-logs.html +2 -2
  91. package/.next/server/app/server-logs.rsc +14 -14
  92. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  93. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  94. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  95. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  96. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  97. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  98. package/.next/server/app/settings/networks/page.js.nft.json +1 -1
  99. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  100. package/.next/server/app/settings/networks.html +2 -2
  101. package/.next/server/app/settings/networks.rsc +14 -14
  102. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  103. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  104. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  105. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  106. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  107. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  108. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  109. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  110. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  111. package/.next/server/app/settings/tokens.html +2 -2
  112. package/.next/server/app/settings/tokens.rsc +14 -14
  113. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  114. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  115. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  116. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  117. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  118. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  119. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  120. package/.next/server/app/settings.html +2 -2
  121. package/.next/server/app/settings.rsc +14 -14
  122. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  123. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  124. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  125. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  126. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  127. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  128. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  129. package/.next/server/app/tasks/page.js.nft.json +1 -1
  130. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  131. package/.next/server/app/tasks.html +2 -2
  132. package/.next/server/app/tasks.rsc +14 -14
  133. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  134. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  135. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  136. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  137. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  138. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  139. package/.next/server/chunks/ssr/[root-of-the-server]__02ee4km._.js +3 -0
  140. package/.next/server/chunks/ssr/[root-of-the-server]__02ee4km._.js.map +1 -0
  141. package/.next/server/chunks/ssr/[root-of-the-server]__0jjqrcf._.js +3 -0
  142. package/.next/server/chunks/ssr/[root-of-the-server]__0jjqrcf._.js.map +1 -0
  143. package/.next/server/chunks/ssr/[root-of-the-server]__0o8yj3c._.js +3 -0
  144. package/.next/server/chunks/ssr/[root-of-the-server]__0o8yj3c._.js.map +1 -0
  145. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js +1 -1
  146. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js.map +1 -1
  147. package/.next/server/chunks/ssr/_0nne7mz._.js +3 -0
  148. package/.next/server/chunks/ssr/_0nne7mz._.js.map +1 -0
  149. package/.next/server/chunks/ssr/app_0.zbtn5._.js +9 -0
  150. package/.next/server/chunks/ssr/app_0.zbtn5._.js.map +1 -0
  151. package/.next/server/chunks/ssr/app_04n78pr._.js +3 -0
  152. package/.next/server/chunks/ssr/app_04n78pr._.js.map +1 -0
  153. package/.next/server/chunks/ssr/app_0_agk0r._.js +3 -0
  154. package/.next/server/chunks/ssr/app_0_agk0r._.js.map +1 -0
  155. package/.next/server/chunks/ssr/app_0dk5wao._.js +1 -1
  156. package/.next/server/chunks/ssr/app_0dk5wao._.js.map +1 -1
  157. package/.next/server/chunks/ssr/app_components_08ys5y3._.js +2 -2
  158. package/.next/server/chunks/ssr/app_components_08ys5y3._.js.map +1 -1
  159. package/.next/server/chunks/ssr/app_components_0lhp_k.._.js +3 -0
  160. package/.next/server/chunks/ssr/app_components_0lhp_k.._.js.map +1 -0
  161. package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js +1 -1
  162. package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js.map +1 -1
  163. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js +1 -1
  164. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js.map +1 -1
  165. package/.next/server/middleware-build-manifest.js +3 -3
  166. package/.next/server/pages/404.html +2 -2
  167. package/.next/server/pages/500.html +1 -1
  168. package/.next/server/server-reference-manifest.js +1 -1
  169. package/.next/server/server-reference-manifest.json +1 -1
  170. package/.next/static/chunks/00-ce-5_uu76b.js +1 -0
  171. package/.next/static/chunks/09i1e1zou_kxn.css +2 -0
  172. package/.next/static/chunks/{07okhs5561y5y.js → 0c1bp~g7.8ru..js} +1 -1
  173. package/.next/static/chunks/0eajktwdxy7qt.js +1 -0
  174. package/.next/static/chunks/0hqhvq6f83m38.js +1 -0
  175. package/.next/static/chunks/0l3gf0ickq445.js +1 -0
  176. package/.next/static/chunks/0pf4_9.-8s34p.js +1 -0
  177. package/.next/static/chunks/0q4rltx2ngh5m.js +1 -0
  178. package/.next/static/chunks/0qnpc7rbxn12u.js +1 -0
  179. package/.next/static/chunks/10-ka4k4ni9u2.js +1 -0
  180. package/.next/static/chunks/15nn5pyy3l5zo.js +7 -0
  181. package/.next/static/chunks/169v~dd1tdxdm.js +7 -0
  182. package/.next/static/chunks/17uwnuk~0zoo3.js +1 -0
  183. package/.next/trace +2 -2
  184. package/.next/trace-build +1 -1
  185. package/app/components/AgentCard.tsx +6 -2
  186. package/app/components/AliasAvatar.tsx +51 -0
  187. package/app/components/CommandPalette.tsx +53 -20
  188. package/app/components/HelpOverlay.tsx +50 -10
  189. package/app/components/Sidebar.tsx +40 -35
  190. package/app/components/TaskChatPanel.tsx +8 -10
  191. package/app/components/TaskDrawer.tsx +62 -23
  192. package/app/components/TopoGraph.tsx +16 -3
  193. package/app/globals.css +129 -0
  194. package/app/logs/page.tsx +28 -4
  195. package/app/messages/page.tsx +79 -48
  196. package/app/nodes/page.tsx +21 -23
  197. package/app/page.tsx +66 -13
  198. package/app/server-logs/page.tsx +37 -3
  199. package/app/settings/networks/page.tsx +37 -13
  200. package/app/settings/page.tsx +33 -8
  201. package/app/tasks/page.tsx +95 -35
  202. package/package.json +1 -1
  203. package/.next/server/chunks/ssr/[root-of-the-server]__0_jnflc._.js +0 -3
  204. package/.next/server/chunks/ssr/[root-of-the-server]__0_jnflc._.js.map +0 -1
  205. package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js +0 -3
  206. package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js.map +0 -1
  207. package/.next/server/chunks/ssr/[root-of-the-server]__12mpoig._.js +0 -3
  208. package/.next/server/chunks/ssr/[root-of-the-server]__12mpoig._.js.map +0 -1
  209. package/.next/server/chunks/ssr/_0d-fuox._.js +0 -3
  210. package/.next/server/chunks/ssr/_0d-fuox._.js.map +0 -1
  211. package/.next/server/chunks/ssr/app_0.0wpa~._.js +0 -3
  212. package/.next/server/chunks/ssr/app_0.0wpa~._.js.map +0 -1
  213. package/.next/server/chunks/ssr/app_0et5yua._.js +0 -3
  214. package/.next/server/chunks/ssr/app_0et5yua._.js.map +0 -1
  215. package/.next/server/chunks/ssr/app_0ff~kjj._.js +0 -9
  216. package/.next/server/chunks/ssr/app_0ff~kjj._.js.map +0 -1
  217. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js +0 -3
  218. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js.map +0 -1
  219. package/.next/static/chunks/06t_vzei8s9_h.js +0 -1
  220. package/.next/static/chunks/0b1ymo-btjfha.js +0 -1
  221. package/.next/static/chunks/0e7c40p~wq665.js +0 -1
  222. package/.next/static/chunks/0g7m7zk.v7g6h.js +0 -1
  223. package/.next/static/chunks/0khpyxpdrp~ty.js +0 -1
  224. package/.next/static/chunks/0lo_g.q1fm-hr.js +0 -7
  225. package/.next/static/chunks/0ogeals5ufhho.js +0 -1
  226. package/.next/static/chunks/0qgc758gz3on4.js +0 -7
  227. package/.next/static/chunks/0xxk2ms76kkuz.css +0 -2
  228. package/.next/static/chunks/1031v6nw33ud3.js +0 -1
  229. package/.next/static/chunks/10njj7kn9ylkf.js +0 -1
  230. package/.next/static/chunks/13.dst7liziwo.js +0 -1
  231. /package/.next/static/{dvjBKNMRovte6RFP_k8Lq → MBLKeNzIT_BLWGkgUbmPI}/_buildManifest.js +0 -0
  232. /package/.next/static/{dvjBKNMRovte6RFP_k8Lq → MBLKeNzIT_BLWGkgUbmPI}/_clientMiddlewareManifest.js +0 -0
  233. /package/.next/static/{dvjBKNMRovte6RFP_k8Lq → MBLKeNzIT_BLWGkgUbmPI}/_ssgManifest.js +0 -0
@@ -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
  ))}
@@ -471,14 +475,12 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
471
475
 
472
476
  return (
473
477
  <>
474
- <div className="fixed inset-0 bg-black/30 z-40 lg:hidden" onClick={onClose} />
478
+ <div className="fixed inset-0 bg-black/30 z-40 lg:hidden anet-fade-in" onClick={onClose} />
475
479
  <div className="fixed top-0 right-0 h-full w-full lg:w-[500px] bg-[#0a0a1a] border-l border-[#2a2a4a] z-50 flex flex-col shadow-2xl shadow-black/60 animate-slide-in">
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>
@@ -492,10 +494,6 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
492
494
  </div>
493
495
  {chatContent}
494
496
  </div>
495
- <style jsx global>{`
496
- @keyframes slide-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
497
- .animate-slide-in { animation: slide-in 0.2s ease-out; }
498
- `}</style>
499
497
  </>
500
498
  );
501
499
  }
@@ -58,12 +58,20 @@ export function TaskDrawer({ taskId, onClose }: TaskDrawerProps) {
58
58
  })();
59
59
  }, [taskId]);
60
60
 
61
+ /** Timeline steps — same 4 hops every task goes through. `time` is set
62
+ * when the hop has happened; missing time means pending. Round 36 adds
63
+ * `key` for the "current step" highlight + the `done` flag for layout. */
61
64
  const timeline = task ? [
62
- { label: 'Created', time: task.created_at, color: 'bg-gray-400' },
63
- { label: 'Delivered', time: task.delivered_at, color: 'bg-blue-400' },
64
- { label: 'Started', time: task.started_at, color: 'bg-green-400' },
65
- { label: 'Completed', time: task.completed_at, color: 'bg-purple-400' },
66
- ] : [];
65
+ { key: 'created', label: 'Created', time: task.created_at, color: 'bg-gray-400' },
66
+ { key: 'delivered', label: 'Delivered', time: task.delivered_at, color: 'bg-blue-400' },
67
+ { key: 'started', label: 'Started', time: task.started_at, color: 'bg-green-400' },
68
+ { key: 'completed', label: 'Completed', time: task.completed_at, color: 'bg-purple-400'},
69
+ ].map(s => ({ ...s, done: !!s.time })) : [];
70
+
71
+ /** Index of the highest-completed step. The step *after* this index is
72
+ * the "current" one (in-progress). When all are done, currentIdx = -1. */
73
+ const currentStepIdx = timeline.findIndex(s => !s.done);
74
+ const isActive = task && task.status !== 'completed' && task.status !== 'failed' && task.status !== 'expired' && task.status !== 'cancelled';
67
75
 
68
76
  const duration = task?.started_at && task?.completed_at
69
77
  ? Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 1000)
@@ -71,7 +79,7 @@ export function TaskDrawer({ taskId, onClose }: TaskDrawerProps) {
71
79
 
72
80
  return (
73
81
  <>
74
- <div className="fixed inset-0 bg-black/30 z-40" onClick={onClose} />
82
+ <div className="fixed inset-0 bg-black/30 z-40 anet-fade-in" onClick={onClose} />
75
83
  <div className="fixed top-0 right-0 h-full w-full lg:w-[500px] bg-[#0a0a1a] border-l border-[#2a2a4a] z-50 flex flex-col shadow-2xl shadow-black/60 overflow-y-auto animate-slide-in">
76
84
  {/* Header */}
77
85
  <div className="flex items-center justify-between px-5 py-4 border-b border-[#2a2a4a] bg-[#0d0d1a] sticky top-0">
@@ -112,22 +120,57 @@ export function TaskDrawer({ taskId, onClose }: TaskDrawerProps) {
112
120
  <span className="text-cyan-400 font-medium">{task.to_name || '--'}</span>
113
121
  </div>
114
122
 
115
- {/* Timeline */}
123
+ {/* Timeline — round 36 polish: relative timestamps, current-step
124
+ pulse if task is still in flight, full ISO in title=. */}
116
125
  <div className="bg-[#111128] border border-[#2a2a4a] rounded-xl p-4">
117
- <div className="text-xs text-gray-500 uppercase mb-3">Timeline</div>
126
+ <div className="flex items-center justify-between mb-3">
127
+ <div className="text-xs text-gray-500 uppercase tracking-wide">Timeline</div>
128
+ {duration !== null && (
129
+ <div className="text-[10px] text-gray-600">
130
+ {duration < 60 ? `${duration}s` : `${Math.floor(duration/60)}m ${duration%60}s`} runtime
131
+ </div>
132
+ )}
133
+ </div>
118
134
  <div className="space-y-3">
119
- {timeline.map((step, i) => (
120
- <div key={step.label} className="flex items-center gap-3">
121
- <div className="flex flex-col items-center">
122
- <div className={`w-3 h-3 rounded-full ${step.time ? step.color : 'bg-gray-700'}`} />
123
- {i < timeline.length - 1 && <div className={`w-0.5 h-4 mt-1 ${step.time && timeline[i+1]?.time ? 'bg-gray-600' : 'bg-gray-800'}`} />}
135
+ {timeline.map((step, i) => {
136
+ const isCurrent = isActive && i === currentStepIdx;
137
+ const isNextDone = !!timeline[i+1]?.done;
138
+ return (
139
+ <div key={step.key} className="flex items-start gap-3">
140
+ <div className="flex flex-col items-center pt-0.5">
141
+ <span
142
+ className={`relative w-3 h-3 rounded-full shrink-0 ${
143
+ step.done ? step.color : 'bg-gray-700'
144
+ }`}
145
+ >
146
+ {isCurrent && (
147
+ <span
148
+ aria-hidden
149
+ className="absolute -inset-1 rounded-full border border-current opacity-60 anet-current-step-pulse"
150
+ style={{ borderColor: 'currentColor' }}
151
+ />
152
+ )}
153
+ </span>
154
+ {i < timeline.length - 1 && (
155
+ <div className={`w-0.5 h-5 mt-1 ${
156
+ step.done && isNextDone ? 'bg-gray-500' : step.done ? 'bg-gradient-to-b from-gray-500 to-gray-800' : 'bg-gray-800'
157
+ }`} />
158
+ )}
159
+ </div>
160
+ <div className="flex-1 min-w-0">
161
+ <div className={`text-xs font-medium flex items-center gap-2 ${
162
+ step.done ? 'text-gray-200' : isCurrent ? 'text-cyan-300' : 'text-gray-600'
163
+ }`}>
164
+ <span>{step.label}</span>
165
+ {isCurrent && <span className="text-[9px] uppercase tracking-wide text-cyan-400">in&nbsp;progress</span>}
166
+ </div>
167
+ <div className="text-[10px] text-gray-500" title={step.time || undefined}>
168
+ {step.time ? timeAgo(step.time) : isCurrent ? '—' : 'Pending'}
169
+ </div>
170
+ </div>
124
171
  </div>
125
- <div className="flex-1">
126
- <div className={`text-xs font-medium ${step.time ? 'text-gray-300' : 'text-gray-600'}`}>{step.label}</div>
127
- <div className="text-[10px] text-gray-500">{step.time || 'Pending'}</div>
128
- </div>
129
- </div>
130
- ))}
172
+ );
173
+ })}
131
174
  </div>
132
175
  </div>
133
176
 
@@ -172,10 +215,6 @@ export function TaskDrawer({ taskId, onClose }: TaskDrawerProps) {
172
215
  </div>
173
216
  )}
174
217
  </div>
175
- <style jsx global>{`
176
- @keyframes slide-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
177
- .animate-slide-in { animation: slide-in 0.2s ease-out; }
178
- `}</style>
179
218
  </>
180
219
  );
181
220
  }
@@ -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]) {
@@ -748,3 +811,69 @@ body {
748
811
  color: #92400e !important;
749
812
  border-color: rgba(217, 119, 6, 0.28) !important;
750
813
  }
814
+
815
+ /* Round 30 — global drawer entrance keyframes so TaskDrawer, TaskChatPanel,
816
+ and any future right-side panel share one source. Previously the
817
+ keyframe lived inside TaskChatPanel's <style jsx global>; if that
818
+ component never mounted, the keyframe was missing for TaskDrawer. */
819
+ @keyframes anet-slide-in {
820
+ from { transform: translateX(100%); }
821
+ to { transform: translateX(0); }
822
+ }
823
+ .animate-slide-in { animation: anet-slide-in 0.2s ease-out; }
824
+
825
+ @keyframes anet-fade-in {
826
+ from { opacity: 0; }
827
+ to { opacity: 1; }
828
+ }
829
+ .anet-fade-in { animation: anet-fade-in 0.15s ease-out; }
830
+
831
+ /* Round 36 — current-step ring pulse on TaskDrawer timeline. Halo gently
832
+ breathes around the active step's dot so users can spot "task is here". */
833
+ @keyframes anet-current-step-pulse-kf {
834
+ 0%, 100% { transform: scale(1); opacity: 0.6; }
835
+ 50% { transform: scale(1.25); opacity: 0.0; }
836
+ }
837
+ .anet-current-step-pulse {
838
+ animation: anet-current-step-pulse-kf 1.6s ease-in-out infinite;
839
+ }
840
+
841
+ /* ─────────────────────────────────────────────────────────────────────
842
+ Round 29 — prefers-reduced-motion a11y sweep
843
+
844
+ Rounds 19~27 accumulated a lot of small animations: Tasks row 0fr↔1fr
845
+ expand, chevron rotate, hover bg shifts, Cmd+K popover, TaskChatPanel
846
+ slide-in, brand/skeleton pulses already had their own guards.
847
+
848
+ For users who set system "reduce motion": neutralise everything in one
849
+ pass. Don't kill state changes outright — only their durations and
850
+ keyframes. Hover and focus colours still apply, just instantly.
851
+ ───────────────────────────────────────────────────────────────────── */
852
+ @media (prefers-reduced-motion: reduce) {
853
+ *,
854
+ *::before,
855
+ *::after {
856
+ animation-duration: 0.001ms !important;
857
+ animation-iteration-count: 1 !important;
858
+ transition-duration: 0.001ms !important;
859
+ scroll-behavior: auto !important;
860
+ }
861
+ /* Skeleton pulse already has its own guard; harden in case the bar
862
+ class is reused without anet-skeleton-pulse. */
863
+ .anet-skeleton-bar {
864
+ animation: none !important;
865
+ }
866
+ /* Brand pulse hidden — green/red dot still indicates state via color. */
867
+ .anet-brand-pulse {
868
+ animation: none !important;
869
+ }
870
+ /* Drawer entrance kills — keyframes promoted to globals in r30. */
871
+ .animate-slide-in,
872
+ .anet-fade-in,
873
+ .anet-current-step-pulse {
874
+ animation: none !important;
875
+ }
876
+ /* Tasks row grid 0fr↔1fr keep working (it's the toggle mechanism),
877
+ but the smooth transition just snaps. Already handled via the
878
+ blanket transition-duration override above. */
879
+ }
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[]>([]);
@@ -125,13 +138,23 @@ export default function LogsPage() {
125
138
  <EmptyState variant="logs" />
126
139
  ) : (
127
140
  <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">
141
+ {logs.map(log => {
142
+ const userName = log.username || log.user_id;
143
+ return (
144
+ <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">
145
+ {/* 2px left rail per action (round 33) — failed logins, token
146
+ rotations spike out of a wall of register/login rows. */}
147
+ <span
148
+ aria-hidden
149
+ className="absolute left-0 top-0 bottom-0 w-0.5"
150
+ style={{ backgroundColor: ACTION_STRIPE[log.action] || 'transparent' }}
151
+ />
130
152
  <div className="flex flex-wrap items-center gap-2 mb-1">
131
153
  <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
154
  {log.action}
133
155
  </span>
134
- <span className="text-sm text-blue-400 font-medium">{log.username || log.user_id}</span>
156
+ {userName && <AliasAvatar alias={userName} size={16} />}
157
+ <span className="text-sm text-gray-200 font-medium">{userName}</span>
135
158
  {log.target_type && (
136
159
  <>
137
160
  <span className="text-xs text-gray-600">&rarr;</span>
@@ -143,7 +166,8 @@ export default function LogsPage() {
143
166
  {log.detail && <div className="text-xs text-gray-500">{log.detail}</div>}
144
167
  {log.ip && log.ip !== 'unknown' && <div className="text-[10px] text-gray-700">IP: {log.ip}</div>}
145
168
  </div>
146
- ))}
169
+ );
170
+ })}
147
171
  </div>
148
172
  )}
149
173
  </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('');
@@ -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
  })}