@sleep2agi/agent-network-dashboard 0.6.3-preview.49 → 0.6.3-preview.50

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 (258) hide show
  1. package/.next/BUILD_COMMIT +1 -1
  2. package/.next/BUILD_ID +1 -1
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/diagnostics/route-bundle-stats.json +97 -97
  5. package/.next/fallback-build-manifest.json +3 -3
  6. package/.next/prerender-manifest.json +3 -3
  7. package/.next/server/app/_global-error.html +1 -1
  8. package/.next/server/app/_global-error.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  15. package/.next/server/app/_not-found.html +5 -5
  16. package/.next/server/app/_not-found.rsc +13 -13
  17. package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  18. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  19. package/.next/server/app/_not-found.segments/_index.segment.rsc +8 -8
  20. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  21. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  24. package/.next/server/app/admin.html +5 -5
  25. package/.next/server/app/admin.rsc +15 -15
  26. package/.next/server/app/admin.segments/_full.segment.rsc +15 -15
  27. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  28. package/.next/server/app/admin.segments/_index.segment.rsc +8 -8
  29. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  30. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  31. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  32. package/.next/server/app/index.html +5 -5
  33. package/.next/server/app/index.rsc +15 -15
  34. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  35. package/.next/server/app/index.segments/_full.segment.rsc +15 -15
  36. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  37. package/.next/server/app/index.segments/_index.segment.rsc +8 -8
  38. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  39. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  40. package/.next/server/app/login.html +2 -2
  41. package/.next/server/app/login.rsc +15 -15
  42. package/.next/server/app/login.segments/_full.segment.rsc +15 -15
  43. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  44. package/.next/server/app/login.segments/_index.segment.rsc +8 -8
  45. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  46. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  47. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  48. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  49. package/.next/server/app/logs.html +5 -5
  50. package/.next/server/app/logs.rsc +15 -15
  51. package/.next/server/app/logs.segments/_full.segment.rsc +15 -15
  52. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  53. package/.next/server/app/logs.segments/_index.segment.rsc +8 -8
  54. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  55. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  56. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  57. package/.next/server/app/messages/page.js.nft.json +1 -1
  58. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  59. package/.next/server/app/messages.html +5 -5
  60. package/.next/server/app/messages.rsc +15 -15
  61. package/.next/server/app/messages.segments/_full.segment.rsc +15 -15
  62. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  63. package/.next/server/app/messages.segments/_index.segment.rsc +8 -8
  64. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  65. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  66. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  67. package/.next/server/app/node/page.js.nft.json +1 -1
  68. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  69. package/.next/server/app/node.html +5 -5
  70. package/.next/server/app/node.rsc +15 -15
  71. package/.next/server/app/node.segments/_full.segment.rsc +15 -15
  72. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  73. package/.next/server/app/node.segments/_index.segment.rsc +8 -8
  74. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  75. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  76. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  77. package/.next/server/app/nodes/[alias]/page.js.nft.json +1 -1
  78. package/.next/server/app/nodes/[alias]/page_client-reference-manifest.js +1 -1
  79. package/.next/server/app/nodes/page.js.nft.json +1 -1
  80. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  81. package/.next/server/app/nodes.html +5 -5
  82. package/.next/server/app/nodes.rsc +15 -15
  83. package/.next/server/app/nodes.segments/_full.segment.rsc +15 -15
  84. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  85. package/.next/server/app/nodes.segments/_index.segment.rsc +8 -8
  86. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  87. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  88. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  89. package/.next/server/app/page.js.nft.json +1 -1
  90. package/.next/server/app/page_client-reference-manifest.js +1 -1
  91. package/.next/server/app/providers/page_client-reference-manifest.js +1 -1
  92. package/.next/server/app/providers.html +5 -5
  93. package/.next/server/app/providers.rsc +15 -15
  94. package/.next/server/app/providers.segments/_full.segment.rsc +15 -15
  95. package/.next/server/app/providers.segments/_head.segment.rsc +4 -4
  96. package/.next/server/app/providers.segments/_index.segment.rsc +8 -8
  97. package/.next/server/app/providers.segments/_tree.segment.rsc +2 -2
  98. package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +4 -4
  99. package/.next/server/app/providers.segments/providers.segment.rsc +3 -3
  100. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  101. package/.next/server/app/server-logs.html +5 -5
  102. package/.next/server/app/server-logs.rsc +15 -15
  103. package/.next/server/app/server-logs.segments/_full.segment.rsc +15 -15
  104. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  105. package/.next/server/app/server-logs.segments/_index.segment.rsc +8 -8
  106. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  107. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  108. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  109. package/.next/server/app/servers/page_client-reference-manifest.js +1 -1
  110. package/.next/server/app/servers.html +5 -5
  111. package/.next/server/app/servers.rsc +15 -15
  112. package/.next/server/app/servers.segments/_full.segment.rsc +15 -15
  113. package/.next/server/app/servers.segments/_head.segment.rsc +4 -4
  114. package/.next/server/app/servers.segments/_index.segment.rsc +8 -8
  115. package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
  116. package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +4 -4
  117. package/.next/server/app/servers.segments/servers.segment.rsc +3 -3
  118. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  119. package/.next/server/app/settings/networks.html +5 -5
  120. package/.next/server/app/settings/networks.rsc +15 -15
  121. package/.next/server/app/settings/networks.segments/_full.segment.rsc +15 -15
  122. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  123. package/.next/server/app/settings/networks.segments/_index.segment.rsc +8 -8
  124. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  125. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  126. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  127. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  128. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  129. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  130. package/.next/server/app/settings/tokens.html +5 -5
  131. package/.next/server/app/settings/tokens.rsc +15 -15
  132. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +15 -15
  133. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  134. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +8 -8
  135. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  136. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  137. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  138. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  139. package/.next/server/app/settings.html +5 -5
  140. package/.next/server/app/settings.rsc +15 -15
  141. package/.next/server/app/settings.segments/_full.segment.rsc +15 -15
  142. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  143. package/.next/server/app/settings.segments/_index.segment.rsc +8 -8
  144. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  145. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  146. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  147. package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
  148. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  149. package/.next/server/app/tasks/page.js.nft.json +1 -1
  150. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  151. package/.next/server/app/tasks.html +5 -5
  152. package/.next/server/app/tasks.rsc +15 -15
  153. package/.next/server/app/tasks.segments/_full.segment.rsc +15 -15
  154. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  155. package/.next/server/app/tasks.segments/_index.segment.rsc +8 -8
  156. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  157. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  158. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  159. package/.next/server/chunks/ssr/[root-of-the-server]__05z_qq6._.js +3 -0
  160. package/.next/server/chunks/ssr/[root-of-the-server]__05z_qq6._.js.map +1 -0
  161. package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js +3 -0
  162. package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js.map +1 -0
  163. package/.next/server/chunks/ssr/[root-of-the-server]__0jgqpfv._.js +3 -0
  164. package/.next/server/chunks/ssr/[root-of-the-server]__0jgqpfv._.js.map +1 -0
  165. package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js +1 -1
  166. package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js.map +1 -1
  167. package/.next/server/chunks/ssr/[root-of-the-server]__11wln8.._.js +3 -0
  168. package/.next/server/chunks/ssr/[root-of-the-server]__11wln8.._.js.map +1 -0
  169. package/.next/server/chunks/ssr/{dash-stageb-wt_app_0e7vp44._.js → dash-stageb-wt_app_045dcq5._.js} +2 -2
  170. package/.next/server/chunks/ssr/dash-stageb-wt_app_045dcq5._.js.map +1 -0
  171. package/.next/server/chunks/ssr/{dash-stageb-wt_app_0g~a.7i._.js → dash-stageb-wt_app_05u4_h~._.js} +8 -2
  172. package/.next/server/chunks/ssr/dash-stageb-wt_app_05u4_h~._.js.map +1 -0
  173. package/.next/server/chunks/ssr/{dash-stageb-wt_app_0qpn85e._.js → dash-stageb-wt_app_0nuizh~._.js} +2 -2
  174. package/.next/server/chunks/ssr/dash-stageb-wt_app_0nuizh~._.js.map +1 -0
  175. package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js +1 -1
  176. package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js.map +1 -1
  177. package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js +1 -1
  178. package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js.map +1 -1
  179. package/.next/server/chunks/ssr/dash-stageb-wt_app_components_0vzsmx1._.js +5 -5
  180. package/.next/server/chunks/ssr/dash-stageb-wt_app_components_0vzsmx1._.js.map +1 -1
  181. package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js +1 -1
  182. package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js.map +1 -1
  183. package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_TasksView_tsx_0pnz7dd._.js +3 -0
  184. package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_TasksView_tsx_0pnz7dd._.js.map +1 -0
  185. package/.next/server/middleware-build-manifest.js +3 -3
  186. package/.next/server/pages/404.html +5 -5
  187. package/.next/server/pages/500.html +1 -1
  188. package/.next/server/server-reference-manifest.js +1 -1
  189. package/.next/server/server-reference-manifest.json +1 -1
  190. package/.next/static/chunks/02.5_nifak.uu.js +1 -0
  191. package/.next/static/chunks/03hzj129k~odb.js +1 -0
  192. package/.next/static/chunks/06q7-vhy~1-y3.js +7 -0
  193. package/.next/static/chunks/0_4ljrbp.stxy.js +1 -0
  194. package/.next/static/chunks/0cey4i.y0a8~j.js +1 -0
  195. package/.next/static/chunks/{0i8qbq4v47v3r.js → 0ep78sq90x_yc.js} +1 -1
  196. package/.next/static/chunks/{0_xyyy_x0k42j.js → 0hm52b0y~k0~z.js} +1 -1
  197. package/.next/static/chunks/0kncmrm2a_q43.js +1 -0
  198. package/.next/static/chunks/0mhaf6lrw5nn..css +1 -0
  199. package/.next/static/chunks/{12jbb28xy2hw..js → 0qf8xjihw3zi..js} +1 -1
  200. package/.next/static/chunks/0xyu92zdvvghl.js +7 -0
  201. package/.next/static/chunks/0zj_navpyhltx.js +2 -0
  202. package/.next/static/chunks/10px7ehmo7eq0.js +1 -0
  203. package/.next/static/chunks/{0tv-.l-_mw4ye.js → 11zz15q37~bz5.js} +1 -1
  204. package/.next/static/chunks/14y4x3de1lbug.js +1 -0
  205. package/.next/static/chunks/15ka68xi.w-i-.js +1 -0
  206. package/.next/static/chunks/17qkc714xy04..js +1 -0
  207. package/.next/trace +2 -2
  208. package/.next/trace-build +1 -1
  209. package/app/components/ChatPane.tsx +2 -2
  210. package/app/components/NodeList.tsx +40 -4
  211. package/app/components/Sidebar.tsx +180 -29
  212. package/app/components/TaskChatPanel.tsx +1 -1
  213. package/app/components/TaskDetail.tsx +407 -0
  214. package/app/components/TaskList.tsx +117 -0
  215. package/app/nodes/NodesView.tsx +28 -3
  216. package/app/tasks/TasksView.tsx +353 -0
  217. package/app/tasks/[id]/page.tsx +23 -176
  218. package/app/tasks/page.tsx +10 -453
  219. package/package.json +1 -1
  220. package/tests/e2e-sidebar-default.spec.ts +141 -0
  221. package/tests/e2e-sidebar-icon-rail.spec.ts +261 -0
  222. package/tests/e2e-stage-b-chat-pane.spec.ts +286 -0
  223. package/tests/e2e-stage-tasks-two-column.spec.ts +353 -0
  224. package/.next/server/chunks/ssr/[root-of-the-server]__0b-b6yh._.js +0 -3
  225. package/.next/server/chunks/ssr/[root-of-the-server]__0b-b6yh._.js.map +0 -1
  226. package/.next/server/chunks/ssr/[root-of-the-server]__0fs-j~c._.js +0 -3
  227. package/.next/server/chunks/ssr/[root-of-the-server]__0fs-j~c._.js.map +0 -1
  228. package/.next/server/chunks/ssr/[root-of-the-server]__0ma5-5u._.js +0 -3
  229. package/.next/server/chunks/ssr/[root-of-the-server]__0ma5-5u._.js.map +0 -1
  230. package/.next/server/chunks/ssr/[root-of-the-server]__0~hwpo-._.js +0 -3
  231. package/.next/server/chunks/ssr/[root-of-the-server]__0~hwpo-._.js.map +0 -1
  232. package/.next/server/chunks/ssr/dash-stageb-wt_app_095q0~q._.js +0 -3
  233. package/.next/server/chunks/ssr/dash-stageb-wt_app_095q0~q._.js.map +0 -1
  234. package/.next/server/chunks/ssr/dash-stageb-wt_app_0e7vp44._.js.map +0 -1
  235. package/.next/server/chunks/ssr/dash-stageb-wt_app_0g~a.7i._.js.map +0 -1
  236. package/.next/server/chunks/ssr/dash-stageb-wt_app_0qpn85e._.js.map +0 -1
  237. package/.next/server/chunks/ssr/dash-stageb-wt_app_0tppjr_._.js +0 -9
  238. package/.next/server/chunks/ssr/dash-stageb-wt_app_0tppjr_._.js.map +0 -1
  239. package/.next/server/chunks/ssr/dash-stageb-wt_app_0y764mt._.js +0 -3
  240. package/.next/server/chunks/ssr/dash-stageb-wt_app_0y764mt._.js.map +0 -1
  241. package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_page_tsx_0.fhtup._.js +0 -3
  242. package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_page_tsx_0.fhtup._.js.map +0 -1
  243. package/.next/static/chunks/027yjbjm9afuk.js +0 -1
  244. package/.next/static/chunks/07wf61~wof.l~.js +0 -1
  245. package/.next/static/chunks/0ev252k-tf_aa.css +0 -1
  246. package/.next/static/chunks/0j-jjs3w89dei.js +0 -1
  247. package/.next/static/chunks/0ko39v-gezo7d.js +0 -1
  248. package/.next/static/chunks/0l8vf4q2t5x4d.js +0 -7
  249. package/.next/static/chunks/0xcazs6t8~l4q.js +0 -2
  250. package/.next/static/chunks/10sr651~7vm42.js +0 -7
  251. package/.next/static/chunks/13_i5b0qmko7z.js +0 -1
  252. package/.next/static/chunks/140suz13o9va6.js +0 -1
  253. package/.next/static/chunks/144cf7ds3_9~b.js +0 -1
  254. package/.next/static/chunks/17c4wlsbmmgk9.js +0 -1
  255. package/.next/static/chunks/17cfly-04z1j0.js +0 -1
  256. /package/.next/static/{HIKVEkFShWE481BxQDjwH → vc_aJHtYwefsVcl2_f14Z}/_buildManifest.js +0 -0
  257. /package/.next/static/{HIKVEkFShWE481BxQDjwH → vc_aJHtYwefsVcl2_f14Z}/_clientMiddlewareManifest.js +0 -0
  258. /package/.next/static/{HIKVEkFShWE481BxQDjwH → vc_aJHtYwefsVcl2_f14Z}/_ssgManifest.js +0 -0
