@sleep2agi/agent-network-dashboard 0.6.3-preview.51 → 0.6.3-preview.53

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 (244) 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 +48 -48
  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.js.nft.json +1 -1
  15. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/server/app/_not-found.html +2 -2
  17. package/.next/server/app/_not-found.rsc +13 -13
  18. package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  19. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  20. package/.next/server/app/_not-found.segments/_index.segment.rsc +8 -8
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  22. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  23. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  24. package/.next/server/app/admin/page.js.nft.json +1 -1
  25. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  26. package/.next/server/app/admin.html +2 -2
  27. package/.next/server/app/admin.rsc +15 -15
  28. package/.next/server/app/admin.segments/_full.segment.rsc +15 -15
  29. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  30. package/.next/server/app/admin.segments/_index.segment.rsc +8 -8
  31. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  32. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  33. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  34. package/.next/server/app/index.html +2 -2
  35. package/.next/server/app/index.rsc +15 -15
  36. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  37. package/.next/server/app/index.segments/_full.segment.rsc +15 -15
  38. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  39. package/.next/server/app/index.segments/_index.segment.rsc +8 -8
  40. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  41. package/.next/server/app/login/page.js.nft.json +1 -1
  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.js.nft.json +1 -1
  52. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  53. package/.next/server/app/logs.html +2 -2
  54. package/.next/server/app/logs.rsc +15 -15
  55. package/.next/server/app/logs.segments/_full.segment.rsc +15 -15
  56. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  57. package/.next/server/app/logs.segments/_index.segment.rsc +8 -8
  58. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  59. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  60. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  61. package/.next/server/app/messages/page.js.nft.json +1 -1
  62. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  63. package/.next/server/app/messages.html +2 -2
  64. package/.next/server/app/messages.rsc +15 -15
  65. package/.next/server/app/messages.segments/_full.segment.rsc +15 -15
  66. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  67. package/.next/server/app/messages.segments/_index.segment.rsc +8 -8
  68. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  69. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  70. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  71. package/.next/server/app/node/page.js.nft.json +1 -1
  72. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  73. package/.next/server/app/node.html +2 -2
  74. package/.next/server/app/node.rsc +15 -15
  75. package/.next/server/app/node.segments/_full.segment.rsc +15 -15
  76. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  77. package/.next/server/app/node.segments/_index.segment.rsc +8 -8
  78. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  79. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  80. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  81. package/.next/server/app/nodes/[alias]/page.js.nft.json +1 -1
  82. package/.next/server/app/nodes/[alias]/page_client-reference-manifest.js +1 -1
  83. package/.next/server/app/nodes/page.js.nft.json +1 -1
  84. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/nodes.html +2 -2
  86. package/.next/server/app/nodes.rsc +15 -15
  87. package/.next/server/app/nodes.segments/_full.segment.rsc +15 -15
  88. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  89. package/.next/server/app/nodes.segments/_index.segment.rsc +8 -8
  90. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  91. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  92. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  93. package/.next/server/app/page.js.nft.json +1 -1
  94. package/.next/server/app/page_client-reference-manifest.js +1 -1
  95. package/.next/server/app/providers/page.js.nft.json +1 -1
  96. package/.next/server/app/providers/page_client-reference-manifest.js +1 -1
  97. package/.next/server/app/providers.html +2 -2
  98. package/.next/server/app/providers.rsc +15 -15
  99. package/.next/server/app/providers.segments/_full.segment.rsc +15 -15
  100. package/.next/server/app/providers.segments/_head.segment.rsc +4 -4
  101. package/.next/server/app/providers.segments/_index.segment.rsc +8 -8
  102. package/.next/server/app/providers.segments/_tree.segment.rsc +2 -2
  103. package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +4 -4
  104. package/.next/server/app/providers.segments/providers.segment.rsc +3 -3
  105. package/.next/server/app/server-logs/page.js.nft.json +1 -1
  106. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  107. package/.next/server/app/server-logs.html +2 -2
  108. package/.next/server/app/server-logs.rsc +15 -15
  109. package/.next/server/app/server-logs.segments/_full.segment.rsc +15 -15
  110. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  111. package/.next/server/app/server-logs.segments/_index.segment.rsc +8 -8
  112. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  113. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  114. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  115. package/.next/server/app/servers/page.js.nft.json +1 -1
  116. package/.next/server/app/servers/page_client-reference-manifest.js +1 -1
  117. package/.next/server/app/servers.html +2 -2
  118. package/.next/server/app/servers.rsc +15 -15
  119. package/.next/server/app/servers.segments/_full.segment.rsc +15 -15
  120. package/.next/server/app/servers.segments/_head.segment.rsc +4 -4
  121. package/.next/server/app/servers.segments/_index.segment.rsc +8 -8
  122. package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
  123. package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +4 -4
  124. package/.next/server/app/servers.segments/servers.segment.rsc +3 -3
  125. package/.next/server/app/settings/networks/page.js.nft.json +1 -1
  126. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  127. package/.next/server/app/settings/networks.html +2 -2
  128. package/.next/server/app/settings/networks.rsc +15 -15
  129. package/.next/server/app/settings/networks.segments/_full.segment.rsc +15 -15
  130. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  131. package/.next/server/app/settings/networks.segments/_index.segment.rsc +8 -8
  132. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  133. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  134. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  135. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  136. package/.next/server/app/settings/page.js.nft.json +1 -1
  137. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  138. package/.next/server/app/settings/tokens/page.js.nft.json +1 -1
  139. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  140. package/.next/server/app/settings/tokens.html +2 -2
  141. package/.next/server/app/settings/tokens.rsc +15 -15
  142. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +15 -15
  143. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  144. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +8 -8
  145. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  146. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  147. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  148. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  149. package/.next/server/app/settings.html +2 -2
  150. package/.next/server/app/settings.rsc +15 -15
  151. package/.next/server/app/settings.segments/_full.segment.rsc +15 -15
  152. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  153. package/.next/server/app/settings.segments/_index.segment.rsc +8 -8
  154. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  155. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  156. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  157. package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
  158. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  159. package/.next/server/app/tasks/page.js.nft.json +1 -1
  160. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  161. package/.next/server/app/tasks.html +2 -2
  162. package/.next/server/app/tasks.rsc +15 -15
  163. package/.next/server/app/tasks.segments/_full.segment.rsc +15 -15
  164. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  165. package/.next/server/app/tasks.segments/_index.segment.rsc +8 -8
  166. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  167. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  168. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  169. package/.next/server/chunks/[root-of-the-server]__0t--g3k._.js +1 -1
  170. package/.next/server/chunks/[root-of-the-server]__0t--g3k._.js.map +1 -1
  171. package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js +1 -1
  172. package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js.map +1 -1
  173. package/.next/server/chunks/ssr/[root-of-the-server]__0dcez4x._.js +11 -0
  174. package/.next/server/chunks/ssr/[root-of-the-server]__0dcez4x._.js.map +1 -0
  175. package/.next/server/chunks/ssr/{[root-of-the-server]__0jgqpfv._.js → [root-of-the-server]__0gyqilt._.js} +2 -2
  176. package/.next/server/chunks/ssr/[root-of-the-server]__0gyqilt._.js.map +1 -0
  177. package/.next/server/chunks/ssr/[root-of-the-server]__0j-def.._.js +11 -0
  178. package/.next/server/chunks/ssr/[root-of-the-server]__0j-def.._.js.map +1 -0
  179. package/.next/server/chunks/ssr/{[root-of-the-server]__11wln8.._.js → [root-of-the-server]__0oavw6j._.js} +2 -2
  180. package/.next/server/chunks/ssr/[root-of-the-server]__0oavw6j._.js.map +1 -0
  181. package/.next/server/chunks/ssr/[root-of-the-server]__0rct4.k._.js +3 -0
  182. package/.next/server/chunks/ssr/{[root-of-the-server]__02o~-py._.js.map → [root-of-the-server]__0rct4.k._.js.map} +1 -1
  183. package/.next/server/chunks/ssr/dash-stageb-wt_app_05u4_h~._.js +2 -2
  184. package/.next/server/chunks/ssr/dash-stageb-wt_app_05u4_h~._.js.map +1 -1
  185. package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js +1 -1
  186. package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js.map +1 -1
  187. package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js +1 -1
  188. package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js.map +1 -1
  189. package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js +2 -2
  190. package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js.map +1 -1
  191. package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_TasksView_tsx_0pnz7dd._.js +1 -1
  192. package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_TasksView_tsx_0pnz7dd._.js.map +1 -1
  193. package/.next/server/functions-config-manifest.json +4 -0
  194. package/.next/server/middleware-build-manifest.js +3 -3
  195. package/.next/server/pages/404.html +2 -2
  196. package/.next/server/pages/500.html +1 -1
  197. package/.next/server/server-reference-manifest.js +1 -1
  198. package/.next/server/server-reference-manifest.json +1 -1
  199. package/.next/static/chunks/051a-ut_4_f~3.js +9 -0
  200. package/.next/static/chunks/{0advoo2tzdcyi.js → 0hhij-plsekos.js} +1 -1
  201. package/.next/static/chunks/{0_4ljrbp.stxy.js → 0i465ex5.l573.js} +1 -1
  202. package/.next/static/chunks/{10px7ehmo7eq0.js → 0jbkmykrnr~q6.js} +1 -1
  203. package/.next/static/chunks/0lc115vavown3.css +1 -0
  204. package/.next/static/chunks/{0z_4d13crd_af.js → 0n_7389-uuzik.js} +1 -1
  205. package/.next/static/chunks/0oai8y23-yy20.js +9 -0
  206. package/.next/static/chunks/{0dkk_l872ocgm.js → 0ohx4jpm9~61..js} +1 -1
  207. package/.next/static/chunks/{0uk67ty_0h_36.js → 0p3f6dx723h6w.js} +1 -1
  208. package/.next/static/chunks/0sy8dlbnq8b.5.js +2 -0
  209. package/.next/static/chunks/{0fz7zk.ek.154.js → 0u5y.gmi~jkw6.js} +1 -1
  210. package/.next/static/chunks/0ws0p1n62l~ub.js +1 -0
  211. package/.next/static/chunks/{06q7-vhy~1-y3.js → 0~rx9_~1wupcp.js} +2 -2
  212. package/.next/static/{JL_QjNKv1KhsCF68ZK28x → wbJm2a-VxMJqS77WvU7--}/_clientMiddlewareManifest.js +4 -0
  213. package/.next/trace +2 -2
  214. package/.next/trace-build +1 -1
  215. package/CHANGELOG.md +143 -0
  216. package/app/components/ChatPane.tsx +66 -25
  217. package/app/components/ChatPaneOverflowMenu.tsx +152 -0
  218. package/app/components/NodeList.tsx +14 -4
  219. package/app/components/TaskChatPanel.tsx +172 -31
  220. package/app/messages/page.tsx +22 -22
  221. package/app/nodes/NodesView.tsx +52 -11
  222. package/app/tasks/TasksView.tsx +12 -2
  223. package/package.json +1 -1
  224. package/proxy.ts +50 -1
  225. package/tests/e2e-9-nodes-team-tag-filter.spec.ts +14 -7
  226. package/tests/e2e-batch-list-actions.spec.ts +6 -0
  227. package/tests/e2e-chatpane-header-menu.spec.ts +259 -0
  228. package/tests/e2e-messages-spec-visual-align.spec.ts +244 -0
  229. package/tests/e2e-pin-mute-attr-always-present.spec.ts +171 -0
  230. package/tests/e2e-proxy-auth-boundary.spec.ts +196 -0
  231. package/tests/e2e-stage-a-node-list.spec.ts +14 -7
  232. package/tests/e2e-stage-b-chat-pane.spec.ts +74 -10
  233. package/tests/e2e-status-labels.spec.ts +218 -0
  234. package/.next/server/chunks/ssr/[root-of-the-server]__02o~-py._.js +0 -3
  235. package/.next/server/chunks/ssr/[root-of-the-server]__0jgqpfv._.js.map +0 -1
  236. package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js +0 -9
  237. package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js.map +0 -1
  238. package/.next/server/chunks/ssr/[root-of-the-server]__11wln8.._.js.map +0 -1
  239. package/.next/static/chunks/05~lgfzsv.p~p.js +0 -2
  240. package/.next/static/chunks/092zqb~4un32l.css +0 -1
  241. package/.next/static/chunks/11zz15q37~bz5.js +0 -7
  242. package/.next/static/chunks/17qkc714xy04..js +0 -1
  243. /package/.next/static/{JL_QjNKv1KhsCF68ZK28x → wbJm2a-VxMJqS77WvU7--}/_buildManifest.js +0 -0
  244. /package/.next/static/{JL_QjNKv1KhsCF68ZK28x → wbJm2a-VxMJqS77WvU7--}/_ssgManifest.js +0 -0
