@sleep2agi/agent-network-dashboard 0.5.7-preview.1 → 0.5.7-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 (250) 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 +75 -60
  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 +13 -13
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  17. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  18. package/.next/server/app/_not-found.segments/_index.segment.rsc +8 -8
  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 +15 -15
  25. package/.next/server/app/admin.segments/_full.segment.rsc +15 -15
  26. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  27. package/.next/server/app/admin.segments/_index.segment.rsc +8 -8
  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/api/hub/session/route.js +1 -1
  32. package/.next/server/app/api/hub/session/route.js.nft.json +1 -1
  33. package/.next/server/app/api/hub/status/route.js +1 -1
  34. package/.next/server/app/api/hub/status/route.js.nft.json +1 -1
  35. package/.next/server/app/index.html +2 -2
  36. package/.next/server/app/index.rsc +15 -15
  37. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  38. package/.next/server/app/index.segments/_full.segment.rsc +15 -15
  39. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  40. package/.next/server/app/index.segments/_index.segment.rsc +8 -8
  41. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  42. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  43. package/.next/server/app/login.html +2 -2
  44. package/.next/server/app/login.rsc +15 -15
  45. package/.next/server/app/login.segments/_full.segment.rsc +15 -15
  46. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  47. package/.next/server/app/login.segments/_index.segment.rsc +8 -8
  48. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  49. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  50. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  51. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  52. package/.next/server/app/logs.html +2 -2
  53. package/.next/server/app/logs.rsc +15 -15
  54. package/.next/server/app/logs.segments/_full.segment.rsc +15 -15
  55. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  56. package/.next/server/app/logs.segments/_index.segment.rsc +8 -8
  57. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  58. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  59. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  60. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  61. package/.next/server/app/messages.html +2 -2
  62. package/.next/server/app/messages.rsc +15 -15
  63. package/.next/server/app/messages.segments/_full.segment.rsc +15 -15
  64. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  65. package/.next/server/app/messages.segments/_index.segment.rsc +8 -8
  66. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  67. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  68. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  69. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  70. package/.next/server/app/node.html +2 -2
  71. package/.next/server/app/node.rsc +15 -15
  72. package/.next/server/app/node.segments/_full.segment.rsc +15 -15
  73. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  74. package/.next/server/app/node.segments/_index.segment.rsc +8 -8
  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_client-reference-manifest.js +1 -1
  79. package/.next/server/app/nodes.html +2 -2
  80. package/.next/server/app/nodes.rsc +15 -15
  81. package/.next/server/app/nodes.segments/_full.segment.rsc +15 -15
  82. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  83. package/.next/server/app/nodes.segments/_index.segment.rsc +8 -8
  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_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 +15 -15
  91. package/.next/server/app/server-logs.segments/_full.segment.rsc +15 -15
  92. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  93. package/.next/server/app/server-logs.segments/_index.segment.rsc +8 -8
  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/servers/page_client-reference-manifest.js +1 -1
  98. package/.next/server/app/servers.html +2 -2
  99. package/.next/server/app/servers.rsc +15 -15
  100. package/.next/server/app/servers.segments/_full.segment.rsc +15 -15
  101. package/.next/server/app/servers.segments/_head.segment.rsc +4 -4
  102. package/.next/server/app/servers.segments/_index.segment.rsc +8 -8
  103. package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
  104. package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +4 -4
  105. package/.next/server/app/servers.segments/servers.segment.rsc +3 -3
  106. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  107. package/.next/server/app/settings/networks.html +2 -2
  108. package/.next/server/app/settings/networks.rsc +15 -15
  109. package/.next/server/app/settings/networks.segments/_full.segment.rsc +15 -15
  110. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  111. package/.next/server/app/settings/networks.segments/_index.segment.rsc +8 -8
  112. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  113. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  114. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  115. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  116. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  117. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  118. package/.next/server/app/settings/tokens.html +2 -2
  119. package/.next/server/app/settings/tokens.rsc +15 -15
  120. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +15 -15
  121. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  122. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +8 -8
  123. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  124. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  125. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  126. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  127. package/.next/server/app/settings.html +2 -2
  128. package/.next/server/app/settings.rsc +15 -15
  129. package/.next/server/app/settings.segments/_full.segment.rsc +15 -15
  130. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  131. package/.next/server/app/settings.segments/_index.segment.rsc +8 -8
  132. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  133. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  134. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  135. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  136. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  137. package/.next/server/app/tasks.html +2 -2
  138. package/.next/server/app/tasks.rsc +15 -15
  139. package/.next/server/app/tasks.segments/_full.segment.rsc +15 -15
  140. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  141. package/.next/server/app/tasks.segments/_index.segment.rsc +8 -8
  142. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  143. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  144. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  145. package/.next/server/chunks/[root-of-the-server]__0-29ynz._.js +3 -0
  146. package/.next/server/chunks/[root-of-the-server]__0-29ynz._.js.map +1 -0
  147. package/.next/server/chunks/[root-of-the-server]__0lhq4ha._.js +3 -0
  148. package/.next/server/chunks/[root-of-the-server]__0lhq4ha._.js.map +1 -0
  149. package/.next/server/chunks/ssr/[root-of-the-server]__030vg4n._.js +2 -2
  150. package/.next/server/chunks/ssr/[root-of-the-server]__030vg4n._.js.map +1 -1
  151. package/.next/server/chunks/ssr/[root-of-the-server]__0fhoq8i._.js +1 -1
  152. package/.next/server/chunks/ssr/[root-of-the-server]__0fhoq8i._.js.map +1 -1
  153. package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js +1 -1
  154. package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js.map +1 -1
  155. package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js +1 -1
  156. package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js.map +1 -1
  157. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -3
  158. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  159. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  160. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  161. package/.next/server/chunks/ssr/agent-network-dashboard_app_057q.ne._.js +1 -1
  162. package/.next/server/chunks/ssr/agent-network-dashboard_app_057q.ne._.js.map +1 -1
  163. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  164. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  165. package/.next/server/chunks/ssr/agent-network-dashboard_app_0i3759l._.js +1 -1
  166. package/.next/server/chunks/ssr/agent-network-dashboard_app_0i3759l._.js.map +1 -1
  167. package/.next/server/chunks/ssr/agent-network-dashboard_app_10hjgv4._.js +1 -1
  168. package/.next/server/chunks/ssr/agent-network-dashboard_app_10hjgv4._.js.map +1 -1
  169. package/.next/server/chunks/ssr/agent-network-dashboard_app_1153xeb._.js +2 -2
  170. package/.next/server/chunks/ssr/agent-network-dashboard_app_1153xeb._.js.map +1 -1
  171. package/.next/server/chunks/ssr/agent-network-dashboard_app_12l4oto._.js +1 -1
  172. package/.next/server/chunks/ssr/agent-network-dashboard_app_12l4oto._.js.map +1 -1
  173. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0s5uqlp._.js +1 -1
  174. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0s5uqlp._.js.map +1 -1
  175. package/.next/server/chunks/ssr/agent-network-dashboard_app_server-logs_page_tsx_0dg.l_8._.js +1 -1
  176. package/.next/server/chunks/ssr/agent-network-dashboard_app_server-logs_page_tsx_0dg.l_8._.js.map +1 -1
  177. package/.next/server/chunks/ssr/agent-network-dashboard_app_tasks_page_tsx_0mwxy4z._.js +1 -1
  178. package/.next/server/chunks/ssr/agent-network-dashboard_app_tasks_page_tsx_0mwxy4z._.js.map +1 -1
  179. package/.next/server/middleware-build-manifest.js +3 -3
  180. package/.next/server/pages/404.html +2 -2
  181. package/.next/server/pages/500.html +1 -1
  182. package/.next/server/server-reference-manifest.js +1 -1
  183. package/.next/server/server-reference-manifest.json +1 -1
  184. package/.next/static/chunks/00hpbc4_15cxb.js +1 -0
  185. package/.next/static/chunks/03~5pxwbxxw-b.js +1 -0
  186. package/.next/static/chunks/04koh~08gofyy.js +1 -0
  187. package/.next/static/chunks/07xy.c.pwy1q_.js +7 -0
  188. package/.next/static/chunks/082m-ou~6nopx.js +1 -0
  189. package/.next/static/chunks/0_d.vbw._9_xf.css +1 -0
  190. package/.next/static/chunks/0a.9~-nf0gpec.js +1 -0
  191. package/.next/static/chunks/0cmfkyuf7x3ip.js +4 -0
  192. package/.next/static/chunks/0dq~~c79pqq8g.js +1 -0
  193. package/.next/static/chunks/0im751o4n61c7.js +1 -0
  194. package/.next/static/chunks/0jp~cs9-zkmqa.js +4 -0
  195. package/.next/static/chunks/0ku0fjqlm9mca.js +1 -0
  196. package/.next/static/chunks/0u240x6paxno2.js +1 -0
  197. package/.next/static/chunks/{0.66f3.rtcybb.js → 136r0ae9ihgvo.js} +1 -1
  198. package/.next/static/chunks/14ngt0l293vrr.js +1 -0
  199. package/.next/static/chunks/14q8_9i5izcwj.js +7 -0
  200. package/.next/static/chunks/181u38qblp8lz.js +1 -0
  201. package/.next/trace +2 -2
  202. package/.next/trace-build +1 -1
  203. package/app/admin/page.tsx +8 -3
  204. package/app/api/hub/session/route.ts +3 -1
  205. package/app/api/hub/status/route.ts +3 -0
  206. package/app/components/AgentCard.tsx +19 -9
  207. package/app/components/AliasAvatar.tsx +22 -0
  208. package/app/components/CommandPalette.tsx +1 -1
  209. package/app/components/DispatchPanel.tsx +2 -2
  210. package/app/components/HealthBanner.tsx +10 -2
  211. package/app/components/TaskChatPanel.tsx +8 -0
  212. package/app/components/TopoGraph.tsx +23 -0
  213. package/app/globals.css +24 -0
  214. package/app/lib/chat-unread.ts +158 -0
  215. package/app/lib/session-normalize.ts +17 -0
  216. package/app/lib/vendorIdentity.ts +23 -3
  217. package/app/messages/page.tsx +18 -11
  218. package/app/node/page.tsx +9 -1
  219. package/app/nodes/page.tsx +33 -9
  220. package/app/server-logs/page.tsx +10 -3
  221. package/app/settings/page.tsx +6 -3
  222. package/app/tasks/page.tsx +13 -9
  223. package/bin/start.js +0 -0
  224. package/package.json +1 -1
  225. package/public/vendors/grok.svg +5 -0
  226. package/.next/cache/.previewinfo +0 -1
  227. package/.next/cache/.rscinfo +0 -1
  228. package/.next/cache/config.json +0 -7
  229. package/.next/server/chunks/[root-of-the-server]__0-g~15e._.js +0 -3
  230. package/.next/server/chunks/[root-of-the-server]__0-g~15e._.js.map +0 -1
  231. package/.next/server/chunks/[root-of-the-server]__0hcnyp.._.js +0 -3
  232. package/.next/server/chunks/[root-of-the-server]__0hcnyp.._.js.map +0 -1
  233. package/.next/static/chunks/0-g7_e.uxknwf.css +0 -1
  234. package/.next/static/chunks/08kf-.bgs10ze.js +0 -1
  235. package/.next/static/chunks/0glmrdj9112tc.js +0 -7
  236. package/.next/static/chunks/0milchnxu3pma.js +0 -1
  237. package/.next/static/chunks/0oo6ou1f1vfaj.js +0 -1
  238. package/.next/static/chunks/0ps~rywjn70v6.js +0 -4
  239. package/.next/static/chunks/0qwqp6ulyj3o6.js +0 -1
  240. package/.next/static/chunks/0rxrez_cjn.eh.js +0 -1
  241. package/.next/static/chunks/0uzchut_f9a6m.js +0 -1
  242. package/.next/static/chunks/0wyjrc0bekhiz.js +0 -1
  243. package/.next/static/chunks/0x8x.29hkju5a.js +0 -1
  244. package/.next/static/chunks/0~dcueyvq.4ra.js +0 -1
  245. package/.next/static/chunks/15-ltfhot3b4n.js +0 -7
  246. package/.next/static/chunks/174re5ar~kxne.js +0 -1
  247. package/.next/static/chunks/17r9h6cx1w6q-.js +0 -4
  248. /package/.next/static/{TvIJHvMV6_ETYDMkMySqd → fBBOqrPbHswdz_woUXk6R}/_buildManifest.js +0 -0
  249. /package/.next/static/{TvIJHvMV6_ETYDMkMySqd → fBBOqrPbHswdz_woUXk6R}/_clientMiddlewareManifest.js +0 -0
  250. /package/.next/static/{TvIJHvMV6_ETYDMkMySqd → fBBOqrPbHswdz_woUXk6R}/_ssgManifest.js +0 -0
