@sleep2agi/agent-network-dashboard 0.5.4 → 0.5.5-preview.1

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 (238) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/diagnostics/route-bundle-stats.json +66 -65
  5. package/.next/fallback-build-manifest.json +3 -3
  6. package/.next/prerender-manifest.json +24 -0
  7. package/.next/routes-manifest.json +6 -0
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  16. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/.next/server/app/_not-found.html +3 -3
  18. package/.next/server/app/_not-found.rsc +13 -13
  19. package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  20. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  21. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  22. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  23. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  24. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  25. package/.next/server/app/admin/page.js.nft.json +1 -1
  26. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  27. package/.next/server/app/admin.html +3 -3
  28. package/.next/server/app/admin.rsc +15 -15
  29. package/.next/server/app/admin.segments/_full.segment.rsc +15 -15
  30. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  31. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  32. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  33. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  34. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  35. package/.next/server/app/index.html +3 -3
  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 +7 -7
  41. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  42. package/.next/server/app/login/page.js.nft.json +1 -1
  43. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  44. package/.next/server/app/login.html +2 -2
  45. package/.next/server/app/login.rsc +15 -15
  46. package/.next/server/app/login.segments/_full.segment.rsc +15 -15
  47. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  48. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  49. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  50. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  51. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  52. package/.next/server/app/logs/page.js.nft.json +1 -1
  53. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  54. package/.next/server/app/logs.html +3 -3
  55. package/.next/server/app/logs.rsc +15 -15
  56. package/.next/server/app/logs.segments/_full.segment.rsc +15 -15
  57. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  58. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  59. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  60. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  61. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  62. package/.next/server/app/messages/page.js.nft.json +1 -1
  63. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  64. package/.next/server/app/messages.html +3 -3
  65. package/.next/server/app/messages.rsc +15 -15
  66. package/.next/server/app/messages.segments/_full.segment.rsc +15 -15
  67. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  68. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  69. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  70. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  71. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  72. package/.next/server/app/node/page.js.nft.json +1 -1
  73. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  74. package/.next/server/app/node.html +3 -3
  75. package/.next/server/app/node.rsc +15 -15
  76. package/.next/server/app/node.segments/_full.segment.rsc +15 -15
  77. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  78. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  79. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  80. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  81. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  82. package/.next/server/app/nodes/page.js.nft.json +1 -1
  83. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/nodes.html +3 -3
  85. package/.next/server/app/nodes.rsc +15 -15
  86. package/.next/server/app/nodes.segments/_full.segment.rsc +15 -15
  87. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  88. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  89. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  90. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  91. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  92. package/.next/server/app/page.js.nft.json +1 -1
  93. package/.next/server/app/page_client-reference-manifest.js +1 -1
  94. package/.next/server/app/server-logs/page.js.nft.json +1 -1
  95. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  96. package/.next/server/app/server-logs.html +3 -3
  97. package/.next/server/app/server-logs.rsc +15 -15
  98. package/.next/server/app/server-logs.segments/_full.segment.rsc +15 -15
  99. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  100. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  101. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  102. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  103. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  104. package/.next/server/app/servers/page/app-paths-manifest.json +3 -0
  105. package/.next/server/app/servers/page/build-manifest.json +17 -0
  106. package/.next/server/app/servers/page/next-font-manifest.json +10 -0
  107. package/.next/server/app/servers/page/react-loadable-manifest.json +1 -0
  108. package/.next/server/app/servers/page/server-reference-manifest.json +4 -0
  109. package/.next/server/app/servers/page.js +14 -0
  110. package/.next/server/app/servers/page.js.map +5 -0
  111. package/.next/server/app/servers/page.js.nft.json +1 -0
  112. package/.next/server/app/servers/page_client-reference-manifest.js +3 -0
  113. package/.next/server/app/servers.html +12 -0
  114. package/.next/server/app/servers.meta +15 -0
  115. package/.next/server/app/servers.rsc +24 -0
  116. package/.next/server/app/servers.segments/_full.segment.rsc +24 -0
  117. package/.next/server/app/servers.segments/_head.segment.rsc +6 -0
  118. package/.next/server/app/servers.segments/_index.segment.rsc +8 -0
  119. package/.next/server/app/servers.segments/_tree.segment.rsc +3 -0
  120. package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +9 -0
  121. package/.next/server/app/servers.segments/servers.segment.rsc +5 -0
  122. package/.next/server/app/settings/networks/page.js.nft.json +1 -1
  123. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app/settings/networks.html +3 -3
  125. package/.next/server/app/settings/networks.rsc +15 -15
  126. package/.next/server/app/settings/networks.segments/_full.segment.rsc +15 -15
  127. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  128. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  129. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  130. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  131. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  132. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  133. package/.next/server/app/settings/page.js.nft.json +1 -1
  134. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  135. package/.next/server/app/settings/tokens/page.js.nft.json +1 -1
  136. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  137. package/.next/server/app/settings/tokens.html +3 -3
  138. package/.next/server/app/settings/tokens.rsc +15 -15
  139. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +15 -15
  140. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  141. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  142. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  143. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  144. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  145. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  146. package/.next/server/app/settings.html +3 -3
  147. package/.next/server/app/settings.rsc +15 -15
  148. package/.next/server/app/settings.segments/_full.segment.rsc +15 -15
  149. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  150. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  151. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  152. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  153. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  154. package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
  155. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  156. package/.next/server/app/tasks/page.js.nft.json +1 -1
  157. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  158. package/.next/server/app/tasks.html +3 -3
  159. package/.next/server/app/tasks.rsc +15 -15
  160. package/.next/server/app/tasks.segments/_full.segment.rsc +15 -15
  161. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  162. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  163. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  164. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  165. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  166. package/.next/server/app-paths-manifest.json +1 -0
  167. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_10sgwq_.js +4 -0
  168. package/.next/server/chunks/ssr/00jm_next_dist_esm_build_templates_app-page_10sgwq_.js.map +1 -0
  169. package/.next/server/chunks/ssr/[root-of-the-server]__030vg4n._.js +2 -2
  170. package/.next/server/chunks/ssr/[root-of-the-server]__030vg4n._.js.map +1 -1
  171. package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js +1 -1
  172. package/.next/server/chunks/ssr/[root-of-the-server]__0nw~zhp._.js.map +1 -1
  173. package/.next/server/chunks/ssr/[root-of-the-server]__0qylf1f._.js +3 -0
  174. package/.next/server/chunks/ssr/[root-of-the-server]__0qylf1f._.js.map +1 -0
  175. package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js +1 -1
  176. package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js.map +1 -1
  177. package/.next/server/chunks/ssr/[root-of-the-server]__0u5aqkk._.js +1 -1
  178. package/.next/server/chunks/ssr/[root-of-the-server]__0u5aqkk._.js.map +1 -1
  179. package/.next/server/chunks/ssr/[root-of-the-server]__11fu-5m._.js +3 -0
  180. package/.next/server/chunks/ssr/[root-of-the-server]__11fu-5m._.js.map +1 -0
  181. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +1 -1
  182. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  183. package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_servers_page_actions_0mc5jn..js +3 -0
  184. package/.next/server/chunks/ssr/agent-network-dashboard__next-internal_server_app_servers_page_actions_0mc5jn..js.map +1 -0
  185. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  186. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  187. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  188. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  189. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +2 -2
  190. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -1
  191. package/.next/server/chunks/ssr/agent-network-dashboard_app_servers_page_tsx_0jib5qm._.js +3 -0
  192. package/.next/server/chunks/ssr/agent-network-dashboard_app_servers_page_tsx_0jib5qm._.js.map +1 -0
  193. package/.next/server/chunks/ssr/agent-network-dashboard_app_tasks_page_tsx_0mwxy4z._.js +1 -1
  194. package/.next/server/chunks/ssr/agent-network-dashboard_app_tasks_page_tsx_0mwxy4z._.js.map +1 -1
  195. package/.next/server/middleware-build-manifest.js +3 -3
  196. package/.next/server/next-font-manifest.js +1 -1
  197. package/.next/server/next-font-manifest.json +3 -0
  198. package/.next/server/pages/404.html +3 -3
  199. package/.next/server/pages/500.html +1 -1
  200. package/.next/static/chunks/{04wjx7vbxusw5.js → 0csnc6nlttr5s.js} +2 -2
  201. package/.next/static/chunks/{0k-c1chkvf78s.js → 0iwava1u7a3b4.js} +2 -2
  202. package/.next/static/chunks/0l4_5zb9iglew.css +2 -0
  203. package/.next/static/chunks/0lxkdd7i1ck0g.js +1 -0
  204. package/.next/static/chunks/{09e8kxo30n5cf.js → 0q_0ejb7xirob.js} +1 -1
  205. package/.next/static/chunks/0qwqp6ulyj3o6.js +1 -0
  206. package/.next/static/chunks/0wyjrc0bekhiz.js +1 -0
  207. package/.next/static/chunks/0~ykmap37nw9d.js +1 -0
  208. package/.next/static/chunks/13yktdzuatx3d.js +1 -0
  209. package/.next/static/chunks/176am8j.kv9.u.js +1 -0
  210. package/.next/static/chunks/{089t1exs6apb8.js → 17r9h6cx1w6q-.js} +1 -1
  211. package/.next/trace +2 -2
  212. package/.next/trace-build +1 -1
  213. package/.next/types/routes.d.ts +2 -1
  214. package/.next/types/validator.ts +9 -0
  215. package/app/components/AppShell.tsx +2 -2
  216. package/app/components/CommandCenter.tsx +1 -1
  217. package/app/components/DispatchPanel.tsx +1 -1
  218. package/app/components/MobileNav.tsx +16 -3
  219. package/app/components/ServersDrawer.tsx +170 -158
  220. package/app/components/Sidebar.tsx +1 -0
  221. package/app/components/TaskChatPanel.tsx +81 -26
  222. package/app/components/TaskDrawer.tsx +1 -1
  223. package/app/layout.tsx +2 -2
  224. package/app/nodes/page.tsx +21 -151
  225. package/app/servers/page.tsx +30 -0
  226. package/package.json +1 -1
  227. package/.next/server/chunks/ssr/[root-of-the-server]__0p-95r_._.js +0 -3
  228. package/.next/server/chunks/ssr/[root-of-the-server]__0p-95r_._.js.map +0 -1
  229. package/.next/static/chunks/01lmbsd37fybu.js +0 -1
  230. package/.next/static/chunks/03o.h6kvmw4l_.js +0 -1
  231. package/.next/static/chunks/0ku0fjqlm9mca.js +0 -1
  232. package/.next/static/chunks/0tvn2l1pc.h65.js +0 -1
  233. package/.next/static/chunks/0v2~nlpk-cx6v.css +0 -2
  234. package/.next/static/chunks/0xsye-9kffdi0.js +0 -1
  235. package/.next/static/chunks/0~rx_~akeylmq.js +0 -1
  236. /package/.next/static/{xq88BMF7fMUHWh10yaKTn → nB1Z7KvLgOywguHTiJ_My}/_buildManifest.js +0 -0
  237. /package/.next/static/{xq88BMF7fMUHWh10yaKTn → nB1Z7KvLgOywguHTiJ_My}/_clientMiddlewareManifest.js +0 -0
  238. /package/.next/static/{xq88BMF7fMUHWh10yaKTn → nB1Z7KvLgOywguHTiJ_My}/_ssgManifest.js +0 -0