@@ -0,0 +1,117 @@
1
+ 'use client';
2
+
3
+ import { AliasAvatar } from './AliasAvatar';
4
+ import { previewContent } from './utils';
5
+ import { STATUS_CHIP_CLASS, STATUS_DOT_HEX } from '../lib/status';
6
+
7
+ // Task-list rail for the /tasks two-column layout (类飞书 "list + detail").
8
+ // Rendered inside TasksView's <aside> — this file is intentionally small
9
+ // and only owns the list-row shape, so the same component can back both
10
+ // desktop rail and (later) a mobile drawer without duplicating the row
11
+ // template.
12
+ //
13
+ // #Stage-tasks: mirrors NodeList.tsx narrow-rail pattern. Row click calls
14
+ // onSelect(task_id) so the parent can drive URL/state; row does NOT know
15
+ // about routing.
16
+
17
+ export interface TaskListItem {
18
+ task_id: string;
19
+ network_id?: string;
20
+ from_name: string;
21
+ to_name: string;
22
+ status: string;
23
+ priority: string;
24
+ content: string;
25
+ created_at: string;
26
+ }
27
+
28
+ function timeAgo(dateStr: string): string {
29
+ if (!dateStr) return '--';
30
+ const diff = Date.now() - new Date(dateStr.replace(' ', 'T') + 'Z').getTime();
31
+ const s = Math.floor(diff / 1000);
32
+ if (s < 60) return `${s}s`;
33
+ if (s < 3600) return `${Math.floor(s / 60)}m`;
34
+ if (s < 86400) return `${Math.floor(s / 3600)}h`;
35
+ return `${Math.floor(s / 86400)}d`;
36
+ }
37
+
38
+ export function TaskList({
39
+ tasks,
40
+ selectedId,
41
+ onSelect,
42
+ }: {
43
+ tasks: TaskListItem[];
44
+ selectedId: string | null;
45
+ onSelect: (task_id: string) => void;
46
+ }) {
47
+ return (
48
+ <div
49
+ data-testid="task-list-rail"
50
+ className="flex flex-col h-full max-h-screen overflow-y-auto"
51
+ >
52
+ {tasks.length === 0 ? (
53
+ <div className="px-4 py-6 text-xs text-gray-600 text-center">
54
+ 没有匹配的任务
55
+ </div>
56
+ ) : (
57
+ <ul className="flex flex-col divide-y divide-[#1c1c1f]">
58
+ {tasks.map((t) => {
59
+ const active = t.task_id === selectedId;
60
+ return (
61
+ <li key={t.task_id}>
62
+ <button
63
+ type="button"
64
+ data-task-row
65
+ data-task-id={t.task_id}
66
+ data-selected={active ? 'true' : 'false'}
67
+ onClick={() => onSelect(t.task_id)}
68
+ className={`w-full text-left px-3 py-2.5 transition-colors ${
69
+ active
70
+ ? 'bg-cyan-500/10 border-l-2 border-cyan-500'
71
+ : 'border-l-2 border-transparent hover:bg-[#1c1c1f]/40'
72
+ }`}
73
+ >
74
+ <div className="flex items-center gap-2 mb-1">
75
+ {/* status dot + label */}
76
+ <span
77
+ aria-hidden
78
+ className="inline-block w-1.5 h-1.5 rounded-full shrink-0"
79
+ style={{ backgroundColor: STATUS_DOT_HEX[t.status] || '#6b7280' }}
80
+ />
81
+ <span
82
+ className={`text-[10px] px-1.5 py-0.5 rounded border ${
83
+ STATUS_CHIP_CLASS[t.status] ||
84
+ 'bg-gray-500/10 text-gray-400 border-gray-500/20'
85
+ }`}
86
+ >
87
+ {t.status}
88
+ </span>
89
+ {t.priority === 'high' && (
90
+ <span className="text-[10px] text-red-400 shrink-0">HIGH</span>
91
+ )}
92
+ <span className="ml-auto text-[10px] text-gray-600 tabular-nums shrink-0">
93
+ {timeAgo(t.created_at)}
94
+ </span>
95
+ </div>
96
+ <div className="flex items-center gap-1 text-xs text-gray-300 min-w-0 mb-1">
97
+ {t.from_name && <AliasAvatar alias={t.from_name} size={14} />}
98
+ <span className="truncate max-w-[35%]">{t.from_name || '--'}</span>
99
+ <span className="text-gray-600 shrink-0">→</span>
100
+ {t.to_name && <AliasAvatar alias={t.to_name} size={14} />}
101
+ <span className="truncate max-w-[35%]">{t.to_name || '--'}</span>
102
+ </div>
103
+ <div
104
+ className="text-[11px] text-gray-500 line-clamp-2 leading-snug"
105
+ title={t.content}
106
+ >
107
+ {previewContent(t.content) || <span className="italic">(no content)</span>}
108
+ </div>
109
+ </button>
110
+ </li>
111
+ );
112
+ })}
113
+ </ul>
114
+ )}
115
+ </div>
116
+ );
117
+ }
@@ -135,6 +135,28 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
135
135
  window.addEventListener('popstate', onPop);