@@ -1,9 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { useState } from 'react';
3
+ import { useRef, useState } from 'react';
4
4
  import { AliasAvatar } from './AliasAvatar';
5
5
  import { TaskChatPanel } from './TaskChatPanel';
6
6
  import { NodeSettingsPanel } from './NodeSettingsPanel';
7
+ import { ChatPaneOverflowMenu } from './ChatPaneOverflowMenu';
7
8
  import { useSessions } from '../lib/hooks';
8
9
  import { t } from '../lib/ui-lang';
9
10
 
@@ -35,10 +36,20 @@ export interface ChatPaneProps {
35
36
  alias: string;
36
37
  /** Close the pane (unselect + URL back to /nodes — parent owns both). */
37
38
  onClose: () => void;
39
+ /** #Mobile two-level nav: 'page' renders full-width/full-height with a
40
+ * back arrow (Feishu phone form — chat is a page, not a column).
41
+ * Default 'column' keeps the desktop right-column behavior. */
42
+ variant?: 'column' | 'page';
43
+ /** Back arrow handler (page variant). Falls back to onClose. */
44
+ onBack?: () => void;
38
45
  }
39
46
 
40
- export function ChatPane({ alias, onClose }: ChatPaneProps) {
47
+ export function ChatPane({ alias, onClose, variant = 'column', onBack }: ChatPaneProps) {
41
48
  const [settingsOpen, setSettingsOpen] = useState(false);
49
+ // 07-31 通信龙 batch (SPEC §12 remaining): ⋮ opens overflow menu
50
+ // (not settings direct). Menu items include 会话设置 as one of four.
51
+ const [overflowOpen, setOverflowOpen] = useState(false);
52
+ const overflowAnchorRef = useRef<HTMLButtonElement | null>(null);
42
53
  // Look up session for NodeSettingsPanel — it requires the full row.
43
54
  // useSessions is SWR-cached (same key as NodesView), so no extra
44
55
  // network cost.
@@ -54,35 +65,56 @@ export function ChatPane({ alias, onClose }: ChatPaneProps) {
54
65
  // drawer would cover the whole page — the exact regression
55
66
  // 通信龙's judgment ("settings-as-right-drawer, not 4th column
56
67
  // and not fullscreen") is designed to prevent.
57
- className="relative hidden md:flex md:flex-col flex-1 min-w-0 h-full bg-[#141826]"
68
+ // page variant (#mobile two-level nav): full-bleed chat page below md.
69
+ // pb keeps the composer above the fixed MobileNav (visible < lg) —
70
+ // same offset AppShell reserves for normal pages.
71
+ className={variant === 'page'
72
+ ? 'relative flex flex-col w-full min-w-0 h-full bg-[#141826] pb-[calc(5rem+env(safe-area-inset-bottom))] lg:pb-0'
73
+ : 'relative hidden md:flex md:flex-col flex-1 min-w-0 h-full bg-[#141826] pb-[calc(5rem+env(safe-area-inset-bottom))] lg:pb-0'}
58
74
  >
59
75
  <div className="flex items-center justify-between gap-2 px-3 py-2.5 border-b border-[rgba(255,255,255,0.06)] bg-[#141826] shrink-0">
60
76
  <div className="flex items-center gap-2 min-w-0">
77
+ {variant === 'page' && (
78
+ <button
79
+ type="button"
80
+ onClick={onBack ?? onClose}
81
+ aria-label={t('返回列表', 'Back to list')}
82
+ data-testid="chat-back"
83
+ className="inline-flex h-8 w-8 -ml-1 items-center justify-center rounded-lg text-gray-400 hover:text-gray-100 hover:bg-[#1c1c1f] transition-colors shrink-0"
84
+ >
85
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
86
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
87
+ </svg>
88
+ </button>
89
+ )}
61
90
  <AliasAvatar alias={alias} size={26} />
62
91
  <span className="truncate text-sm font-semibold text-white">{alias}</span>
63
92
  </div>
64
- {/* SPEC §12: right-side icon row. ⋮ IS the settings entry
65
- (opens NodeSettingsPanel directly rather than expanding an
66
- overflow menu — spec calls for both, but MVP folds them
67
- into one click since the only overflow item that has a
68
- wired backend right now IS settings; other items in the
69
- spec's 5-item overflow are not yet implemented, per
70
- INVENTORY.md). Script accepts either
71
- data-testid="chat-pane-settings" or "chat-pane-more" or
72
- aria-label containing "设置" — we set testid=settings,
73
- aria-label contains "设置", and add data-chat-pane-more so
74
- all three script matchers hit. */}
93
+ {/* SPEC §12 (this batch): four header controls.
94
+ - ⋮ chat-pane-more opens overflow dropdown (4 items)
95
+ - 📋 chat-pane-view-tasks jumps to /tasks?to_name=<alias>
96
+ (URL param wired via TasksView.tsx:75-76 same batch —
97
+ the prior `?alias=` was silently dropped in prod)
98
+ - 🔍 chat-pane-search dispatches CustomEvent
99
+ 'chat:open-search' which TaskChatPanel listens for
100
+ (search infra already existed; needed a header trigger)
101
+ - ✕ chat-pane-close unchanged
102
+ NodeSettingsPanel is now inside the overflow menu as
103
+ 'chat-pane-settings' (same testid preserved), NOT the
104
+ direct handler of ⋮. */}
75
105
  <div className="flex items-center gap-1 shrink-0">
76
106
  <button
107
+ ref={overflowAnchorRef}
77
108
  type="button"
78
- onClick={() => setSettingsOpen(true)}
79
- aria-label={t('打开会话设置', 'Open conversation settings')}
80
- title={t('会话设置', 'Conversation settings')}
81
- data-testid="chat-pane-settings"
82
- data-chat-pane-more
109
+ onClick={() => setOverflowOpen(v => !v)}
110
+ aria-label={t('会话溢出菜单', 'Chat overflow menu')}
111
+ aria-haspopup="menu"
112
+ aria-expanded={overflowOpen}
113
+ title={t('更多', 'More')}
114
+ data-testid="chat-pane-more"
115
+ data-menu-open={overflowOpen ? 'true' : 'false'}
83
116
  className="inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 hover:text-gray-200 hover:bg-[#1c1c1f] transition-colors"
84
117
  >
85
- {/* ⋮ vertical dots — spec §12 "overflow menu" glyph. */}
86
118
  <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
87
119
  <circle cx="12" cy="5" r="1.5" fill="currentColor" />
88
120
  <circle cx="12" cy="12" r="1.5" fill="currentColor" />
@@ -90,7 +122,7 @@ export function ChatPane({ alias, onClose }: ChatPaneProps) {
90
122
  </svg>
91
123
  </button>
92
124
  <a
93
- href={`/tasks?alias=${encodeURIComponent(alias)}`}
125
+ href={`/tasks?to_name=${encodeURIComponent(alias)}`}
94
126
  aria-label={t(`查看 ${alias} 的任务`, `View ${alias} tasks`)}
95
127
  title={t('查看任务', 'View tasks')}
96
128
  data-testid="chat-pane-view-tasks"
@@ -102,11 +134,12 @@ export function ChatPane({ alias, onClose }: ChatPaneProps) {
102
134
  </a>
103
135
  <button
104
136
  type="button"
105
- disabled
106
- aria-label={t('会话内搜索(即将支持)', 'In-chat search (coming soon)')}
107
- title={t('即将支持', 'Coming soon')}
137
+ onClick={() => window.dispatchEvent(new CustomEvent('chat:open-search', { detail: { alias } }))}
138
+ aria-label={t('会话内搜索', 'In-chat search')}
139
+ title={t('搜索(⌘/Ctrl+F)', 'Search (⌘/Ctrl+F)')}
108
140
  data-testid="chat-pane-search"
109
- className="inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-700 cursor-not-allowed"
141
+ data-search-enabled="true"
142
+ className="inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 hover:text-gray-200 hover:bg-[#1c1c1f] transition-colors"
110
143
  >
111
144
  <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
112
145
  <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
@@ -144,6 +177,14 @@ export function ChatPane({ alias, onClose }: ChatPaneProps) {
144
177
  positioning="absolute"
145
178
  />
146
179
  )}
180
+ {overflowOpen && (
181
+ <ChatPaneOverflowMenu
182
+ alias={alias}
183
+ anchorRef={overflowAnchorRef}
184
+ onClose={() => setOverflowOpen(false)}
185
+ onOpenSettings={() => setSettingsOpen(true)}
186
+ />
187
+ )}
147
188
  </aside>
148
189
  );
149
190
  }
@@ -0,0 +1,152 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ import { togglePin, usePinned } from '../lib/chat-pin';
6
+ import { t } from '../lib/ui-lang';
7
+
8
+ // SPEC §12: chat header ⋮ opens an overflow menu. Container reuses the
9
+ // same visual tokens as NodeListContextMenu (SPEC §10): 200px width,
10
+ // 32px item height, #1E293B background, 1px border, no shadow, edge-
11
+ // flip on overflow. Portal to body so overflow-y-auto ancestors don't
12
+ // clip us.
13
+ //
14
+ // Items:
15
+ // - 添加标签页 / 移除标签页 → togglePin(alias) (label swaps by state)
16
+ // - 查看任务 → navigate /tasks?to_name=<alias>
17
+ // - 会话设置 → open NodeSettingsPanel (via onOpenSettings)
18
+ // - ─ → separator (visual only, non-interactive)
19
+ //
20
+ // 清空历史 is intentionally absent — hub has no clear-history endpoint
21
+ // (see issue #66); rendering it as pure-client setChatEvents([]) would
22
+ // "hide until next fetch", which is worse than not offering it.
23
+
24
+ export interface ChatPaneOverflowMenuProps {
25
+ alias: string;
26
+ anchorRef: React.RefObject<HTMLElement | null>;
27
+ onClose: () => void;
28
+ onOpenSettings: () => void;
29
+ }
30
+
31
+ const EDGE = 8;
32
+
33
+ export function ChatPaneOverflowMenu({ alias, anchorRef, onClose, onOpenSettings }: ChatPaneOverflowMenuProps) {
34
+ const pinned = usePinned(alias);
35
+ const menuRef = useRef<HTMLDivElement | null>(null);
36
+ const [pos, setPos] = useState<{ top: number; left: number; flippedY: boolean; flippedX: boolean }>({
37
+ top: 0, left: 0, flippedY: false, flippedX: false,
38
+ });
39
+
40
+ // Position below the anchor (⋮ button) by default; flip up if would
41
+ // overflow bottom, flip left if would overflow right. Same logic as
42
+ // NodeListContextMenu but anchored to an element instead of a cursor.
43
+ useLayoutEffect(() => {
44
+ if (!anchorRef.current || !menuRef.current) return;
45
+ const a = anchorRef.current.getBoundingClientRect();
46
+ const m = menuRef.current.getBoundingClientRect();
47
+ const vh = window.innerHeight;
48
+ const vw = window.innerWidth;
49
+ let top = a.bottom + 4;
50
+ let left = a.right - m.width;
51
+ let flippedY = false;
52
+ let flippedX = false;
53
+ if (top + m.height + EDGE > vh) {
54
+ top = Math.max(EDGE, a.top - m.height - 4);
55
+ flippedY = true;
56
+ }
57
+ if (left < EDGE) {
58
+ left = a.left;
59
+ flippedX = true;
60
+ }
61
+ top = Math.max(EDGE, Math.min(top, vh - m.height - EDGE));
62
+ left = Math.max(EDGE, Math.min(left, vw - m.width - EDGE));
63
+ setPos({ top, left, flippedY, flippedX });
64
+ }, [anchorRef]);
65
+
66
+ // Close on click-outside (excluding the anchor button itself, which
67
+ // has its own toggle handler) and Escape.
68
+ useEffect(() => {
69
+ const onDown = (e: MouseEvent) => {
70
+ const target = e.target as Node;
71
+ if (menuRef.current?.contains(target)) return;
72
+ if (anchorRef.current?.contains(target)) return;
73
+ onClose();
74
+ };
75
+ const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
76
+ document.addEventListener('mousedown', onDown, true);
77
+ document.addEventListener('keydown', onKey);
78
+ return () => {
79
+ document.removeEventListener('mousedown', onDown, true);
80
+ document.removeEventListener('keydown', onKey);
81
+ };
82
+ }, [anchorRef, onClose]);
83
+
84
+ if (typeof window === 'undefined') return null;
85
+
86
+ return createPortal(
87
+ <div
88
+ ref={menuRef}
89
+ role="menu"
90
+ aria-label={t('会话溢出菜单', 'Chat overflow menu')}
91
+ data-testid="chat-pane-overflow-menu"
92
+ data-flipped-y={pos.flippedY ? 'true' : undefined}
93
+ data-flipped-x={pos.flippedX ? 'true' : undefined}
94
+ style={{ position: 'fixed', top: pos.top, left: pos.left, width: 200, zIndex: 200 }}
95
+ className="rounded-md border border-[rgba(255,255,255,0.08)] bg-[#1E293B] py-1 text-[13px] text-gray-100"
96
+ >
97
+ <MenuItem
98
+ testid="chat-pane-overflow-pin"
99
+ onClick={() => { togglePin(alias); onClose(); }}
100
+ >
101
+ {pinned ? t('移除标签页', 'Remove tab') : t('添加标签页', 'Add as tab')}
102
+ </MenuItem>
103
+ <MenuItemLink
104
+ testid="chat-pane-overflow-view-tasks"
105
+ href={`/tasks?to_name=${encodeURIComponent(alias)}`}
106
+ onClick={onClose}
107
+ >
108
+ {t('查看任务', 'View tasks')}
109
+ </MenuItemLink>
110
+ <MenuItem
111
+ testid="chat-pane-settings"
112
+ onClick={() => { onOpenSettings(); onClose(); }}
113
+ >
114
+ {t('会话设置', 'Conversation settings')}
115
+ </MenuItem>
116
+ <div
117
+ role="separator"
118
+ data-testid="chat-pane-overflow-separator"
119
+ className="my-1 h-px bg-[rgba(255,255,255,0.08)]"
120
+ />
121
+ </div>,
122
+ document.body,
123
+ );
124
+ }
125
+
126
+ function MenuItem({ children, onClick, testid }: { children: React.ReactNode; onClick: () => void; testid: string }) {
127
+ return (
128
+ <button
129
+ type="button"
130
+ role="menuitem"
131
+ data-testid={testid}
132
+ onClick={onClick}
133
+ className="flex h-8 w-full items-center px-3 text-left hover:bg-[rgba(125,211,252,0.08)] focus:bg-[rgba(125,211,252,0.08)] focus:outline-none"
134
+ >
135
+ {children}
136
+ </button>
137
+ );
138
+ }
139
+
140
+ function MenuItemLink({ children, href, onClick, testid }: { children: React.ReactNode; href: string; onClick: () => void; testid: string }) {
141
+ return (
142
+ <a
143
+ role="menuitem"
144
+ data-testid={testid}
145
+ href={href}
146
+ onClick={onClick}
147
+ className="flex h-8 w-full items-center px-3 text-left hover:bg-[rgba(125,211,252,0.08)] focus:bg-[rgba(125,211,252,0.08)] focus:outline-none"
148
+ >
149
+ {children}
150
+ </a>
151
+ );
152
+ }
@@ -79,8 +79,15 @@ function NodeListRow({ session: s, selected, unread, onSelect, onContextMenu }:
79
79
  data-node-list-item
80
80
  data-node-list-alias={s.alias}
81
81
  data-selected={selected ? 'true' : 'false'}
82
- data-node-pinned={pinned ? 'true' : undefined}
83
- data-node-muted={muted ? 'true' : undefined}
82
+ // 通信龙 07-31: attribute is ALWAYS present ('true'|'false'),
83
+ // never removed. This separates capability (attr exists on every
84
+ // row → wire is intact) from state (attr === 'true' → user did
85
+ // pin/mute this one). Prior form (undefined when false) made a
86
+ // gate script's "at least one on page" check indistinguishable
87
+ // from "wire got ripped out" — both looked like `[data-node-pinned]`
88
+ // empty. See feedback_tolerant_assertion_accepts_noncompliance.
89
+ data-node-pinned={pinned ? 'true' : 'false'}
90
+ data-node-muted={muted ? 'true' : 'false'}
84
91
  onClick={() => onSelect(s.alias)}
85
92
  onContextMenu={e => onContextMenu(s.alias, e)}
86
93
  // SPEC §11.1: pinned row = whole-row light background
@@ -204,7 +211,10 @@ export function NodeList({ sessions, selectedAlias, onSelect, search, onSearchCh
204
211
  // Instead: attach ONCE (empty deps), read latest values via ref.
205
212
  const scrollRef = useRef<HTMLDivElement | null>(null);
206
213
  const stateRef = useRef({ sessions, selectedAlias, onSelect });
207
- stateRef.current = { sessions, selectedAlias, onSelect };
214
+ // (was a bare write during render — react-hooks/refs violation caught by
215
+ // the pre-commit gate; an effect keeps the same "listener reads latest
216
+ // values" semantics, updated post-render.)
217
+ useEffect(() => { stateRef.current = { sessions, selectedAlias, onSelect }; });
208
218
  useEffect(() => {
209
219
  if (typeof window === 'undefined') return;
210
220
  const onKey = (e: KeyboardEvent) => {
@@ -276,7 +286,7 @@ export function NodeList({ sessions, selectedAlias, onSelect, search, onSearchCh
276
286
  onClose={() => setMenu(null)}
277
287
  />
278
288
  )}
279
- <div ref={scrollRef} data-testid="node-list-scroll" className="flex-1 min-h-0 overflow-y-auto">
289
+ <div ref={scrollRef} data-testid="node-list-scroll" className="flex-1 min-h-0 overflow-y-auto pb-[calc(5rem+env(safe-area-inset-bottom))] lg:pb-0">
280
290
  {sessions.length === 0 ? (
281
291
  search ? (
282
292
  /* #Stage D (Vincent ③ 判据): a no-match search MUST look
@@ -315,45 +315,170 @@ function LocalFilePreview({ file, onRemove }: { file: File; onRemove: () => void
315
315
  );
316
316
  }
317
317
 
318
- const STATUS_STEPS = ['created', 'delivered', 'running', 'replied'];
319
- const STATUS_COLORS: Record<string, string> = {
320
- created: 'bg-gray-400', delivered: 'bg-blue-400', running: 'bg-green-400',
321
- replied: 'bg-purple-400', failed: 'bg-red-400', closed: 'bg-gray-500',
318
+ // 通信龙 07-31 GAP 2nd tier: translate hub status codes into human
319
+ // labels. The old form was `● ─ ● ─ ○ ─ ○ delivered` — a colored dot
320
+ // progression + the raw English word. Nobody could tell "delivered"
321
+ // (hub-side mark) from "acked" (node process received) from "replied"
322
+ // (agent actually processed).
323
+ //
324
+ // Two things this rewrite fixes:
325
+ //
326
+ // 1. STATUS_STEPS was `['created','delivered','running','replied']`.
327
+ // Neither `created` nor `running` correspond to hub-emitted terminal
328
+ // statuses (`created` is client-side optimistic, `running` is READ
329
+ // by hub queries but never WRITTEN — grep server/src for `= 'running'`
330
+ // returns zero). Real hub progression is queued → delivered → acked
331
+ // → replied, and the old third dot literally never lit up.
332
+ //
333
+ // 2. `delivered` was labeled as if it meant "message reached the node".
334
+ // Reading hub source (server/src/index.ts:1568 + tools.ts:1080/1232)
335
+ // shows it's set at task-insert time — same instant as delivered_at
336
+ // (which is why delivered_at delay is empirically 0). The label now
337
+ // says "已推送" and the hover explicitly disclaims node reception.
338
+ //
339
+ // Vocabulary — must stay in sync with hub emits:
340
+ // queued / delivered / acked / replied / failed ← the primary 5
341
+ // created ← client-side pre-POST
342
+ // closed / expired / cancelled ← terminal, non-reply
343
+ // running ← reserved (never written today)
344
+ // <anything else> ← fail-loudly (raw + banner)
345
+ const STATUS_STEPS = ['queued', 'delivered', 'acked', 'replied'] as const;
346
+ type StatusInfo = { label: string; hover: string; color: string; icon: React.ReactNode };
347
+ const STATUS_INFO: Record<string, StatusInfo> = {
348
+ created: { label: '发送中', hover: '客户端已建消息,尚未收到 hub 回执。', color: 'text-gray-400', icon: <IconSending /> },
349
+ queued: { label: '排队中', hover: '已入队,尚未推给节点。节点可能离线、或通道拥堵。', color: 'text-gray-400', icon: <IconClock /> },
350
+ delivered: { label: '已推送', hover: 'hub 侧已标记推送。尚未确认节点是否收到(delivered_at 是入队时刻,不代表 SSE 已到达)。', color: 'text-sky-400', icon: <IconCheck /> },
351
+ acked: { label: '已接收', hover: '节点进程已确认收到。未必已处理——对方 agent 可能尚未唤醒。', color: 'text-gray-300', icon: <IconCheckDouble /> },
352
+ replied: { label: '已回复', hover: '对方已针对此消息回复。', color: 'text-emerald-400', icon: <IconCheckDouble filled /> },
353
+ failed: { label: '失败', hover: '发送失败——详细原因见消息下方文本。', color: 'text-red-400', icon: <IconWarn /> },
354
+ closed: { label: '已关闭', hover: '会话已被关闭,不再期待回复。', color: 'text-gray-500', icon: <IconClose /> },
355
+ expired: { label: '已超时', hover: '在期限内未收到回复,任务已超时。', color: 'text-gray-500', icon: <IconClock /> },
356
+ cancelled: { label: '已取消', hover: '任务被主动取消。', color: 'text-gray-500', icon: <IconClose /> },
357
+ running: { label: '处理中', hover: '节点进程正在处理(今日 hub 不写入此值,见组件注释)。', color: 'text-green-400', icon: <IconClock /> },
322
358
  };
323
359
 
324
- function StatusBar({ status }: { status: string }) {
325
- const idx = STATUS_STEPS.indexOf(status);
326
- const isFailed = status === 'failed';
360
+ function StatusBar({ status, result }: { status: string; result?: string }) {
361
+ const info = STATUS_INFO[status];
362
+ const known = !!info;
363
+ // Progression label list for the hover — same string appended after
364
+ // the per-status hover so the reader gets a compact map of the flow.
365
+ // Not per-step timestamps: hub doesn't expose queued_at / acked_at
366
+ // separately from delivered_at, so a per-step timeline would be
367
+ // fabricated data. See feedback_assert_the_fact_not_the_declaration.
368
+ const idx = STATUS_STEPS.indexOf(status as typeof STATUS_STEPS[number]);
369
+ const flow = STATUS_STEPS.map((s, i) => {
370
+ const mark = i < idx || (i === idx && idx >= 0) ? '✓' : '○';
371
+ return `${mark} ${STATUS_INFO[s].label}`;
372
+ }).join(' → ');
373
+ const hoverText = known
374
+ ? `${info.hover}\n\n进度:${flow}`
375
+ : `未知状态「${status}」——请报 bug。UI 词库没有匹配项,可能 hub 加了新 status 但前端没跟上。`;
376
+ const label = known ? info.label : status;
377
+ const color = known ? info.color : 'text-yellow-400';
378
+
379
+ // failed inline reason — hub has no dedicated failure_reason column
380
+ // (task table: task_id/from_*/to_*/status/content/result/... —
381
+ // grep-verified as of 2026-07-31). We show `result` first 40 chars
382
+ // when present. When result is empty, we explicitly say "节点未提供
383
+ // 原因" rather than falling back to a generic "未知错误" — that
384
+ // distinguishes "hub has no error field" from "reason field exists
385
+ // but got a bad value".
386
+ const failedReason = status === 'failed'
387
+ ? ((result && result.trim()) ? result.trim().slice(0, 40) : '节点未提供原因')
388
+ : null;
389
+
327
390
  return (
328
- <div className="flex items-center gap-1 mt-1.5">
329
- {STATUS_STEPS.map((s, i) => (
330
- <div key={s} className="flex items-center gap-1">
331
- <div className={`w-2 h-2 rounded-full transition-all duration-700 ${
332
- isFailed ? (i === 0 ? 'bg-red-400' : 'bg-[var(--border)]')
333
- : i <= idx ? (STATUS_COLORS[s] || 'bg-gray-600') : 'bg-[var(--border)]'
334
- }`} />
335
- {i < STATUS_STEPS.length - 1 && (
336
- <div className={`w-4 h-px transition-colors duration-700 ${
337
- isFailed ? 'bg-red-800' : i < idx ? 'bg-gray-500' : 'bg-[var(--border)]'
338
- }`} />
339
- )}
340
- </div>
341
- ))}
342
- <span className={`text-[10px] ml-1.5 font-medium ${
343
- isFailed ? 'text-red-400' : status === 'replied' ? 'text-purple-400'
344
- : status === 'running' ? 'text-green-400' : 'text-[var(--fg-muted)]'
345
- }`}>{status}</span>
346
- {(status === 'running' || status === 'delivered') && (
347
- <span className="ml-1">
348
- <span className="inline-block w-1 h-1 rounded-full bg-green-400 animate-pulse" />
349
- <span className="inline-block w-1 h-1 rounded-full bg-green-400 animate-pulse ml-0.5" style={{ animationDelay: '0.2s' }} />
350
- <span className="inline-block w-1 h-1 rounded-full bg-green-400 animate-pulse ml-0.5" style={{ animationDelay: '0.4s' }} />
391
+ <div
392
+ data-status-key={status}
393
+ data-status-known={known ? 'true' : 'false'}
394
+ data-status-label={label}
395
+ className="inline-flex flex-col gap-0.5 mt-1.5"
396
+ >
397
+ <span
398
+ title={hoverText}
399
+ aria-label={known ? info.label : `未知状态 ${status}`}
400
+ className={`inline-flex items-center gap-1.5 text-[11px] font-medium ${color}`}
401
+ >
402
+ {known ? info.icon : <IconQuestion />}
403
+ <span>{label}</span>
404
+ {(status === 'delivered' || status === 'created') && (
405
+ <span aria-hidden className="inline-flex items-center gap-0.5 ml-0.5">
406
+ <span className="inline-block w-1 h-1 rounded-full bg-current opacity-70 animate-pulse" />
407
+ <span className="inline-block w-1 h-1 rounded-full bg-current opacity-70 animate-pulse" style={{ animationDelay: '0.2s' }} />
408
+ <span className="inline-block w-1 h-1 rounded-full bg-current opacity-70 animate-pulse" style={{ animationDelay: '0.4s' }} />
409
+ </span>
410
+ )}
411
+ </span>
412
+ {failedReason !== null && (
413
+ <span
414
+ data-status-reason={failedReason}
415
+ className="text-[10px] text-red-400/80 max-w-[260px] truncate"
416
+ title={result || undefined}
417
+ >
418
+ {failedReason}
351
419
  </span>
352
420
  )}
353
421
  </div>
354
422
  );
355
423
  }
356
424
 
425
+ // Icon primitives — inline SVG, single-color (currentColor), sized to
426
+ // match the 11px label text. No external icon library — keeps the
427
+ // StatusBar self-contained and avoids a Tailwind purge/tree-shake
428
+ // dance for icons only this component uses.
429
+ function IconClock() {
430
+ return (
431
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
432
+ <circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" />
433
+ </svg>
434
+ );
435
+ }
436
+ function IconCheck() {
437
+ return (
438
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round">
439
+ <path d="M4 12l6 6L20 6" />
440
+ </svg>
441
+ );
442
+ }
443
+ function IconCheckDouble({ filled }: { filled?: boolean } = {}) {
444
+ return (
445
+ <svg width="14" height="12" viewBox="0 0 28 24" fill="none" stroke="currentColor" strokeWidth={filled ? 2.5 : 2} strokeLinecap="round" strokeLinejoin="round">
446
+ <path d="M2 12l6 6L18 6" />
447
+ <path d="M10 12l6 6L26 6" opacity={filled ? 1 : 0.7} />
448
+ </svg>
449
+ );
450
+ }
451
+ function IconWarn() {
452
+ return (
453
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
454
+ <path d="M12 2L2 20h20L12 2z" /><path d="M12 9v5" /><circle cx="12" cy="17.5" r="0.6" fill="currentColor" />
455
+ </svg>
456
+ );
457
+ }
458
+ function IconClose() {
459
+ return (
460
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
461
+ <circle cx="12" cy="12" r="10" /><path d="M8 12h8" />
462
+ </svg>
463
+ );
464
+ }
465
+ function IconSending() {
466
+ return (
467
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
468
+ <path d="M4 12l16-8-6 16-3-7-7-1z" />
469
+ </svg>
470
+ );
471
+ }
472
+ function IconQuestion() {
473
+ return (
474
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
475
+ <circle cx="12" cy="12" r="10" />
476
+ <path d="M9.5 9.5a2.5 2.5 0 015 0c0 1.5-2.5 2-2.5 3.5" />
477
+ <circle cx="12" cy="17" r="0.6" fill="currentColor" />
478
+ </svg>
479
+ );
480
+ }
481
+
357
482
  // Full markdown rendering — handles tables, headings, lists, links, code,
358
483
  // blockquotes via react-markdown + GFM. Earlier hand-rolled parser only did
359
484
  // code blocks + bold; tables / headings showed as raw `|---|` text.
@@ -586,6 +711,22 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes, active }
586
711
  window.addEventListener('keydown', onKey, true);
587
712
  return () => window.removeEventListener('keydown', onKey, true);
588
713
  }, [isActive]);
714
+ // 07-31 通信龙 batch (SPEC §12 header 🔍): ChatPane's search button
715
+ // dispatches `chat:open-search` (detail.alias). Same effect as
716
+ // Cmd/Ctrl+F, but reachable by pointer users. Filter on detail.alias
717
+ // === alias so a search click on chat A doesn't accidentally open
718
+ // search on chat B if both were somehow mounted.
719
+ useEffect(() => {
720
+ if (!isActive) return;
721
+ const onEvent = (e: Event) => {
722
+ const detail = (e as CustomEvent<{ alias?: string }>).detail;
723
+ if (detail?.alias && detail.alias !== alias) return;
724
+ setSearchOpen(true);
725
+ setTimeout(() => searchInputRef.current?.focus(), 50);
726
+ };
727
+ window.addEventListener('chat:open-search', onEvent);
728
+ return () => window.removeEventListener('chat:open-search', onEvent);
729
+ }, [isActive, alias]);
589
730
  const lightboxOpenerRef = useRef<HTMLElement | null>(null);
590
731
  const openLightbox = useCallback((url: string) => {
591
732
  lightboxOpenerRef.current = document.activeElement as HTMLElement | null;
@@ -1442,7 +1583,7 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes, active }
1442
1583
  ))}
1443
1584
  </div>
1444
1585
  <div className="flex flex-wrap items-center justify-between mt-1.5 gap-2 sm:gap-3">
1445
- <StatusBar status={m.status} />
1586
+ <StatusBar status={m.status} result={m.result} />
1446
1587
  <div className="flex items-center gap-2 shrink-0">
1447
1588
  {/* R4: WeChat send-feedback loop — spinner while the
1448
1589
  optimistic bubble is still in flight; tap-to-resend