@@ -195,27 +195,27 @@ function AgentList({ agents }: { agents?: ServerAgent[] }) {
195
195
  );
196
196
  }
197
197
 
198
- export function ServersDrawer() {
199
- const [open, setOpen] = useState(false);
200
- /** v0.10.0 Hero 1+2 — per-server expanded state for detail card
201
- * (sparklines + disk bar + agent rollup). Stored as a Set of
202
- * hostnames; persists in localStorage so the user's last selection
203
- * survives reload. Independent of the drawer's open/close state. */
198
+ function useServers(enabled = true) {
199
+ const { data, error } = useSWR<ServersResponse>(
200
+ enabled ? '/api/hub/servers' : null,
201
+ fetcher,
202
+ { refreshInterval: 5000, dedupingInterval: 3000 },
203
+ );
204
+ const servers = data?.servers ?? [];
205
+ const unavailable = data?.unavailable === true;
206
+ const onlineCount = servers.filter(s => s.status === 'online').length;
207
+ const loading = enabled && !data && !error;
208
+ return { data, error, servers, unavailable, onlineCount, loading };
209
+ }
210
+
211
+ export function ServersPanel({ enabled = true, className = '' }: { enabled?: boolean; className?: string }) {
204
212
  const [expanded, setExpanded] = useState<Set<string>>(new Set());
205
- // Drawer state is per-user-machine — persist like the other dashboard
206
- // sticky toggles (`anet-topo-layout`, `anet-topo-view`, etc.).
207
213
  useEffect(() => {
208
- try { if (localStorage.getItem('anet-servers-drawer') === '1') setOpen(true); } catch {}
209
214
  try {
210
215
  const raw = localStorage.getItem('anet-servers-drawer-expanded');
211
216
  if (raw) setExpanded(new Set(JSON.parse(raw)));
212
217
  } catch {}
213
218
  }, []);
214
- const toggle = () => setOpen(prev => {
215
- const next = !prev;
216
- try { localStorage.setItem('anet-servers-drawer', next ? '1' : '0'); } catch {}
217
- return next;
218
- });
219
219
  const toggleExpanded = (hostname: string) => setExpanded(prev => {
220
220
  const next = new Set(prev);
221
221
  if (next.has(hostname)) next.delete(hostname); else next.add(hostname);
@@ -223,19 +223,165 @@ export function ServersDrawer() {
223
223
  return next;
224
224
  });
225
225
 
226
+ const { data, error, servers, unavailable, loading } = useServers(enabled);
227
+
228
+ return (
229
+ <div className={`min-h-0 overflow-y-auto px-2 py-2 space-y-2 ${className}`} data-servers-body>
230
+ {loading && (
231
+ <div className="text-[10px] text-[var(--fg-muted)] font-mono text-center py-4">
232
+ loading servers…
233
+ </div>
234
+ )}
235
+ {error && !data && (
236
+ <div className="rounded-md border px-2.5 py-2 text-[10px] font-mono"
237
+ style={{ background: 'rgb(239 68 68 / 0.06)', borderColor: 'rgb(239 68 68 / 0.25)', color: '#ef4444' }}>
238
+ hub unreachable · retrying every 5s
239
+ </div>
240
+ )}
241
+ {unavailable && (
242
+ <div className="text-[10px] text-[var(--fg-muted)] font-mono text-center py-3 leading-relaxed">
243
+ host telemetry not available<br/>
244
+ <span className="text-[var(--fg-dim)]">upgrade commhub-server ≥ 0.8.1-preview.2</span>
245
+ </div>
246
+ )}
247
+ {!loading && !error && !unavailable && servers.length === 0 && (
248
+ <div className="text-[10px] text-[var(--fg-muted)] font-mono text-center py-4">
249
+ no servers reporting yet
250
+ </div>
251
+ )}
252
+ {servers.map(s => {
253
+ const offline = s.status === 'offline';
254
+ const cpuPct = s.cpu_load_1min != null && s.cpu_cores > 0 ? (s.cpu_load_1min / s.cpu_cores) * 100 : null;
255
+ const memPct = s.mem_used_gb != null && s.mem_total_gb != null && s.mem_total_gb > 0 ? (s.mem_used_gb / s.mem_total_gb) * 100 : null;
256
+ const diskPct = s.disk_used_gb != null && s.disk_total_gb != null && s.disk_total_gb > 0 ? (s.disk_used_gb / s.disk_total_gb) * 100 : null;
257
+ const isExpanded = expanded.has(s.hostname);
258
+ return (
259
+ <div
260
+ key={s.hostname}
261
+ className="rounded-lg border px-2.5 py-2 space-y-1.5"
262
+ style={{
263
+ background: 'var(--bg)',
264
+ borderColor: 'var(--border)',
265
+ opacity: offline ? 0.55 : 1,
266
+ }}
267
+ data-server-card={s.hostname}
268
+ data-server-expanded={isExpanded ? 'true' : 'false'}
269
+ title={s.ip ? `${s.hostname} · ${s.ip}` : s.hostname}
270
+ >
271
+ {/* v0.10.0 Hero 1+2: header row — click toggles expanded
272
+ detail view (sparklines + disk + agent rollup). */}
273
+ <button
274
+ type="button"
275
+ onClick={() => toggleExpanded(s.hostname)}
276
+ aria-expanded={isExpanded}
277
+ className="w-full flex items-center justify-between gap-2 text-left"
278
+ data-server-card-toggle={s.hostname}
279
+ >
280
+ <div className="min-w-0 flex items-center gap-1.5">
281
+ {/* Hero 1 health badge — worst-of CPU/Mem/Disk */}
282
+ {!offline && <HealthBadge cpu={cpuPct} mem={memPct} disk={diskPct} />}
283
+ <span className="font-mono text-[12px] font-semibold truncate" style={{ color: 'var(--fg)' }}>{s.hostname}</span>
284
+ {s.note && <span className="text-[9px] text-[var(--fg-dim)]">({s.note})</span>}
285
+ </div>
286
+ <span className="flex items-center gap-1 shrink-0">
287
+ <span className="text-[10px] text-[var(--fg-muted)] font-mono tabular-nums">
288
+ {s.agent_count}&nbsp;agent{s.agent_count === 1 ? '' : 's'}
289
+ </span>
290
+ {/* Chevron — rotates 90° on expanded */}
291
+ <svg
292
+ width="10" height="10" viewBox="0 0 10 10"
293
+ style={{ transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 150ms ease-out' }}
294
+ aria-hidden
295
+ >
296
+ <path d="M3 1.5L7 5L3 8.5" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
297
+ </svg>
298
+ </span>
299
+ </button>
300
+ {!offline && cpuPct != null && (
301
+ <Bar pct={cpuPct} label={`CPU ${s.cpu_load_1min!.toFixed(2)}/${s.cpu_cores}`} />
302
+ )}
303
+ {!offline && memPct != null && (
304
+ <Bar pct={memPct} label={`RAM ${s.mem_used_gb!.toFixed(1)}/${s.mem_total_gb!.toFixed(1)}G`} />
305
+ )}
306
+ {offline && (
307
+ <div className="text-[10px] text-[var(--fg-dim)] font-mono italic">CPU n/a · RAM n/a · offline</div>
308
+ )}
309
+ {/* v0.10.0 Hero 1+2: expanded detail — disk bar +
310
+ 5-min sparklines + agent rollup. Visible only when
311
+ the user clicks the card. Renders gracefully when
312
+ upstream hub hasn't shipped optional fields. */}
313
+ {isExpanded && !offline && (
314
+ <div className="pt-1 mt-1 border-t space-y-1.5" style={{ borderColor: 'var(--border)' }}>
315
+ {diskPct != null ? (
316
+ <Bar pct={diskPct} label={`DISK ${s.disk_used_gb!.toFixed(1)}/${s.disk_total_gb!.toFixed(1)}G`} />
317
+ ) : (
318
+ /* #157 sibling fix — same misleading version-pin copy
319
+ dropped at the disk-metric placeholder. Same
320
+ rationale as the agent-rollup copy above. */
321
+ <div className="text-[9px] text-[var(--fg-dim)] font-mono italic" data-server-disk-missing="true">disk metric not reported by hub</div>
322
+ )}
323
+ {s.cpu_history && s.cpu_history.length >= 2 && (
324
+ <div className="space-y-0.5">
325
+ <div className="text-[9px] text-[var(--fg-muted)] font-mono">CPU · 5-min</div>
326
+ <Sparkline values={s.cpu_history} tint="#10b981" label="CPU" />
327
+ </div>
328
+ )}
329
+ {s.mem_history && s.mem_history.length >= 2 && (
330
+ <div className="space-y-0.5">
331
+ <div className="text-[9px] text-[var(--fg-muted)] font-mono">RAM · 5-min</div>
332
+ <Sparkline values={s.mem_history} tint="#06b6d4" label="MEM" />
333
+ </div>
334
+ )}
335
+ {/* v0.10.2 RFC-014 §7 close gate #3 — disk usage
336
+ 5-min curve. Amber tint matches the disk bar
337
+ tier convention (DISK > CPU/Mem in alert-
338
+ priority hierarchy since disk-full is a hard
339
+ failure mode). Render only when agent-node
340
+ 2.4.1-preview.0+ has shipped disk_history;
341
+ backward-compat handles older agents silently
342
+ (no sparkline, no broken state). */}
343
+ {s.disk_history && s.disk_history.length >= 2 && (
344
+ <div className="space-y-0.5">
345
+ <div className="text-[9px] text-[var(--fg-muted)] font-mono">DISK · 5-min</div>
346
+ <Sparkline values={s.disk_history} tint="#f59e0b" label="DISK" />
347
+ </div>
348
+ )}
349
+ <div className="pt-1">
350
+ <div className="text-[9px] text-[var(--fg-muted)] font-mono mb-0.5">agents</div>
351
+ <AgentList agents={s.agents} />
352
+ </div>
353
+ </div>
354
+ )}
355
+ </div>
356
+ );
357
+ })}
358
+ {servers.length > 0 && (
359
+ <div className="pt-1 text-[9px] text-[var(--fg-dim)] font-mono text-center">
360
+ live · refreshing every 5s
361
+ </div>
362
+ )}
363
+ </div>
364
+ );
365
+ }
366
+
367
+ export function ServersDrawer() {
368
+ const [open, setOpen] = useState(false);
369
+ // Drawer state is per-user-machine — persist like the other dashboard
370
+ // sticky toggles (`anet-topo-layout`, `anet-topo-view`, etc.).
371
+ useEffect(() => {
372
+ try { if (localStorage.getItem('anet-servers-drawer') === '1') setOpen(true); } catch {}
373
+ }, []);
374
+ const toggle = () => setOpen(prev => {
375
+ const next = !prev;
376
+ try { localStorage.setItem('anet-servers-drawer', next ? '1' : '0'); } catch {}
377
+ return next;
378
+ });
379
+
226
380
  // Round 20 / #119 step 3 final delivery — real SWR fetch. 5s refresh
227
381
  // matches the other live drawers (HealthBanner, Sidebar) so an operator
228
382
  // sees host telemetry update in roughly the same beat as session state.
229
383
  // Only poll while expanded — collapsed icon strip doesn't need fresh data.
230
- const { data, error } = useSWR<ServersResponse>(
231
- open ? '/api/hub/servers' : null,
232
- fetcher,
233
- { refreshInterval: 5000, dedupingInterval: 3000 },
234
- );
235
- const servers = data?.servers ?? [];
236
- const unavailable = data?.unavailable === true;
237
- const onlineCount = servers.filter(s => s.status === 'online').length;
238
- const loading = open && !data && !error;
384
+ const { servers, onlineCount } = useServers(open);
239
385
 
240
386
  return (
241
387
  <aside
@@ -271,141 +417,7 @@ export function ServersDrawer() {
271
417
  </button>
272
418
 
273
419
  {open && (
274
- <div className="flex-1 min-h-0 overflow-y-auto px-2 py-2 space-y-2" data-servers-body>
275
- {loading && (
276
- <div className="text-[10px] text-[var(--fg-muted)] font-mono text-center py-4">
277
- loading servers…
278
- </div>
279
- )}
280
- {error && !data && (
281
- <div className="rounded-md border px-2.5 py-2 text-[10px] font-mono"
282
- style={{ background: 'rgb(239 68 68 / 0.06)', borderColor: 'rgb(239 68 68 / 0.25)', color: '#ef4444' }}>
283
- hub unreachable · retrying every 5s
284
- </div>
285
- )}
286
- {unavailable && (
287
- <div className="text-[10px] text-[var(--fg-muted)] font-mono text-center py-3 leading-relaxed">
288
- host telemetry not available<br/>
289
- <span className="text-[var(--fg-dim)]">upgrade commhub-server ≥ 0.8.1-preview.2</span>
290
- </div>
291
- )}
292
- {!loading && !error && !unavailable && servers.length === 0 && (
293
- <div className="text-[10px] text-[var(--fg-muted)] font-mono text-center py-4">
294
- no servers reporting yet
295
- </div>
296
- )}
297
- {servers.map(s => {
298
- const offline = s.status === 'offline';
299
- const cpuPct = s.cpu_load_1min != null && s.cpu_cores > 0 ? (s.cpu_load_1min / s.cpu_cores) * 100 : null;
300
- const memPct = s.mem_used_gb != null && s.mem_total_gb != null && s.mem_total_gb > 0 ? (s.mem_used_gb / s.mem_total_gb) * 100 : null;
301
- const diskPct = s.disk_used_gb != null && s.disk_total_gb != null && s.disk_total_gb > 0 ? (s.disk_used_gb / s.disk_total_gb) * 100 : null;
302
- const isExpanded = expanded.has(s.hostname);
303
- return (
304
- <div
305
- key={s.hostname}
306
- className="rounded-lg border px-2.5 py-2 space-y-1.5"
307
- style={{
308
- background: 'var(--bg)',
309
- borderColor: 'var(--border)',
310
- opacity: offline ? 0.55 : 1,
311
- }}
312
- data-server-card={s.hostname}
313
- data-server-expanded={isExpanded ? 'true' : 'false'}
314
- title={s.ip ? `${s.hostname} · ${s.ip}` : s.hostname}
315
- >
316
- {/* v0.10.0 Hero 1+2: header row — click toggles expanded
317
- detail view (sparklines + disk + agent rollup). */}
318
- <button
319
- type="button"
320
- onClick={() => toggleExpanded(s.hostname)}
321
- aria-expanded={isExpanded}
322
- className="w-full flex items-center justify-between gap-2 text-left"
323
- data-server-card-toggle={s.hostname}
324
- >
325
- <div className="min-w-0 flex items-center gap-1.5">
326
- {/* Hero 1 health badge — worst-of CPU/Mem/Disk */}
327
- {!offline && <HealthBadge cpu={cpuPct} mem={memPct} disk={diskPct} />}
328
- <span className="font-mono text-[12px] font-semibold truncate" style={{ color: 'var(--fg)' }}>{s.hostname}</span>
329
- {s.note && <span className="text-[9px] text-[var(--fg-dim)]">({s.note})</span>}
330
- </div>
331
- <span className="flex items-center gap-1 shrink-0">
332
- <span className="text-[10px] text-[var(--fg-muted)] font-mono tabular-nums">
333
- {s.agent_count}&nbsp;agent{s.agent_count === 1 ? '' : 's'}
334
- </span>
335
- {/* Chevron — rotates 90° on expanded */}
336
- <svg
337
- width="10" height="10" viewBox="0 0 10 10"
338
- style={{ transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 150ms ease-out' }}
339
- aria-hidden
340
- >
341
- <path d="M3 1.5L7 5L3 8.5" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
342
- </svg>
343
- </span>
344
- </button>
345
- {!offline && cpuPct != null && (
346
- <Bar pct={cpuPct} label={`CPU ${s.cpu_load_1min!.toFixed(2)}/${s.cpu_cores}`} />
347
- )}
348
- {!offline && memPct != null && (
349
- <Bar pct={memPct} label={`RAM ${s.mem_used_gb!.toFixed(1)}/${s.mem_total_gb!.toFixed(1)}G`} />
350
- )}
351
- {offline && (
352
- <div className="text-[10px] text-[var(--fg-dim)] font-mono italic">CPU n/a · RAM n/a · offline</div>
353
- )}
354
- {/* v0.10.0 Hero 1+2: expanded detail — disk bar +
355
- 5-min sparklines + agent rollup. Visible only when
356
- the user clicks the card. Renders gracefully when
357
- upstream hub hasn't shipped optional fields. */}
358
- {isExpanded && !offline && (
359
- <div className="pt-1 mt-1 border-t space-y-1.5" style={{ borderColor: 'var(--border)' }}>
360
- {diskPct != null ? (
361
- <Bar pct={diskPct} label={`DISK ${s.disk_used_gb!.toFixed(1)}/${s.disk_total_gb!.toFixed(1)}G`} />
362
- ) : (
363
- /* #157 sibling fix — same misleading version-pin copy
364
- dropped at the disk-metric placeholder. Same
365
- rationale as the agent-rollup copy above. */
366
- <div className="text-[9px] text-[var(--fg-dim)] font-mono italic" data-server-disk-missing="true">disk metric not reported by hub</div>
367
- )}
368
- {s.cpu_history && s.cpu_history.length >= 2 && (
369
- <div className="space-y-0.5">
370
- <div className="text-[9px] text-[var(--fg-muted)] font-mono">CPU · 5-min</div>
371
- <Sparkline values={s.cpu_history} tint="#10b981" label="CPU" />
372
- </div>
373
- )}
374
- {s.mem_history && s.mem_history.length >= 2 && (
375
- <div className="space-y-0.5">
376
- <div className="text-[9px] text-[var(--fg-muted)] font-mono">RAM · 5-min</div>
377
- <Sparkline values={s.mem_history} tint="#06b6d4" label="MEM" />
378
- </div>
379
- )}
380
- {/* v0.10.2 RFC-014 §7 close gate #3 — disk usage
381
- 5-min curve. Amber tint matches the disk bar
382
- tier convention (DISK > CPU/Mem in alert-
383
- priority hierarchy since disk-full is a hard
384
- failure mode). Render only when agent-node
385
- 2.4.1-preview.0+ has shipped disk_history;
386
- backward-compat handles older agents silently
387
- (no sparkline, no broken state). */}
388
- {s.disk_history && s.disk_history.length >= 2 && (
389
- <div className="space-y-0.5">
390
- <div className="text-[9px] text-[var(--fg-muted)] font-mono">DISK · 5-min</div>
391
- <Sparkline values={s.disk_history} tint="#f59e0b" label="DISK" />
392
- </div>
393
- )}
394
- <div className="pt-1">
395
- <div className="text-[9px] text-[var(--fg-muted)] font-mono mb-0.5">agents</div>
396
- <AgentList agents={s.agents} />
397
- </div>
398
- </div>
399
- )}
400
- </div>
401
- );
402
- })}
403
- {servers.length > 0 && (
404
- <div className="pt-1 text-[9px] text-[var(--fg-dim)] font-mono text-center">
405
- live · refreshing every 5s
406
- </div>
407
- )}
408
- </div>
420
+ <ServersPanel enabled={open} className="flex-1" />
409
421
  )}
410
422
  </aside>
411
423
  );
@@ -13,6 +13,7 @@ const NAV_ITEMS = [
13
13
  { href: '/', label: 'Overview', icon: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6' },
14
14
  { href: '/tasks', label: 'Tasks', icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4' },
15
15
  { href: '/nodes', label: 'Nodes', icon: 'M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01' },
16
+ { href: '/servers', label: 'Servers', icon: 'M4 6.5A2.5 2.5 0 016.5 4h11A2.5 2.5 0 0120 6.5v1A2.5 2.5 0 0117.5 10h-11A2.5 2.5 0 014 7.5v-1zM4 16.5A2.5 2.5 0 016.5 14h11a2.5 2.5 0 012.5 2.5v1a2.5 2.5 0 01-2.5 2.5h-11A2.5 2.5 0 014 17.5v-1zM7 7h.01M7 17h.01' },
16
17
  { href: '/messages', label: 'Messages', icon: 'M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z' },
17
18
  { href: '/settings/networks', label: 'Networks', icon: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9' },
18
19
  { href: '/logs', label: 'Audit Log', icon: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z' },
@@ -13,6 +13,31 @@ interface ChatTask {
13
13
  content: string;
14
14
  result: string;
15
15
  created_at: string;
16
+ completed_at?: string;
17
+ }
18
+
19
+ type ChatEvent =
20
+ | { kind: 'task'; task: ChatTask; at: string }
21
+ | { kind: 'reply'; task: ChatTask; at: string };
22
+
23
+ function eventTime(value?: string) {
24
+ if (!value) return 0;
25
+ return new Date(value.replace(' ', 'T') + (value.includes('T') ? '' : 'Z')).getTime() || 0;
26
+ }
27
+
28
+ function formatTimestamp(value?: string) {
29
+ if (!value) return '--';
30
+ const d = new Date(value.replace(' ', 'T') + (value.includes('T') ? '' : 'Z'));
31
+ if (Number.isNaN(d.getTime())) return value;
32
+ return d.toLocaleString(undefined, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
33
+ }
34
+
35
+ function quotePreview(text: string, max = 140) {
36
+ const compact = (text || '')
37
+ .replace(/\[Dashboard 附件[\s\S]*$/m, '[附件]')
38
+ .replace(/\s+/g, ' ')
39
+ .trim();
40
+ return compact.length > max ? `${compact.slice(0, max)}…` : compact;
16
41
  }
17
42
 
18
43
  function extractAttachmentPreviews(text: string): string[] {
@@ -433,11 +458,24 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
433
458
  el.style.height = Math.min(el.scrollHeight, 150) + 'px';
434
459
  };
435
460
 
461
+ const chatEvents: ChatEvent[] = messages
462
+ .flatMap((task): ChatEvent[] => {
463
+ const events: ChatEvent[] = [{ kind: 'task', task, at: task.created_at }];
464
+ if (task.result) events.push({ kind: 'reply', task, at: task.completed_at || task.created_at });
465
+ return events;
466
+ })
467
+ .sort((a, b) => {
468
+ const delta = eventTime(a.at) - eventTime(b.at);
469
+ if (delta !== 0) return delta;
470
+ if (a.task.task_id !== b.task.task_id) return a.task.task_id.localeCompare(b.task.task_id);
471
+ return a.kind === 'task' ? -1 : 1;
472
+ });
473
+
436
474
  // Inline mode: just the chat content, no panel chrome
437
475
  const chatContent = (
438
476
  <>
439
477
  {/* Messages area */}
440
- <div className="flex-1 overflow-y-auto px-4 py-4 space-y-4">
478
+ <div className="flex-1 overflow-y-auto px-3 py-3 sm:px-4 sm:py-4 space-y-3 sm:space-y-4">
441
479
  {!historyLoaded && (
442
480
  <div className="flex justify-center py-8">
443
481
  <div className="w-5 h-5 border-2 border-cyan-500/30 border-t-cyan-500 rounded-full animate-spin" />
@@ -451,7 +489,8 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
451
489
  </div>
452
490
  )}
453
491
 
454
- {messages.map((m) => {
492
+ {chatEvents.map((event) => {
493
+ const m = event.task;
455
494
  // Distinguish task source so users can tell when a peer agent
456
495
  // forwarded a task vs when they themselves sent it from Dashboard.
457
496
  const fromUser = !m.from_name || m.from_name === 'Dashboard' || m.from_name === 'api' || m.from_name === 'hub';
@@ -459,11 +498,41 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
459
498
  const senderBadge = fromUser
460
499
  ? null
461
500
  : <span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-purple-500/15 border border-purple-500/30 text-[9px] text-purple-300 font-medium">↳ {m.from_name}</span>;
501
+ if (event.kind === 'reply') {
502
+ return (
503
+ <div key={`${m.task_id}:reply`} className="flex justify-start">
504
+ <div className="max-w-[92%] sm:max-w-[85%] bg-green-500/8 border border-green-500/15 rounded-2xl rounded-bl-md px-3 py-2.5 sm:px-4 shadow-sm">
505
+ <div className="flex items-center justify-between gap-2 mb-2">
506
+ <div className="flex items-center gap-1.5 min-w-0">
507
+ {m.to_name && <AliasAvatar alias={m.to_name} size={14} />}
508
+ <span className="text-[10px] text-[var(--fg)] font-medium truncate">{m.to_name}</span>
509
+ <span className="text-[9px] text-green-300/70">replied</span>
510
+ </div>
511
+ <span className="shrink-0 rounded-md bg-black/20 px-1.5 py-0.5 text-[9px] text-[var(--fg-dim)]" title={event.at}>
512
+ {timeAgo(event.at)} · {formatTimestamp(event.at)}
513
+ </span>
514
+ </div>
515
+ <div className="mb-2 rounded-lg border-l-2 border-cyan-400/40 bg-black/20 px-2.5 py-1.5">
516
+ <div className="text-[9px] text-cyan-300/80">
517
+ 引用 {senderLabel} 的任务 · {formatTimestamp(m.created_at)}
518
+ </div>
519
+ <div className="mt-0.5 max-h-10 overflow-hidden text-[11px] leading-5 text-[var(--fg-muted)]">
520
+ {quotePreview(m.content) || 'No content'}
521
+ </div>
522
+ </div>
523
+ <div className="text-[13px] text-[var(--fg)]">
524
+ <MarkdownContent text={m.result} />
525
+ <AttachmentPreviews text={m.result} />
526
+ </div>
527
+ </div>
528
+ </div>
529
+ );
530
+ }
462
531
  return (
463
- <div key={m.task_id} className="space-y-2">
532
+ <div key={`${m.task_id}:task`} className="space-y-2">
464
533
  {/* Outgoing task — labeled with origin so peer-forwarded tasks are obvious */}
465
534
  <div className="flex justify-end">
466
- <div className="max-w-[85%] bg-cyan-500/8 border border-cyan-500/15 rounded-2xl rounded-br-md px-4 py-2.5 shadow-sm">
535
+ <div className="max-w-[92%] sm:max-w-[85%] bg-cyan-500/8 border border-cyan-500/15 rounded-2xl rounded-br-md px-3 py-2.5 sm:px-4 shadow-sm">
467
536
  <div className="flex items-center gap-2 mb-1">
468
537
  <span className={`text-[10px] font-medium ${fromUser ? 'text-cyan-400' : 'text-purple-300'}`}>{senderLabel}</span>
469
538
  {!fromUser && <span className="text-[9px] text-[var(--fg-dim)]">forwarded to {m.to_name}</span>}
@@ -472,32 +541,18 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
472
541
  <MarkdownContent text={m.content} />
473
542
  <AttachmentPreviews text={m.content} />
474
543
  </div>
475
- <div className="flex items-center justify-between mt-1.5 gap-3">
544
+ <div className="flex flex-wrap items-center justify-between mt-1.5 gap-2 sm:gap-3">
476
545
  <StatusBar status={m.status} />
477
546
  <div className="flex items-center gap-2 shrink-0">
478
547
  {senderBadge}
479
- <span className="text-[9px] text-[var(--fg-dim)]">{timeAgo(m.created_at)}</span>
548
+ <span className="rounded-md bg-black/15 px-1.5 py-0.5 text-[9px] text-[var(--fg-dim)]" title={m.created_at}>
549
+ {timeAgo(m.created_at)} · {formatTimestamp(m.created_at)}
550
+ </span>
480
551
  </div>
481
552
  </div>
482
553
  </div>
483
554
  </div>
484
555
 
485
- {/* Incoming reply */}
486
- {m.result && (
487
- <div className="flex justify-start">
488
- <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">
489
- <div className="flex items-center gap-1.5 mb-1.5">
490
- {m.to_name && <AliasAvatar alias={m.to_name} size={14} />}
491
- <span className="text-[10px] text-[var(--fg)] font-medium">{m.to_name}</span>
492
- </div>
493
- <div className="text-[13px] text-[var(--fg)]">
494
- <MarkdownContent text={m.result} />
495
- <AttachmentPreviews text={m.result} />
496
- </div>
497
- </div>
498
- </div>
499
- )}
500
-
501
556
  {/* Typing indicator when running */}
502
557
  {m.status === 'running' && !m.result && (
503
558
  <div className="flex justify-start">
@@ -517,7 +572,7 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
517
572
  </div>
518
573
 
519
574
  {/* Input area */}
520
- <div className="border-t border-[var(--border)] bg-[var(--bg-secondary)] px-4 py-3">
575
+ <div className="border-t border-[var(--border)] bg-[var(--bg-secondary)] px-3 py-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))] sm:px-4">
521
576
  <div className="flex items-end gap-2">
522
577
  <div className="flex-1 relative">
523
578
  {/* @ mention dropdown */}
@@ -540,7 +595,7 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
540
595
  onPaste={handlePaste}
541
596
  placeholder={`Message ${alias}...`}
542
597
  rows={1}
543
- className="w-full bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl px-4 py-2.5 pr-24 text-sm text-[var(--fg)] placeholder-[var(--fg-dim)] focus:border-cyan-500/40 focus:outline-none resize-none transition-colors"
598
+ className="w-full bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl px-3 py-2.5 pr-24 text-base sm:px-4 sm:text-sm text-[var(--fg)] placeholder-[var(--fg-dim)] focus:border-cyan-500/40 focus:outline-none resize-none transition-colors"
544
599
  />
545
600
  <div className="absolute right-2 bottom-1.5 flex items-center gap-1">
546
601
  <label className="p-1 text-[var(--fg-dim)] hover:text-cyan-300 cursor-pointer rounded hover:bg-cyan-500/10" title="Attach image">
@@ -585,7 +640,7 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
585
640
  ))}
586
641
  </div>
587
642
  )}
588
- <div className="flex justify-between text-[9px] text-[var(--fg-dim)] mt-1.5">
643
+ <div className="hidden sm:flex justify-between text-[9px] text-[var(--fg-dim)] mt-1.5">
589
644
  <span>{input.includes('@') ? `Sending to: ${targetAlias}` : `Type @ to mention another node`}</span>
590
645
  <span>Enter to send · paste image</span>
591
646
  </div>
@@ -600,7 +655,7 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
600
655
  return (
601
656
  <>
602
657
  <div className="fixed inset-0 bg-black/30 z-40 lg:hidden anet-fade-in" onClick={onClose} />
603
- <div className="fixed top-0 right-0 h-full w-full lg:w-[500px] bg-[var(--bg)] border-l border-[var(--border)] z-50 flex flex-col shadow-2xl shadow-black/60 animate-slide-in">
658
+ <div className="fixed top-0 right-0 h-[100dvh] w-full lg:w-[500px] bg-[var(--bg)] border-l border-[var(--border)] z-50 flex flex-col shadow-2xl shadow-black/60 animate-slide-in">
604
659
  {/* Header */}
605
660
  <div className="flex items-center justify-between px-4 py-3 border-b border-[var(--border)] bg-[var(--bg-secondary)]">
606
661
  <div className="flex items-center gap-3">
@@ -81,7 +81,7 @@ export function TaskDrawer({ taskId, onClose }: TaskDrawerProps) {
81
81
  return (
82
82
  <>
83
83
  <div className="fixed inset-0 bg-black/30 z-40 anet-fade-in" onClick={onClose} />
84
- <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">
84
+ <div className="fixed top-0 right-0 h-[100dvh] 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">
85
85
  {/* Header */}
86
86
  <div className="flex items-center justify-between px-5 py-4 border-b border-[#2a2a4a] bg-[#0d0d1a] sticky top-0">
87
87
  <div>
package/app/layout.tsx CHANGED
@@ -30,8 +30,8 @@ export const metadata: Metadata = {
30
30
  export const viewport: Viewport = {
31
31
  width: "device-width",
32
32
  initialScale: 1,
33
- maximumScale: 1,
34
- userScalable: false,
33
+ maximumScale: 5,
34
+ userScalable: true,
35
35
  themeColor: "#0a0a1a",
36
36
  };
37
37