136
136
  return () => window.removeEventListener('popstate', onPop);
137
137
  }, []);
138
+ // #Stage D: the shell above us (AppShell) keeps a ~28px HealthBanner in
139
+ // flow even when green, and its root is min-h (content-driven), so a
140
+ // plain h-[100dvh] here overflows the viewport by exactly the banner
141
+ // height and the document scrolls again. AppShell is shared by every
142
+ // page (out of Stage D scope) — instead, size this page to the space
143
+ // actually left below whatever the shell renders above us. The
144
+ // ResizeObserver on body re-measures when the banner appears/collapses/
145
+ // is dismissed. className keeps h-[100dvh] as the SSR first paint.
146
+ const rootRef = useRef<HTMLDivElement | null>(null);
147
+ useEffect(() => {
148
+ const el = rootRef.current;
149
+ if (!el) return;
150
+ const apply = () => {
151
+ const top = el.getBoundingClientRect().top + window.scrollY;
152
+ el.style.height = `${Math.max(240, window.innerHeight - top)}px`;
153
+ };
154
+ apply();
155
+ window.addEventListener('resize', apply);
156
+ const ro = new ResizeObserver(apply);
157
+ ro.observe(document.body);
158
+ return () => { window.removeEventListener('resize', apply); ro.disconnect(); };
159
+ }, []);
138
160
  const [showCreate, setShowCreate] = useState(false);