@@ -1,5 +1,6 @@
1
1
  import { requireDashboardAuth } from '@/app/lib/dashboard-auth';
2
2
  import { hubFetch } from '@/app/lib/hub';
3
+ import { normalizeSessions } from '@/app/lib/session-normalize';
3
4
 
4
5
  export async function GET(req: Request) {
5
6
  const authFailure = await requireDashboardAuth();
@@ -14,12 +15,14 @@ export async function GET(req: Request) {
14
15
  try {
15
16
  const res = await hubFetch(`/api/status${qs}`);
16
17
  const data = await res.json();
18
+ if (Array.isArray(data.sessions)) data.sessions = normalizeSessions(data.sessions);
17
19
 
18
20
  // If filtered result is empty, fetch global count for empty-state hint
19
21
  if (includeFilter && Array.isArray(data.sessions) && data.sessions.length === 0) {
20
22
  try {
21
23
  const globalRes = await hubFetch('/api/status');
22
24
  const globalData = await globalRes.json();
25
+ if (Array.isArray(globalData.sessions)) globalData.sessions = normalizeSessions(globalData.sessions);
23
26
  const globalCount = Array.isArray(globalData.sessions) ? globalData.sessions.length : 0;
24
27
  if (globalCount > 0) {
25
28
  return Response.json({ ...data, _hint: { global_count: globalCount, filtered_network: networkId } });
@@ -28,7 +28,7 @@ export function AgentCard({ session: s, hasSse, sseCount, onChat }: AgentCardPro
28
28
  <Link
29
29
  href={`/node?alias=${encodeURIComponent(s.alias)}`}
30
30
  prefetch={false}
31
- className={`anet-agent-card group relative block rounded-xl border p-4 transition-all duration-300 cursor-pointer hover:-translate-y-0.5 ${
31
+ className={`anet-agent-card group relative block rounded-xl border p-3 sm:p-4 transition-all duration-300 cursor-pointer hover:-translate-y-0.5 ${
32
32
  hasSse
33
33
  ? `bg-[#111128] border-[#2a2a4a] hover:border-cyan-500/30 hover:shadow-lg ${cfg.glow}`
34
34
  : 'bg-[#0d0d1a] border-[#1a1a2a] opacity-40'
@@ -36,8 +36,10 @@ export function AgentCard({ session: s, hasSse, sseCount, onChat }: AgentCardPro
36
36
  >
37
37
  {/* Header: avatar + name + status. Avatar carries the alias→hue map
38
38
  shared with Messages/Nodes/Tasks/Overview; the live status dot
39
- stays as a small pulse-capable indicator. */}
40
- <div className="flex items-center justify-between mb-3">
39
+ stays as a small pulse-capable indicator. R5 of #190 mobile
40
+ polish: trim mb-3 → mb-2 sm:mb-3 so the card's vertical rhythm
41
+ tightens on narrow viewports. */}
42
+ <div className="flex items-center justify-between mb-2 sm:mb-3">
41
43
  <div className="flex items-center gap-2 min-w-0">
42
44
  <AliasAvatar alias={s.alias} size={22} />
43
45
  <span className="font-semibold text-white truncate text-sm" title={s.alias}>{s.alias}</span>
@@ -48,8 +50,11 @@ export function AgentCard({ session: s, hasSse, sseCount, onChat }: AgentCardPro
48
50
  </span>
49
51
  </div>
50
52
 
51
- {/* Agent type badge */}
52
- <div className="flex items-center gap-2 mb-3">
53
+ {/* Agent type badge — hidden below sm because the runtime
54
+ (`claude-code` / `codex`) repeats across nearly every card and
55
+ chews ~28px per card × 99 sessions on Overview mobile. The
56
+ agent type stays one tap away on /node detail. */}
57
+ <div className="hidden sm:flex items-center gap-2 mb-3">
53
58
  <span className="text-xs text-gray-600 bg-[#0a0a15] px-2 py-0.5 rounded border border-[#1a1a2a]">
54
59
  {s.agent || 'unknown'}
55
60
  </span>
@@ -58,18 +63,23 @@ export function AgentCard({ session: s, hasSse, sseCount, onChat }: AgentCardPro
58
63
  )}
59
64
  </div>
60
65
 
61
- {/* Task */}
66
+ {/* Task. Mobile: line-clamp-1 (one-liner) instead of two, and a
67
+ single-line padding (px-2 py-1) so the task strip is ~28px
68
+ rather than ~56px. The full task is still in the title
69
+ tooltip and on /node detail. */}
62
70
  {s.task ? (
63
- <div className="text-xs text-gray-400 bg-[#0a0a15] rounded-lg px-3 py-2 border border-[#1a1a2a] line-clamp-2" title={s.task}>
71
+ <div className="text-xs text-gray-400 bg-[#0a0a15] rounded-lg px-2 sm:px-3 py-1 sm:py-2 border border-[#1a1a2a] line-clamp-1 sm:line-clamp-2" title={s.task}>
64
72
  {s.task}
65
73
  </div>
66
74
  ) : (
67
75
  <div className="text-xs text-gray-700 italic">No active task</div>
68
76
  )}
69
77
 
70
- {/* Progress bar */}
78
+ {/* Progress bar — hidden below sm so an empty 0% bar doesn't
79
+ occupy 20px on every idle card. Visible from sm up where space
80
+ is no longer the constraint. */}
71
81
  {s.progress > 0 && (
72
- <div className="mt-3">
82
+ <div className="hidden sm:block mt-3">
73
83
  <div className="flex justify-between text-[10px] mb-1">
74
84
  <span className="text-gray-600">Progress</span>
75
85
  <span className={cfg.text}>{s.progress}%</span>
@@ -22,6 +22,10 @@ export function aliasInitial(alias?: string): string {
22
22
  return ch.toUpperCase();
23
23
  }
24
24
 
25
+ function isGrokAlias(alias: string) {
26
+ return /\bgrok\b|grok-build|grok测试员|grok-demo/i.test(alias);
27
+ }
28
+
25
29
  interface AliasAvatarProps {
26
30
  alias: string;
27
31
  size?: number;
@@ -29,6 +33,24 @@ interface AliasAvatarProps {
29
33
  }
30
34
 
31
35
  export function AliasAvatar({ alias, size = 28, className = '' }: AliasAvatarProps) {
36
+ if (isGrokAlias(alias)) {
37
+ return (
38
+ <span
39
+ className={`anet-alias-avatar inline-flex items-center justify-center rounded-full border border-emerald-500/45 bg-emerald-950/70 shrink-0 ${className}`}
40
+ style={{
41
+ width: size,
42
+ height: size,
43
+ backgroundImage: 'url(/vendors/grok.svg)',
44
+ backgroundPosition: 'center',
45
+ backgroundRepeat: 'no-repeat',
46
+ backgroundSize: '68% 68%',
47
+ }}
48
+ title={alias}
49
+ aria-hidden
50
+ />
51
+ );
52
+ }
53
+
32
54
  const c = aliasAvatarColors(alias);
33
55
  const fs = Math.max(9, Math.round(size * 0.42));
34
56
  return (
@@ -438,7 +438,7 @@ export function CommandPalette() {
438
438
  value={query}
439
439
  onChange={e => { setQuery(e.target.value); setSelected(0); }}
440
440
  placeholder="Type a command or search…"
441
- className="flex-1 bg-transparent text-sm text-gray-200 placeholder-gray-600 focus:outline-none"
441
+ className="flex-1 bg-transparent text-base sm:text-sm text-gray-200 placeholder-gray-600 focus:outline-none"
442
442
  />
443
443
  <kbd className="text-[10px] text-gray-600 border border-[#2a2a4a] rounded px-1.5 py-0.5 font-mono">esc</kbd>
444
444
  </div>
@@ -94,7 +94,7 @@ export function DispatchPanel({ sessions, onClose }: DispatchPanelProps) {
94
94
  <input
95
95
  type="text" value={filter} onChange={e => setFilter(e.target.value)}
96
96
  placeholder="Filter agents..."
97
- className="w-full bg-[#111128] border border-[#2a2a4a] rounded-lg px-3 py-2 text-xs text-white placeholder-gray-600 focus:border-cyan-500/40 focus:outline-none"
97
+ className="w-full bg-[#111128] border border-[#2a2a4a] rounded-lg px-3 py-2 text-base sm:text-xs text-white placeholder-gray-600 focus:border-cyan-500/40 focus:outline-none"
98
98
  />
99
99
  <div className="flex items-center justify-between mt-2">
100
100
  <button onClick={selectAll} className="text-[10px] text-cyan-400 hover:text-cyan-300">
@@ -126,7 +126,7 @@ export function DispatchPanel({ sessions, onClose }: DispatchPanelProps) {
126
126
  <textarea
127
127
  value={prompt} onChange={e => setPrompt(e.target.value)}
128
128
  placeholder="Enter the task you want to dispatch..."
129
- className="flex-1 min-h-[120px] bg-[#111128] border border-[#2a2a4a] rounded-xl px-4 py-3 text-sm text-white placeholder-gray-600 focus:border-cyan-500/40 focus:outline-none resize-none"
129
+ className="flex-1 min-h-[120px] bg-[#111128] border border-[#2a2a4a] rounded-xl px-4 py-3 text-base sm:text-sm text-white placeholder-gray-600 focus:border-cyan-500/40 focus:outline-none resize-none"
130
130
  />
131
131
 
132
132
  <div className="flex items-center gap-3 mt-4">
@@ -116,11 +116,19 @@ export function HealthBanner() {
116
116
  {cta.label} →
117
117
  </Link>
118
118
  )}
119
+ {/* R9 of #190 mobile polish: the inline `→` CTA and the `×`
120
+ dismiss were ~14px tap targets — below iOS 44px and worst
121
+ for the right-edge dismiss where a thumb-miss either does
122
+ nothing or fires the CTA next to it. The banner is
123
+ intentionally 28px tall (design comment above), so make the
124
+ tap area larger without making the banner taller: an
125
+ invisible `::before` pseudo-element extends the hit zone to
126
+ ~44×40px around each control. Visual size stays as is. */}
119
127
  {cta && (
120
128
  <Link
121
129
  href={cta.href}
122
130
  aria-label={cta.label}
123
- className="sm:hidden text-[11px] font-medium opacity-90 hover:opacity-100"
131
+ className="sm:hidden text-[13px] font-medium opacity-90 hover:opacity-100 relative leading-none px-1.5 before:absolute before:inset-y-[-10px] before:inset-x-[-8px] before:content-['']"
124
132
  >
125
133
 
126
134
  </Link>
@@ -131,7 +139,7 @@ export function HealthBanner() {
131
139
  try { sessionStorage.setItem('anet-hb-dismissed', '1'); } catch {}
132
140
  }}
133
141
  aria-label="Dismiss banner"
134
- className="opacity-50 hover:opacity-100 leading-none px-1"
142
+ className="opacity-60 hover:opacity-100 leading-none px-1.5 text-base relative rounded-md hover:bg-white/5 before:absolute before:inset-y-[-10px] before:inset-x-[-8px] before:content-['']"
135
143
  >
136
144
  ×
137
145
  </button>
@@ -136,6 +136,8 @@ function StatusBar({ status }: { status: string }) {
136
136
  import ReactMarkdown from 'react-markdown';
137
137
  import remarkGfm from 'remark-gfm';
138
138
  import { useSSE } from '../lib/useSSE';
139
+ import { useNetworkId } from '../lib/network-context';
140
+ import { markChatRead } from '../lib/chat-unread';
139
141
 
140
142
  function MarkdownContent({ text }: { text: string }) {
141
143
  if (!text) return <span className="text-[var(--fg-dim)] italic">No content</span>;
@@ -197,6 +199,7 @@ interface TaskChatPanelProps {
197
199
  }
198
200
 
199
201
  export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskChatPanelProps) {
202
+ const { networkId } = useNetworkId();
200
203
  const [messages, setMessages] = useState<ChatTask[]>([]);
201
204
  const [input, setInput] = useState('');
202
205
  const [attachedFiles, setAttachedFiles] = useState<File[]>([]);
@@ -223,6 +226,11 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
223
226
  // Reset target when alias changes
224
227
  useEffect(() => { setTargetAlias(alias); }, [alias]);
225
228
 
229
+ useEffect(() => {
230
+ if (!alias) return;
231
+ markChatRead(alias, networkId);
232
+ }, [alias, networkId]);
233
+
226
234
  // Load task history for this node
227
235
  const loadHistory = useCallback(async () => {
228
236
  try {
@@ -9,6 +9,7 @@ import { ChatPopover } from './ChatPopover';
9
9
  import { vendorForModel, runtimeIdentity, identityLine } from '../lib/vendorIdentity';
10
10
  import { parseHubTime, relativeAgo } from '../lib/time';
11
11
  import { DASHBOARD_VERSION } from '../lib/version';
12
+ import { useChatUnread } from '../lib/chat-unread';
12
13
 
13
14
  /** v0.10.0 Hero 1+2 / §3.F server-health hook — fetches the normalized
14
15
  * /api/hub/servers payload (preview.370 unblocked real-data via the
@@ -632,6 +633,7 @@ function buildFlowLinks(messages: MessageFlow[], tasks: TaskFlow[], positions: R
632
633
  }
633
634
 
634
635
  export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProps) {
636
+ const { hasUnread } = useChatUnread();
635
637
  const theme = useTheme();
636
638
  const isLight = theme === 'light';
637
639
  const reducedMotion = useReducedMotion();
@@ -12372,6 +12374,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
12372
12374
  </g>
12373
12375
  );
12374
12376
  })()}
12377
+ {hasUnread(session.alias) && (
12378
+ <g pointerEvents="none" data-node-unread={session.alias}>
12379
+ <circle
12380
+ cx={pos.x + radius * 0.74}
12381
+ cy={pos.y - radius * 0.74}
12382
+ r={Math.max(5, radius * 0.22)}
12383
+ fill="#ef4444"
12384
+ stroke={pal.containerBg}
12385
+ strokeWidth="2"
12386
+ />
12387
+ <circle
12388
+ cx={pos.x + radius * 0.74}
12389
+ cy={pos.y - radius * 0.74}
12390
+ r={Math.max(8, radius * 0.34)}
12391
+ fill="none"
12392
+ stroke="#ef4444"
12393
+ strokeOpacity="0.22"
12394
+ strokeWidth="1.5"
12395
+ />
12396
+ </g>
12397
+ )}
12375
12398
  {/* Round 294 / Loop: per-node "working" pulse dot retired.
12376
12399
  The pulse was R24's per-node working indicator — a
12377
12400
  small green circle at the top of each working node,
package/app/globals.css CHANGED
@@ -1991,3 +1991,27 @@ body {
1991
1991
  but the smooth transition just snaps. Already handled via the
1992
1992
  blanket transition-duration override above. */
1993
1993
  }
1994
+
1995
+ /* ────────────────────────────────────────────────────
1996
+ #190 mobile polish — tap feel
1997
+ ────────────────────────────────────────────────────
1998
+ `touch-action: manipulation` removes the 300 ms tap-delay browsers
1999
+ add to disambiguate double-tap-zoom, and (more importantly) keeps an
2000
+ accidental double-tap on an interactive element from zooming the
2001
+ page. Pinch-zoom on the rest of the page is unaffected. The hint is
2002
+ a no-op on inputs without a touch driver, so zero desktop cost.
2003
+
2004
+ `-webkit-tap-highlight-color` swaps iOS Safari's default opaque grey
2005
+ tap overlay for a faint themed shimmer (~18 % accent over transparent),
2006
+ matching the cyber / light palettes via the `--accent` token. */
2007
+ @layer base {
2008
+ button,
2009
+ a,
2010
+ [role="button"],
2011
+ [role="tab"],
2012
+ [role="menuitem"],
2013
+ summary {
2014
+ touch-action: manipulation;
2015
+ -webkit-tap-highlight-color: color-mix(in oklab, var(--accent) 18%, transparent);
2016
+ }
2017
+ }
@@ -0,0 +1,158 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import useSWR from 'swr';
5
+ import { useNetworkId } from './network-context';
6
+
7
+ const STORAGE_PREFIX = 'anet_chat_read_v1:';
8
+ const UNREAD_EVENT = 'anet-chat-read-updated';
9
+
10
+ interface ChatMessage {
11
+ from_alias?: string | null;
12
+ created_at?: string | null;
13
+ }
14
+
15
+ interface ChatTask {
16
+ to_name?: string | null;
17
+ status?: string | null;
18
+ result?: string | null;
19
+ created_at?: string | null;
20
+ completed_at?: string | null;
21
+ delivered_at?: string | null;
22
+ }
23
+
24
+ interface ReadMap {
25
+ [alias: string]: string;
26
+ }
27
+
28
+ function storageKey(networkId: string) {
29
+ return `${STORAGE_PREFIX}${networkId || 'global'}`;
30
+ }
31
+
32
+ function parseHubTime(value?: string | null): number {
33
+ if (!value) return 0;
34
+ const normalized = value.includes('T') ? value : value.replace(' ', 'T');
35
+ const withZone = /Z$|[+-]\d{2}:\d{2}$/.test(normalized) ? normalized : `${normalized}Z`;
36
+ const ts = Date.parse(withZone);
37
+ return Number.isFinite(ts) ? ts : 0;
38
+ }
39
+
40
+ function readStoredMap(networkId: string): ReadMap {
41
+ if (typeof window === 'undefined') return {};
42
+ try {
43
+ const raw = window.sessionStorage.getItem(storageKey(networkId));
44
+ if (!raw) return {};
45
+ const parsed = JSON.parse(raw);
46
+ return parsed && typeof parsed === 'object' ? parsed : {};
47
+ } catch {
48
+ return {};
49
+ }
50
+ }
51
+
52
+ function writeStoredMap(networkId: string, next: ReadMap) {
53
+ if (typeof window === 'undefined') return;
54
+ try {
55
+ window.sessionStorage.setItem(storageKey(networkId), JSON.stringify(next));
56
+ window.dispatchEvent(new CustomEvent(UNREAD_EVENT, { detail: { networkId } }));
57
+ } catch {}
58
+ }
59
+
60
+ const fetcher = async (url: string) => {
61
+ const res = await fetch(url);
62
+ if (res.status === 401) {
63
+ window.location.assign('/login');
64
+ throw new Error('unauthorized');
65
+ }
66
+ return res.json();
67
+ };
68
+
69
+ function withNetwork(url: string, networkId: string): string {
70
+ if (!networkId) return url;
71
+ const sep = url.includes('?') ? '&' : '?';
72
+ return `${url}${sep}network_id=${encodeURIComponent(networkId)}`;
73
+ }
74
+
75
+ export function markChatRead(alias: string, networkId: string) {
76
+ if (!alias) return;
77
+ const current = readStoredMap(networkId);
78
+ writeStoredMap(networkId, {
79
+ ...current,
80
+ [alias]: new Date().toISOString(),
81
+ });
82
+ }
83
+
84
+ export function useChatUnread() {
85
+ const { networkId } = useNetworkId();
86
+ const { data: messagesData } = useSWR<{ messages?: ChatMessage[] }>(
87
+ withNetwork('/api/hub/messages?limit=200', networkId),
88
+ fetcher,
89
+ { refreshInterval: 5000, dedupingInterval: 3000 },
90
+ );
91
+ const { data: tasksData } = useSWR<{ tasks?: ChatTask[] }>(
92
+ withNetwork('/api/hub/tasks?limit=200', networkId),
93
+ fetcher,
94
+ { refreshInterval: 5000, dedupingInterval: 3000 },
95
+ );
96
+ const [readVersion, setReadVersion] = useState(0);
97
+
98
+ useEffect(() => {
99
+ const onStorage = (event: StorageEvent) => {
100
+ if (event.key && event.key !== storageKey(networkId)) return;
101
+ setReadVersion((v) => (typeof v === 'number' ? v + 1 : 1));
102
+ };
103
+ const onUnreadUpdate = (event: Event) => {
104
+ const detail = (event as CustomEvent<{ networkId?: string }>).detail;
105
+ if (detail?.networkId !== undefined && detail.networkId !== networkId) return;
106
+ setReadVersion((v) => (typeof v === 'number' ? v + 1 : 1));
107
+ };
108
+ window.addEventListener('storage', onStorage);
109
+ window.addEventListener(UNREAD_EVENT, onUnreadUpdate);
110
+ return () => {
111
+ window.removeEventListener('storage', onStorage);
112
+ window.removeEventListener(UNREAD_EVENT, onUnreadUpdate);
113
+ };
114
+ }, [networkId, setReadVersion]);
115
+
116
+ const readMap = useMemo(() => {
117
+ void readVersion;
118
+ return readStoredMap(networkId);
119
+ }, [networkId, readVersion]);
120
+
121
+ const unreadMap = useMemo(() => {
122
+ const latestByAlias = new Map<string, number>();
123
+ for (const message of messagesData?.messages || []) {
124
+ const alias = message.from_alias?.trim();
125
+ if (!alias) continue;
126
+ const at = parseHubTime(message.created_at);
127
+ if (!at) continue;
128
+ latestByAlias.set(alias, Math.max(latestByAlias.get(alias) || 0, at));
129
+ }
130
+ for (const task of tasksData?.tasks || []) {
131
+ const alias = task.to_name?.trim();
132
+ if (!alias) continue;
133
+ const hasReply = Boolean(task.result) || ['replied', 'failed', 'closed', 'expired', 'cancelled'].includes(task.status || '');
134
+ if (!hasReply) continue;
135
+ const at = parseHubTime(task.completed_at) || parseHubTime(task.delivered_at) || parseHubTime(task.created_at);
136
+ if (!at) continue;
137
+ latestByAlias.set(alias, Math.max(latestByAlias.get(alias) || 0, at));
138
+ }
139
+
140
+ const next = new Map<string, boolean>();
141
+ for (const [alias, latestAt] of latestByAlias) {
142
+ const readAt = parseHubTime(readMap[alias]);
143
+ if (latestAt > readAt) next.set(alias, true);
144
+ }
145
+ return next;
146
+ }, [messagesData, readMap, tasksData]);
147
+
148
+ const hasUnread = useCallback((alias?: string | null) => {
149
+ if (!alias) return false;
150
+ return unreadMap.get(alias) === true;
151
+ }, [unreadMap]);
152
+
153
+ const clearUnread = useCallback((alias: string) => {
154
+ markChatRead(alias, networkId);
155
+ }, [networkId]);
156
+
157
+ return { unreadMap, hasUnread, clearUnread };
158
+ }
@@ -0,0 +1,17 @@
1
+ import type { Session } from '../components/types';
2
+
3
+ type SessionLike = Partial<Session> & { agent?: string | null };
4
+
5
+ export function normalizeSessionIdentity<T extends SessionLike>(session: T): T {
6
+ const agent = session.agent?.toLowerCase() || '';
7
+ const alias = session.alias?.toLowerCase() || '';
8
+ const isGrok = agent.includes('agent-node:grok') || alias.includes('grok');
9
+ if (!session.runtime && isGrok) {
10
+ return { ...session, runtime: 'grok-build-acp' };
11
+ }
12
+ return session;
13
+ }
14
+
15
+ export function normalizeSessions<T extends SessionLike>(sessions: T[] | undefined): T[] {
16
+ return (sessions || []).map(normalizeSessionIdentity);
17
+ }
@@ -70,6 +70,15 @@ const OPENAI_VENDOR: VendorIdentity = {
70
70
  // badge. Vincent-authorized 2026-05-21 to use real vendor marks.
71
71
  logo: '/vendors/openai.svg',
72
72
  };
73
+ const GROK_VENDOR: VendorIdentity = {
74
+ id: 'grok',
75
+ label: 'Grok Build',
76
+ mono: { bg: 'hsl(150 14% 16%)', ring: 'hsl(150 48% 42%)', text: 'hsl(150 54% 82%)' },
77
+ initial: 'G',
78
+ // Local Grok Build mark for network graph avatars; keeps grok-build-acp
79
+ // nodes out of the generic unknown-vendor fallback.
80
+ logo: '/vendors/grok.svg',
81
+ };
73
82
 
74
83
  // Ordered prefix rules — first match wins. `test` runs against a lowercased
75
84
  // model id. Keep the most specific prefixes first.
@@ -77,6 +86,7 @@ const VENDOR_RULES: Array<{ test: (m: string) => boolean; vendor: VendorIdentity
77
86
  { test: (m) => m.startsWith('intern'), vendor: INTERN_VENDOR },
78
87
  { test: (m) => m.startsWith('minimax'), vendor: MINIMAX_VENDOR },
79
88
  { test: (m) => m.startsWith('claude'), vendor: ANTHROPIC_VENDOR },
89
+ { test: (m) => m.startsWith('grok') || m.startsWith('xai'), vendor: GROK_VENDOR },
80
90
  {
81
91
  test: (m) => m.startsWith('gpt') || m.startsWith('codex') || m.startsWith('o1') || m.startsWith('o3') || m.startsWith('o4'),
82
92
  vendor: OPENAI_VENDOR,
@@ -99,6 +109,9 @@ const RUNTIME_VENDOR: Record<string, VendorIdentity> = {
99
109
  'claude-code-cli': ANTHROPIC_VENDOR,
100
110
  'claude-agent-sdk': ANTHROPIC_VENDOR,
101
111
  'codex-sdk': OPENAI_VENDOR,
112
+ 'grok-build-acp': GROK_VENDOR,
113
+ 'grok-build': GROK_VENDOR,
114
+ grok: GROK_VENDOR,
102
115
  };
103
116
 
104
117
  /** Resolve a node's vendor identity. Model id wins; when the model is
@@ -114,11 +127,12 @@ export function vendorForModel(
114
127
  if (rule.test(m)) return rule.vendor;
115
128
  }
116
129
  }
117
- if (runtime && RUNTIME_VENDOR[runtime]) return RUNTIME_VENDOR[runtime];
130
+ const rt = runtime?.toLowerCase();
131
+ if (rt && RUNTIME_VENDOR[rt]) return RUNTIME_VENDOR[rt];
118
132
  return UNKNOWN_VENDOR;
119
133
  }
120
134
 
121
- export type Runtime = 'claude-code-cli' | 'codex-sdk' | 'claude-agent-sdk' | 'http-api';
135
+ export type Runtime = 'claude-code-cli' | 'codex-sdk' | 'claude-agent-sdk' | 'grok-build-acp' | 'http-api';
122
136
 
123
137
  export interface RuntimeIdentity {
124
138
  label: string;
@@ -147,6 +161,12 @@ const RUNTIME_MAP: Record<Runtime, RuntimeIdentity> = {
147
161
  iconPath: 'M4 7h16v10H4z M8 11h8 M8 14h5',
148
162
  color: '#34d399',
149
163
  },
164
+ // Grok Build ACP bridge
165
+ 'grok-build-acp': {
166
+ label: 'Grok Build ACP',
167
+ iconPath: 'M12 3l2.1 6.2H21l-5.5 4 2.1 6.8L12 15.9 6.4 20l2.1-6.8L3 9.2h6.9L12 3z',
168
+ color: '#22c55e',
169
+ },
150
170
  // cloud / API — non-resident process
151
171
  'http-api': {
152
172
  label: 'HTTP API',
@@ -158,7 +178,7 @@ const RUNTIME_MAP: Record<Runtime, RuntimeIdentity> = {
158
178
  /** Resolve a server runtime string to badge identity. Unknown → null. */
159
179
  export function runtimeIdentity(runtime: string | null | undefined): RuntimeIdentity | null {
160
180
  if (!runtime) return null;
161
- return RUNTIME_MAP[runtime as Runtime] ?? null;
181
+ return RUNTIME_MAP[runtime.toLowerCase() as Runtime] ?? null;
162
182
  }
163
183
 
164
184
  /** Compact one-line identity for hover / detail surfaces:
@@ -148,7 +148,7 @@ export default function MessagesPage() {
148
148
  value={search}
149
149
  onChange={e => setSearch(e.target.value)}
150
150
  placeholder="Search from/to/content or use from:alias"
151
- className="bg-[#111128] border border-[#2a2a4a] rounded-lg px-3 py-2 text-sm text-white placeholder-gray-600 focus:border-blue-500/50 focus:outline-none w-full sm:w-72"
151
+ className="bg-[#111128] border border-[#2a2a4a] rounded-lg px-3 py-2 text-base sm:text-sm text-white placeholder-gray-600 focus:border-blue-500/50 focus:outline-none w-full sm:w-72"
152
152
  />
153
153
  <select
154
154
  value={filterType}
@@ -260,19 +260,26 @@ export default function MessagesPage() {
260
260
 
261
261
  return (
262
262
  <div key={message.id}>
263
+ {/* R6 of #190 mobile polish: gap divider was my-4 (=16px
264
+ each side), and with multi-hour message lulls it
265
+ fired several times per session, eating ~32px each.
266
+ Halve it on mobile. */}
263
267
  {gapExceeded && (
264
- <div className="my-4 flex items-center gap-3">
268
+ <div className="my-2 sm:my-4 flex items-center gap-3">
265
269
  <div className="h-px flex-1 bg-[#2a2a4a]" />
266
270
  <div className="text-[11px] text-gray-600">{formatDividerLabel(message.created_at)}</div>
267
271
  <div className="h-px flex-1 bg-[#2a2a4a]" />
268
272
  </div>
269
273
  )}
270
274
 
271
- {/* Broadcasts span full width — no avatar gutter. */}
275
+ {/* Broadcasts span full width — no avatar gutter. R6 mobile:
276
+ tighter padding + tighter header margin + snug leading
277
+ on the content so each bubble is ~25-30% shorter at
278
+ 390px. Desktop unchanged from sm: up. */}
272
279
  {variant === 'broadcast' ? (
273
- <div className={samePrev ? 'mt-1' : 'mt-3'}>
274
- <div className="rounded-2xl border border-purple-500/20 bg-purple-500/10 px-4 py-3 shadow-sm w-full">
275
- <div className="mb-2 flex flex-wrap items-center gap-2">
280
+ <div className={samePrev ? 'mt-0.5 sm:mt-1' : 'mt-2 sm:mt-3'}>
281
+ <div className="rounded-2xl border border-purple-500/20 bg-purple-500/10 px-3 py-2 sm:px-4 sm:py-3 shadow-sm w-full">
282
+ <div className="mb-1 sm:mb-2 flex flex-wrap items-center gap-2">
276
283
  <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'}`}>
277
284
  {message.type || 'unknown'}
278
285
  </span>
@@ -285,24 +292,24 @@ export default function MessagesPage() {
285
292
  long unbroken runs (URLs, ASCII rules like
286
293
  `═══════════════`) wrap instead of pushing the
287
294
  chat bubble past the mobile viewport. */}
288
- <div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm leading-relaxed text-gray-200">
295
+ <div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm leading-snug sm:leading-relaxed text-gray-200">
289
296
  {renderHighlighted(message.content, search)}
290
297
  </div>
291
298
  </div>
292
299
  </div>
293
300
  ) : (
294
- <div className={`${samePrev ? 'mt-1' : 'mt-3'} flex gap-2 ${variant === 'outgoing' ? 'flex-row-reverse' : 'flex-row'}`}>
301
+ <div className={`${samePrev ? 'mt-0.5 sm:mt-1' : 'mt-2 sm:mt-3'} flex gap-2 ${variant === 'outgoing' ? 'flex-row-reverse' : 'flex-row'}`}>
295
302
  {/* Avatar gutter — fixed width keeps bubble columns aligned even on streaks */}
296
303
  <div className="w-8 shrink-0 pt-1">
297
304
  {!samePrev && <AliasAvatar alias={fromAlias} size={32} />}
298
305
  </div>
299
- <div className={`min-w-0 max-w-3xl rounded-2xl border px-4 py-3 shadow-sm ${
306
+ <div className={`min-w-0 max-w-3xl rounded-2xl border px-3 py-2 sm:px-4 sm:py-3 shadow-sm ${
300
307
  variant === 'outgoing'
301
308
  ? 'border-green-500/20 bg-green-500/10'
302
309
  : 'border-blue-500/20 bg-blue-500/10'
303
310
  }`}>
304
311
  {!samePrev && (
305
- <div className="mb-2 flex flex-wrap items-center gap-2">
312
+ <div className="mb-1 sm:mb-2 flex flex-wrap items-center gap-2">
306
313
  <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'}`}>
307
314
  {message.type || 'unknown'}
308
315
  </span>
@@ -320,7 +327,7 @@ export default function MessagesPage() {
320
327
  long unbroken runs (URLs, ASCII rules like
321
328
  `═══════════════`) wrap instead of pushing the
322
329
  chat bubble past the mobile viewport. */}
323
- <div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm leading-relaxed text-gray-200">
330
+ <div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm leading-snug sm:leading-relaxed text-gray-200">
324
331
  {renderHighlighted(message.content, search)}
325
332
  </div>
326
333
 
package/app/node/page.tsx CHANGED
@@ -8,6 +8,7 @@ import { TaskChatPanel } from '../components/TaskChatPanel';
8
8
  import { timeAgo } from '../components/utils';
9
9
  import { AliasAvatar } from '../components/AliasAvatar';
10
10
  import { SESSION_STATUS_TEXT_CLASS } from '../lib/status';
11
+ import { useChatUnread } from '../lib/chat-unread';
11
12
 
12
13
  interface SessionDetail {
13
14
  resume_id: string;
@@ -187,8 +188,10 @@ function NodeFullPanel({ alias, session, sse, sendMsg, setSendMsg, sending, send
187
188
  alias: string; session: any; sse: number; sendMsg: string; setSendMsg: (v: string) => void; sending: boolean; sendTask: () => void; sendError: string;
188
189
  }) {
189
190
  const [tab, setTab] = useState<'chat' | 'events' | 'info'>('chat');
191
+ const { hasUnread } = useChatUnread();
190
192
  const [events, setEvents] = useState<Array<{ id: number; event_type: string; from_status: string; to_status: string; detail: string; created_at: string }>>([]);
191
193
  const [eventsLoaded, setEventsLoaded] = useState(false);
194
+ const chatUnread = hasUnread(alias);
192
195
 
193
196
  useEffect(() => {
194
197
  if (tab === 'events' && !eventsLoaded) {
@@ -216,7 +219,12 @@ function NodeFullPanel({ alias, session, sse, sendMsg, setSendMsg, sending, send
216
219
  <svg className="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
217
220
  <path d={t.icon} />
218
221
  </svg>
219
- <span>{t.label}</span>
222
+ <span className="relative inline-flex items-center gap-2">
223
+ {t.label}
224
+ {t.id === 'chat' && chatUnread && tab !== 'chat' && (
225
+ <span className="inline-block h-2.5 w-2.5 rounded-full bg-red-500" aria-label="Unread chat messages" />
226
+ )}
227
+ </span>
220
228
  </button>
221
229
  ))}
222
230
  </div>