139
161
  // #Stage A (dashboard 3-col, 07-31): persistent narrow node list rail
140
162
  // to the left of the existing table. Default on; user can hide to
@@ -229,9 +251,10 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
229
251
 
230
252
  return (
231
253
  <div
254
+ ref={rootRef}
232
255
  data-testid="nodes-layout"
233
256
  data-rail-visible={showRail ? 'true' : 'false'}
234
- className="min-h-screen max-w-full overflow-x-hidden bg-[#0b0b0d] text-gray-100 flex"
257
+ className="h-[100dvh] max-w-full overflow-hidden bg-[#0b0b0d] text-gray-100 flex"
235
258
  >
236
259
  {/* #Stage A rail — only mounted when showRail is true AND we're
237
260
  past the mobile breakpoint (below sm, rail is hidden by CSS
@@ -244,12 +267,14 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
244
267
  {(showRail || chatOpenDesktop) && sessions.length > 0 && (
245
268
  <aside
246
269
  data-testid="node-list-rail"
247
- className="hidden sm:flex sm:flex-col w-[280px] shrink-0 min-h-screen sticky top-0"
270
+ className="hidden sm:flex sm:flex-col w-[280px] shrink-0 h-full min-h-0 bg-[#0F1320]"
248
271
  >
249
272
  <NodeList
250
273
  sessions={railSessions}
251
274
  selectedAlias={chatAlias}
252
275
  onSelect={selectNode}
276
+ search={search}
277
+ onSearchChange={setSearch}
253
278
  />
254
279
  </aside>
255
280
  )}
@@ -257,7 +282,7 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
257
282
  only when NO node is selected on desktop — see chatOpenDesktop
258
283
  note above. Mobile keeps it always (chat is an overlay there). */}
259
284
  {!chatOpenDesktop && (
260
- <div className="flex-1 min-w-0 max-w-full overflow-x-hidden p-4 sm:p-6">
285
+ <div className="flex-1 min-w-0 max-w-full h-full overflow-y-auto overflow-x-hidden p-4 sm:p-6">
261
286
  {/* #209 R31 (mobile vertical rhythm — goal "大幅提升移动端体验",
262
287
  extending R28's Overview pattern + R30's /tasks pattern):
263
288
  this header row + the status-bar wrapper + the filter row
@@ -0,0 +1,353 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Suspense,
5
+ useCallback,
6
+ useEffect,
7
+ useState,
8
+ useSyncExternalStore,
9
+ } from 'react';
10
+ import { useSearchParams } from 'next/navigation';
11
+ import { useNetworkId } from '../lib/network-context';
12
+ import { EmptyState } from '../components/EmptyState';
13
+ import { TaskList, type TaskListItem } from '../components/TaskList';
14
+ import { TaskDetail } from '../components/TaskDetail';
15
+ import {
16
+ TASK_STATUSES,
17
+ STATUS_CHIP_CLASS,
18
+ STATUS_DOT_HEX,
19
+ STATUS_BAR_CLASS,
20
+ } from '../lib/status';
21
+
22
+ // #Stage-tasks (dashboard 类飞书 two-column, 07-31):
23
+ //
24
+ // /tasks moves from one-big-table + inline-expand + drawer to a
25
+ // list-plus-detail layout with a shareable URL /tasks/[id]. Mirrors
26
+ // /nodes NodesView (see app/nodes/NodesView.tsx for the pattern).
27
+ //
28
+ // Deliberately: the old expand-in-place row and the TaskDrawer side
29
+ // sheet are gone from the /tasks page. The right pane replaces both
30
+ // affordances so we don't leave the layout showing "one thing twice"
31
+ // (通信龙 07-31 catch from /nodes: rail added alongside the table left
32
+ // 199 nodes rendered in two places at once). Detail negative
33
+ // assertions in the tests must be paired with a positive assertion so
34
+ // "detail integrated into pane" and "detail feature gone" can't be
35
+ // confused for each other.
36
+ //
37
+ // TaskDrawer.tsx itself is not deleted — no other file imports it,
38
+ // but the boundary of this PR is app/tasks/**, and dead-code sweeps
39
+ // are a separate concern.
40
+
41
+ const MD_QUERY = '(min-width: 768px)';
42
+ function subscribeMd(cb: () => void) {
43
+ const mql = window.matchMedia(MD_QUERY);
44
+ mql.addEventListener('change', cb);
45
+ return () => mql.removeEventListener('change', cb);
46
+ }
47
+ function useIsDesktop(): boolean {
48
+ return useSyncExternalStore(
49
+ subscribeMd,
50
+ () => window.matchMedia(MD_QUERY).matches,
51
+ () => true,
52
+ );
53
+ }
54
+
55
+ // Task IDs on the CommHub side are opaque strings — usually hex, but
56
+ // treat them as arbitrary to match the alias precedent. Decode once
57
+ // defensively (same shape as /nodes NodesView.decodeAliasSegment).
58
+ export function decodeIdSegment(seg: string): string {
59
+ try {
60
+ return decodeURIComponent(seg);
61
+ } catch {
62
+ return seg;
63
+ }
64
+ }
65
+
66
+ function TasksViewInner({ initialSelectedId = null }: { initialSelectedId?: string | null }) {
67
+ const searchParams = useSearchParams();
68
+ const { networkId } = useNetworkId();
69
+ const [tasks, setTasks] = useState<TaskListItem[]>([]);
70
+ const [count, setCount] = useState(0);
71
+ const [loading, setLoading] = useState(true);
72
+ const [error, setError] = useState('');
73
+
74
+ const [filterStatus, setFilterStatus] = useState(searchParams.get('status') || '');
75
+ const [filterFrom, setFilterFrom] = useState('');
76
+ const [filterTo, setFilterTo] = useState('');
77
+
78
+ // #Stage-tasks: selection is seeded from /tasks/[id] URL, then plain
79
+ // client state kept in sync with pushState (shallow — no route remount,
80
+ // switching tasks swaps the right pane in place).
81
+ const [selectedId, setSelectedId] = useState<string | null>(initialSelectedId);
82
+ const isDesktop = useIsDesktop();
83
+
84
+ const selectTask = (task_id: string) => {
85
+ setSelectedId(task_id);
86
+ try {
87
+ window.history.pushState({}, '', `/tasks/${encodeURIComponent(task_id)}`);
88
+ } catch {}
89
+ };
90
+ const closeDetail = () => {
91
+ setSelectedId(null);
92
+ try {
93
+ window.history.pushState({}, '', '/tasks');
94
+ } catch {}
95
+ };
96
+
97
+ // Back/forward — mirror URL into state.
98
+ useEffect(() => {
99
+ const onPop = () => {
100
+ const m = window.location.pathname.match(/^\/tasks\/([^/]+)\/?$/);
101
+ setSelectedId(m ? decodeIdSegment(m[1]) : null);
102
+ };
103
+ window.addEventListener('popstate', onPop);
104
+ return () => window.removeEventListener('popstate', onPop);
105
+ }, []);
106
+
107
+ const fetchTasks = useCallback(async () => {
108
+ try {
109
+ const params = new URLSearchParams();
110
+ if (networkId) params.set('network_id', networkId);
111
+ if (filterStatus) params.set('status', filterStatus);
112
+ if (filterFrom) params.set('from_name', filterFrom);
113
+ if (filterTo) params.set('to_name', filterTo);
114
+ params.set('limit', '100');
115
+
116
+ const res = await fetch(`/api/hub/tasks?${params.toString()}`);
117
+ if (res.status === 401) {
118
+ window.location.assign('/login');
119
+ return;
120
+ }
121
+ const data = await res.json();
122
+ setTasks(data.tasks || []);
123
+ setCount(data.count ?? (data.tasks?.length || 0));
124
+ setError('');
125
+ } catch (e: unknown) {
126
+ setError(e instanceof Error ? e.message : 'fetch failed');
127
+ } finally {
128
+ setLoading(false);
129
+ }
130
+ }, [filterStatus, filterFrom, filterTo, networkId]);
131
+
132
+ useEffect(() => {
133
+ setLoading(true);
134
+ fetchTasks();
135
+ const interval = setInterval(fetchTasks, 5000);
136
+ return () => clearInterval(interval);
137
+ }, [fetchTasks]);
138
+
139
+ const counts: Record<string, number> = {};
140
+ tasks.forEach((t) => {
141
+ counts[t.status] = (counts[t.status] || 0) + 1;
142
+ });
143
+ const total = tasks.length || 1;
144
+ const bars = TASK_STATUSES.map((key) => ({ key, color: STATUS_BAR_CLASS[key] })).filter(
145
+ (b) => counts[b.key],
146
+ );
147
+
148
+ return (
149
+ <div
150
+ data-testid="tasks-layout"
151
+ data-selected-id={selectedId ?? ''}
152
+ className="min-h-screen max-w-full overflow-x-hidden bg-[#0b0b0d] text-gray-100 flex"
153
+ >
154
+ {/* Rail — desktop always visible; on mobile it holds the whole
155
+ page (detail becomes a full-screen overlay). No "toggle
156
+ rail" button on tasks: there is no dense one-column table
157
+ to fall back to (unlike /nodes where Vincent's 看全局 view
158
+ exists). Removing the old table is the whole point of this
159
+ refactor — 通信龙 07-31 catch from /nodes: don't leave the
160
+ layout showing "one thing twice". */}
161
+ <aside
162
+ data-testid="task-list-column"
163
+ className={`flex flex-col w-full sm:w-[380px] shrink-0 min-h-screen sticky top-0 border-r border-[#1c1c1f] ${
164
+ selectedId && !isDesktop ? 'hidden' : 'flex'
165
+ }`}
166
+ >
167
+ <div className="px-4 py-3 border-b border-[#1c1c1f]">
168
+ <div className="flex items-center gap-3 mb-3">
169
+ <h1 className="text-xl font-bold text-white lg:ml-0 ml-10">Tasks</h1>
170
+ {tasks.length < count && (
171
+ <span
172
+ className="text-xs bg-blue-900/30 text-blue-400 px-2 py-0.5 rounded-full border border-blue-800/30 tabular-nums"
173
+ title={`Showing ${tasks.length} of ${count} tasks`}
174
+ >
175
+ {tasks.length} / {count}
176
+ </span>
177
+ )}
178
+ </div>
179
+
180
+ {tasks.length > 0 && (
181
+ <>
182
+ {/* Status tab strip */}
183
+ <div className="anet-tabstrip-wrap mb-2">
184
+ <div className="anet-tabstrip flex flex-wrap gap-1 bg-[#161618] rounded-lg border border-[#26262b] p-1 overflow-x-auto scrollbar-thin">
185
+ {['', ...TASK_STATUSES].map((s) => {
186
+ const cnt = s === '' ? tasks.length : counts[s] || 0;
187
+ const isActive = filterStatus === s;
188
+ return (
189
+ <button
190
+ key={s || 'all'}
191
+ onClick={() => setFilterStatus(s)}
192
+ disabled={cnt === 0 && s !== '' && !isActive}
193
+ className={`px-2.5 py-1 rounded-md text-xs transition-colors flex items-center gap-1 shrink-0 whitespace-nowrap disabled:opacity-30 disabled:cursor-not-allowed ${
194
+ isActive
195
+ ? `${STATUS_CHIP_CLASS[s] || 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20'} border`
196
+ : 'text-gray-500 hover:text-gray-300 hover:bg-[#1c1c1f]/40'
197
+ }`}
198
+ >
199
+ {s && (
200
+ <span
201
+ aria-hidden
202
+ className="inline-block w-1.5 h-1.5 rounded-full shrink-0"
203
+ style={{ backgroundColor: STATUS_DOT_HEX[s] || '#6b7280' }}
204
+ />
205
+ )}
206
+ <span>{s || 'All'}</span>
207
+ <span
208
+ className={`text-[10px] tabular-nums ${
209
+ isActive ? 'opacity-80' : 'text-gray-600'
210
+ }`}
211
+ >
212
+ {cnt}
213
+ </span>
214
+ </button>
215
+ );
216
+ })}
217
+ </div>
218
+ </div>
219
+
220
+ {/* From/To filters */}
221
+ <div className="flex flex-wrap items-center gap-1.5 mb-2">
222
+ <div className="flex items-center gap-1 rounded border border-[#26262b] bg-[#161618] px-2 py-1 focus-within:border-blue-500/40">
223
+ <span className="text-[9px] uppercase tracking-wide text-gray-600">From</span>
224
+ <input
225
+ type="text"
226
+ value={filterFrom}
227
+ onChange={(e) => setFilterFrom(e.target.value)}
228
+ placeholder="any"
229
+ className="w-16 bg-transparent text-xs text-white placeholder-gray-700 focus:outline-none"
230
+ />
231
+ </div>
232
+ <div className="flex items-center gap-1 rounded border border-[#26262b] bg-[#161618] px-2 py-1 focus-within:border-blue-500/40">
233
+ <span className="text-[9px] uppercase tracking-wide text-gray-600">To</span>
234
+ <input
235
+ type="text"
236
+ value={filterTo}
237
+ onChange={(e) => setFilterTo(e.target.value)}
238
+ placeholder="any"
239
+ className="w-16 bg-transparent text-xs text-white placeholder-gray-700 focus:outline-none"
240
+ />
241
+ </div>
242
+ {(filterStatus || filterFrom || filterTo) && (
243
+ <button
244
+ type="button"
245
+ onClick={() => {
246
+ setFilterStatus('');
247
+ setFilterFrom('');
248
+ setFilterTo('');
249
+ }}
250
+ className="rounded border border-gray-700 px-2 py-0.5 text-[10px] text-gray-500 hover:text-gray-200 hover:border-gray-600"
251
+ >
252
+ Clear
253
+ </button>
254
+ )}
255
+ </div>
256
+
257
+ {/* Status distribution bar (only when no status filter) */}
258
+ {!filterStatus && bars.length > 0 && (
259
+ <div className="flex h-1 rounded-full overflow-hidden bg-gray-800">
260
+ {bars.map((b) => (
261
+ <div
262
+ key={b.key}
263
+ className={b.color}
264
+ style={{ width: `${(counts[b.key] / total) * 100}%` }}
265
+ title={`${b.key}: ${counts[b.key]}`}
266
+ />
267
+ ))}
268
+ </div>
269
+ )}
270
+ </>
271
+ )}
272
+
273
+ {error && (
274
+ <div className="bg-red-900/20 border border-red-800/40 text-red-300 px-3 py-1.5 rounded mt-2 text-xs">
275
+ {error}
276
+ </div>
277
+ )}
278
+ </div>
279
+
280
+ <div className="flex-1 min-h-0 overflow-y-auto">
281
+ {loading ? (
282
+ <div className="animate-pulse space-y-1 px-3 py-2">
283
+ {[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
284
+ <div key={i} className="h-14 bg-gray-800/40 rounded" />
285
+ ))}
286
+ </div>
287
+ ) : tasks.length === 0 ? (
288
+ <div className="px-3 py-6">
289
+ <EmptyState
290
+ variant="tasks"
291
+ sub={
292
+ filterStatus || filterFrom || filterTo
293
+ ? 'No tasks match the current filters. Try clearing them.'
294
+ : 'Tasks will appear here when agents send them via CommHub.'
295
+ }
296
+ />
297
+ </div>
298
+ ) : (
299
+ <TaskList tasks={tasks} selectedId={selectedId} onSelect={selectTask} />
300
+ )}
301
+ </div>
302
+ </aside>
303
+
304
+ {/* Detail — desktop: right pane inside the flex row. Mobile: full-screen
305
+ overlay that hides the list column above. Both variants get onClose:
306
+ on desktop it returns the right pane to the empty state (preserves
307
+ the "← Tasks" return affordance the old full-page detail had);
308
+ on mobile it dismisses the overlay. Either way the URL drops back
309
+ to /tasks. 通信龙 07-31 constraint: 返回入口别丢. */}
310
+ {selectedId && isDesktop && (
311
+ <section
312
+ data-testid="task-detail-column"
313
+ className="flex-1 min-w-0 min-h-screen"
314
+ >
315
+ <TaskDetail taskId={selectedId} onClose={closeDetail} onRetried={fetchTasks} />
316
+ </section>
317
+ )}
318
+ {selectedId && !isDesktop && (
319
+ <section
320
+ data-testid="task-detail-mobile"
321
+ className="fixed inset-0 z-40 bg-[#0b0b0d]"
322
+ >
323
+ <TaskDetail taskId={selectedId} onClose={closeDetail} onRetried={fetchTasks} />
324
+ </section>
325
+ )}
326
+ {!selectedId && isDesktop && (
327
+ <section
328
+ data-testid="task-detail-empty"
329
+ className="flex-1 min-w-0 min-h-screen flex items-center justify-center text-sm text-gray-600"
330
+ >
331
+ 请从左侧列表选择一条任务查看详情
332
+ </section>
333
+ )}
334
+ </div>
335
+ );
336
+ }
337
+
338
+ // useSearchParams needs a Suspense boundary in Next 14+ app-router. Same
339
+ // shape the old page.tsx had (wrapper → Inner). Fallback matches the
340
+ // loaded layout width so the burger button doesn't crash into "Loa…".
341
+ export function TasksView({ initialSelectedId = null }: { initialSelectedId?: string | null }) {
342
+ return (
343
+ <Suspense
344
+ fallback={
345
+ <div className="min-h-screen bg-[#0b0b0d] text-gray-100 p-4 sm:p-6">
346
+ <div className="lg:ml-0 ml-10 text-gray-500 text-sm">Loading tasks…</div>
347
+ </div>
348
+ }
349
+ >
350
+ <TasksViewInner initialSelectedId={initialSelectedId} />
351
+ </Suspense>
352
+ );
353
+ }