@sleep2agi/agent-network-dashboard 0.6.3-preview.52 → 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 (198) 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 +17 -17
  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 +2 -2
  16. package/.next/server/app/_not-found.rsc +2 -2
  17. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  18. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  21. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  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 +2 -2
  25. package/.next/server/app/admin.rsc +2 -2
  26. package/.next/server/app/admin.segments/_full.segment.rsc +2 -2
  27. package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
  28. package/.next/server/app/admin.segments/_index.segment.rsc +2 -2
  29. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  30. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
  31. package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
  32. package/.next/server/app/index.html +2 -2
  33. package/.next/server/app/index.rsc +3 -3
  34. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  35. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  36. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  37. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  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 +3 -3
  42. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  43. package/.next/server/app/login.segments/_head.segment.rsc +1 -1
  44. package/.next/server/app/login.segments/_index.segment.rsc +2 -2
  45. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  46. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  47. package/.next/server/app/login.segments/login.segment.rsc +1 -1
  48. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  49. package/.next/server/app/logs.html +2 -2
  50. package/.next/server/app/logs.rsc +2 -2
  51. package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
  52. package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
  53. package/.next/server/app/logs.segments/_index.segment.rsc +2 -2
  54. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  55. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
  56. package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
  57. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  58. package/.next/server/app/messages.html +2 -2
  59. package/.next/server/app/messages.rsc +2 -2
  60. package/.next/server/app/messages.segments/_full.segment.rsc +2 -2
  61. package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
  62. package/.next/server/app/messages.segments/_index.segment.rsc +2 -2
  63. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  64. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
  65. package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
  66. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  67. package/.next/server/app/node.html +2 -2
  68. package/.next/server/app/node.rsc +3 -3
  69. package/.next/server/app/node.segments/_full.segment.rsc +3 -3
  70. package/.next/server/app/node.segments/_head.segment.rsc +1 -1
  71. package/.next/server/app/node.segments/_index.segment.rsc +2 -2
  72. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  73. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +2 -2
  74. package/.next/server/app/node.segments/node.segment.rsc +1 -1
  75. package/.next/server/app/nodes/[alias]/page_client-reference-manifest.js +1 -1
  76. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  77. package/.next/server/app/nodes.html +2 -2
  78. package/.next/server/app/nodes.rsc +3 -3
  79. package/.next/server/app/nodes.segments/_full.segment.rsc +3 -3
  80. package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
  81. package/.next/server/app/nodes.segments/_index.segment.rsc +2 -2
  82. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  83. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +2 -2
  84. package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
  85. package/.next/server/app/page_client-reference-manifest.js +1 -1
  86. package/.next/server/app/providers/page_client-reference-manifest.js +1 -1
  87. package/.next/server/app/providers.html +2 -2
  88. package/.next/server/app/providers.rsc +2 -2
  89. package/.next/server/app/providers.segments/_full.segment.rsc +2 -2
  90. package/.next/server/app/providers.segments/_head.segment.rsc +1 -1
  91. package/.next/server/app/providers.segments/_index.segment.rsc +2 -2
  92. package/.next/server/app/providers.segments/_tree.segment.rsc +2 -2
  93. package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +1 -1
  94. package/.next/server/app/providers.segments/providers.segment.rsc +1 -1
  95. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  96. package/.next/server/app/server-logs.html +2 -2
  97. package/.next/server/app/server-logs.rsc +2 -2
  98. package/.next/server/app/server-logs.segments/_full.segment.rsc +2 -2
  99. package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
  100. package/.next/server/app/server-logs.segments/_index.segment.rsc +2 -2
  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 +1 -1
  103. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
  104. package/.next/server/app/servers/page_client-reference-manifest.js +1 -1
  105. package/.next/server/app/servers.html +2 -2
  106. package/.next/server/app/servers.rsc +2 -2
  107. package/.next/server/app/servers.segments/_full.segment.rsc +2 -2
  108. package/.next/server/app/servers.segments/_head.segment.rsc +1 -1
  109. package/.next/server/app/servers.segments/_index.segment.rsc +2 -2
  110. package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
  111. package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +1 -1
  112. package/.next/server/app/servers.segments/servers.segment.rsc +1 -1
  113. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  114. package/.next/server/app/settings/networks.html +2 -2
  115. package/.next/server/app/settings/networks.rsc +2 -2
  116. package/.next/server/app/settings/networks.segments/_full.segment.rsc +2 -2
  117. package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
  118. package/.next/server/app/settings/networks.segments/_index.segment.rsc +2 -2
  119. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  120. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
  121. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
  122. package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
  123. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  125. package/.next/server/app/settings/tokens.html +2 -2
  126. package/.next/server/app/settings/tokens.rsc +2 -2
  127. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +2 -2
  128. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
  129. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +2 -2
  130. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  131. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
  132. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
  133. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
  134. package/.next/server/app/settings.html +2 -2
  135. package/.next/server/app/settings.rsc +3 -3
  136. package/.next/server/app/settings.segments/_full.segment.rsc +3 -3
  137. package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  138. package/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  139. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  140. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
  141. package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  142. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  143. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  144. package/.next/server/app/tasks.html +2 -2
  145. package/.next/server/app/tasks.rsc +3 -3
  146. package/.next/server/app/tasks.segments/_full.segment.rsc +3 -3
  147. package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
  148. package/.next/server/app/tasks.segments/_index.segment.rsc +2 -2
  149. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  150. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +2 -2
  151. package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
  152. package/.next/server/chunks/[root-of-the-server]__0t--g3k._.js +1 -1
  153. package/.next/server/chunks/[root-of-the-server]__0t--g3k._.js.map +1 -1
  154. package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js +1 -1
  155. package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js.map +1 -1
  156. package/.next/server/chunks/ssr/[root-of-the-server]__0dcez4x._.js +1 -1
  157. package/.next/server/chunks/ssr/[root-of-the-server]__0dcez4x._.js.map +1 -1
  158. package/.next/server/chunks/ssr/[root-of-the-server]__0j-def.._.js +1 -1
  159. package/.next/server/chunks/ssr/[root-of-the-server]__0j-def.._.js.map +1 -1
  160. package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js +1 -1
  161. package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js.map +1 -1
  162. package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js +1 -1
  163. package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js.map +1 -1
  164. package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js +1 -1
  165. package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js.map +1 -1
  166. package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_TasksView_tsx_0pnz7dd._.js +1 -1
  167. package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_TasksView_tsx_0pnz7dd._.js.map +1 -1
  168. package/.next/server/functions-config-manifest.json +4 -0
  169. package/.next/server/middleware-build-manifest.js +3 -3
  170. package/.next/server/pages/404.html +2 -2
  171. package/.next/server/pages/500.html +1 -1
  172. package/.next/server/server-reference-manifest.js +1 -1
  173. package/.next/server/server-reference-manifest.json +1 -1
  174. package/.next/static/chunks/{0pg4pce.q5ne_.js → 051a-ut_4_f~3.js} +3 -3
  175. package/.next/static/chunks/{051p0pw4z~6ag.js → 0hhij-plsekos.js} +1 -1
  176. package/.next/static/chunks/{0_4ljrbp.stxy.js → 0i465ex5.l573.js} +1 -1
  177. package/.next/static/chunks/{10px7ehmo7eq0.js → 0jbkmykrnr~q6.js} +1 -1
  178. package/.next/static/chunks/{0wf03_efeii.w.css → 0lc115vavown3.css} +1 -1
  179. package/.next/static/chunks/{14x094g58kg61.js → 0n_7389-uuzik.js} +1 -1
  180. package/.next/static/chunks/{00adqh_rd04uf.js → 0oai8y23-yy20.js} +3 -3
  181. package/.next/static/chunks/{0lf8e-h91s10h.js → 0p3f6dx723h6w.js} +1 -1
  182. package/.next/static/chunks/0sy8dlbnq8b.5.js +2 -0
  183. package/.next/static/{RiXBfeE2zY5tzXRmF7kBD → wbJm2a-VxMJqS77WvU7--}/_clientMiddlewareManifest.js +4 -0
  184. package/.next/trace +2 -2
  185. package/.next/trace-build +1 -1
  186. package/CHANGELOG.md +143 -0
  187. package/app/components/ChatPane.tsx +40 -23
  188. package/app/components/ChatPaneOverflowMenu.tsx +152 -0
  189. package/app/components/TaskChatPanel.tsx +16 -0
  190. package/app/tasks/TasksView.tsx +12 -2
  191. package/package.json +1 -1
  192. package/proxy.ts +50 -1
  193. package/tests/e2e-batch-list-actions.spec.ts +6 -0
  194. package/tests/e2e-chatpane-header-menu.spec.ts +259 -0
  195. package/tests/e2e-proxy-auth-boundary.spec.ts +196 -0
  196. package/.next/static/chunks/0-ot~88nectgp.js +0 -2
  197. /package/.next/static/{RiXBfeE2zY5tzXRmF7kBD → wbJm2a-VxMJqS77WvU7--}/_buildManifest.js +0 -0
  198. /package/.next/static/{RiXBfeE2zY5tzXRmF7kBD → 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
 
@@ -45,6 +46,10 @@ export interface ChatPaneProps {
45
46
 
46
47
  export function ChatPane({ alias, onClose, variant = 'column', onBack }: ChatPaneProps) {
47
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);
48
53
  // Look up session for NodeSettingsPanel — it requires the full row.
49
54
  // useSessions is SWR-cached (same key as NodesView), so no extra
50
55
  // network cost.
@@ -85,28 +90,31 @@ export function ChatPane({ alias, onClose, variant = 'column', onBack }: ChatPan
85
90
  <AliasAvatar alias={alias} size={26} />
86
91
  <span className="truncate text-sm font-semibold text-white">{alias}</span>
87
92
  </div>
88
- {/* SPEC §12: right-side icon row. ⋮ IS the settings entry
89
- (opens NodeSettingsPanel directly rather than expanding an
90
- overflow menu spec calls for both, but MVP folds them
91
- into one click since the only overflow item that has a
92
- wired backend right now IS settings; other items in the
93
- spec's 5-item overflow are not yet implemented, per
94
- INVENTORY.md). Script accepts either
95
- data-testid="chat-pane-settings" or "chat-pane-more" or
96
- aria-label containing "设置" — we set testid=settings,
97
- aria-label contains "设置", and add data-chat-pane-more so
98
- 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 ⋮. */}
99
105
  <div className="flex items-center gap-1 shrink-0">
100
106
  <button
107
+ ref={overflowAnchorRef}
101
108
  type="button"
102
- onClick={() => setSettingsOpen(true)}
103
- aria-label={t('打开会话设置', 'Open conversation settings')}
104
- title={t('会话设置', 'Conversation settings')}
105
- data-testid="chat-pane-settings"
106
- 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'}
107
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"
108
117
  >
109
- {/* ⋮ vertical dots — spec §12 "overflow menu" glyph. */}
110
118
  <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
111
119
  <circle cx="12" cy="5" r="1.5" fill="currentColor" />
112
120
  <circle cx="12" cy="12" r="1.5" fill="currentColor" />
@@ -114,7 +122,7 @@ export function ChatPane({ alias, onClose, variant = 'column', onBack }: ChatPan
114
122
  </svg>
115
123
  </button>
116
124
  <a
117
- href={`/tasks?alias=${encodeURIComponent(alias)}`}
125
+ href={`/tasks?to_name=${encodeURIComponent(alias)}`}
118
126
  aria-label={t(`查看 ${alias} 的任务`, `View ${alias} tasks`)}
119
127
  title={t('查看任务', 'View tasks')}
120
128
  data-testid="chat-pane-view-tasks"
@@ -126,11 +134,12 @@ export function ChatPane({ alias, onClose, variant = 'column', onBack }: ChatPan
126
134
  </a>
127
135
  <button
128
136
  type="button"
129
- disabled
130
- aria-label={t('会话内搜索(即将支持)', 'In-chat search (coming soon)')}
131
- 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)')}
132
140
  data-testid="chat-pane-search"
133
- 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"
134
143
  >
135
144
  <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
136
145
  <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
@@ -168,6 +177,14 @@ export function ChatPane({ alias, onClose, variant = 'column', onBack }: ChatPan
168
177
  positioning="absolute"
169
178
  />
170
179
  )}
180
+ {overflowOpen && (
181
+ <ChatPaneOverflowMenu
182
+ alias={alias}
183
+ anchorRef={overflowAnchorRef}
184
+ onClose={() => setOverflowOpen(false)}
185
+ onOpenSettings={() => setSettingsOpen(true)}
186
+ />
187
+ )}
171
188
  </aside>
172
189
  );
173
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
+ }
@@ -711,6 +711,22 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes, active }
711
711
  window.addEventListener('keydown', onKey, true);
712
712
  return () => window.removeEventListener('keydown', onKey, true);
713
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]);
714
730
  const lightboxOpenerRef = useRef<HTMLElement | null>(null);
715
731
  const openLightbox = useCallback((url: string) => {
716
732
  lightboxOpenerRef.current = document.activeElement as HTMLElement | null;
@@ -72,8 +72,16 @@ function TasksViewInner({ initialSelectedId = null }: { initialSelectedId?: stri
72
72
  const [error, setError] = useState('');
73
73
 
74
74
  const [filterStatus, setFilterStatus] = useState(searchParams.get('status') || '');
75
- const [filterFrom, setFilterFrom] = useState('');
76
- const [filterTo, setFilterTo] = useState('');
75
+ // 07-31 通信龙 dispatch (临时开放此文件): let ChatPane's "查看任务"
76
+ // button prefill the from/to filters via URL. Before this, the /tasks
77
+ // link on ChatPane header carried `?alias=` which was silently ignored
78
+ // (both filters init'd to '' regardless of URL) — user tapped, saw the
79
+ // full unfiltered list, thought "that's all the tasks for this alias".
80
+ // See feedback_verify_both_directions_with_real_data — this is a
81
+ // silent-half-broken feature that already shipped in preview.51 and
82
+ // needs both prefill directions.
83
+ const [filterFrom, setFilterFrom] = useState(searchParams.get('from_name') || '');
84
+ const [filterTo, setFilterTo] = useState(searchParams.get('to_name') || '');
77
85
 
78
86
  // #Stage-tasks: selection is seeded from /tasks/[id] URL, then plain
79
87
  // client state kept in sync with pushState (shallow — no route remount,
@@ -226,6 +234,7 @@ function TasksViewInner({ initialSelectedId = null }: { initialSelectedId?: stri
226
234
  value={filterFrom}
227
235
  onChange={(e) => setFilterFrom(e.target.value)}
228
236
  placeholder="any"
237
+ data-testid="tasks-filter-from"
229
238
  className="w-16 bg-transparent text-xs text-white placeholder-gray-700 focus:outline-none"
230
239
  />
231
240
  </div>
@@ -236,6 +245,7 @@ function TasksViewInner({ initialSelectedId = null }: { initialSelectedId?: stri
236
245
  value={filterTo}
237
246
  onChange={(e) => setFilterTo(e.target.value)}
238
247
  placeholder="any"
248
+ data-testid="tasks-filter-to"
239
249
  className="w-16 bg-transparent text-xs text-white placeholder-gray-700 focus:outline-none"
240
250
  />
241
251
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.6.3-preview.52",
3
+ "version": "0.6.3-preview.53",
4
4
  "description": "Agent Network Dashboard — Web UI for managing AI Agent networks",
5
5
  "main": "apps/desktop/electron/main.cjs",
6
6
  "scripts": {
package/proxy.ts CHANGED
@@ -18,6 +18,55 @@ export function proxy(request: NextRequest) {
18
18
  return NextResponse.next();
19
19
  }
20
20
 
21
+ // ROUTE GATE — read carefully before editing.
22
+ //
23
+ // This is NOT authentication. It's a cookie-presence redirect:
24
+ // `request.cookies.has(SESSION_COOKIE)` at line 8 checks only that
25
+ // the cookie EXISTS, not that its value is valid. Any garbage
26
+ // (e.g. `anet_dashboard_session=xyz`) passes this gate and lands
27
+ // on the page. Real authentication lives in the /api/hub layer,
28
+ // which validates the token content when the page tries to fetch
29
+ // data.
30
+ //
31
+ // So this matcher's job is narrower than it looks: it decides
32
+ // which paths bounce no-cookie visitors to /login for a nicer UX.
33
+ // It does NOT prevent access to page bytes by a visitor who
34
+ // forges any cookie value ("has a cookie" is not "is authenticated").
35
+ // Do not write code that trusts this gate for authorization —
36
+ // always call the hub for data and check the response.
37
+ //
38
+ // 07-31 通信龙 catch: `/nodes/:path*` was missing. Every other
39
+ // page route with a dynamic child had it (`/tasks/:path*`,
40
+ // `/settings/:path*`), so this was a slip, not a design choice.
41
+ // Verified in prod: a no-session-cookie GET to `/nodes/<alias>`
42
+ // was REACHABLE — returned 200 with the full 36KB shell; every
43
+ // other matcher entry returned 307 → /login as intended.
44
+ //
45
+ // Blast radius today: /nodes/[alias] page happens to not run a
46
+ // server-side data fetch — 通信龙 hit it with a nonexistent alias
47
+ // `ZZZ-not-a-real-agent-9999` and got 200 + 36108 bytes back (only
48
+ // 16 bytes bigger = the alias-length difference echoed in the
49
+ // shell). Since the page doesn't query, no DB row leaks. This is
50
+ // STRUCTURAL, not sensitive: if the page ever gains a server-side
51
+ // data fetch, the leak begins immediately. Fix now, don't wait.
52
+ //
53
+ // If you add a new page route with a dynamic child, add its
54
+ // `:path*` variant here AND add its bare path to the test list
55
+ // in `tests/e2e-proxy-auth-boundary.spec.ts` — the test
56
+ // enumerates matcher entries and asserts each redirects the
57
+ // no-cookie case, so it catches this class of miss for the next
58
+ // time. (The test's filename says "auth-boundary" for continuity
59
+ // with #69; the actual behavior it tests is cookie-presence,
60
+ // same as this comment describes.)
21
61
  export const config = {
22
- matcher: ['/', '/node', '/tasks', '/tasks/:path*', '/nodes', '/messages', '/logs', '/settings', '/settings/:path*', '/admin'],
62
+ matcher: [
63
+ '/',
64
+ '/node',
65
+ '/tasks', '/tasks/:path*',
66
+ '/nodes', '/nodes/:path*',
67
+ '/messages',
68
+ '/logs',
69
+ '/settings', '/settings/:path*',
70
+ '/admin',
71
+ ],
23
72
  };
@@ -110,6 +110,12 @@ test.describe('Batch — list actions (07-31)', () => {
110
110
  await expect(chatInput).toHaveValue(DRAFT);
111
111
 
112
112
  // Open settings drawer → close → text MUST still be there.
113
+ // 07-31 SPEC §12 batch: ⋮ now opens overflow menu (not settings
114
+ // direct). "会话设置" is one of four menu items and still carries
115
+ // data-testid="chat-pane-settings" so this assertion's target is
116
+ // preserved — but the click flow is now two steps.
117
+ await page.locator('[data-testid="chat-pane-more"]').click();
118
+ await expect(page.locator('[data-testid="chat-pane-overflow-menu"]')).toBeVisible({ timeout: 3000 });
113
119
  await page.locator('[data-testid="chat-pane-settings"]').click();
114
120
  await expect(page.locator('[role="dialog"][aria-label*="节点设置"]').first()).toBeVisible({ timeout: 5000 });
115
121
  // Close via the backdrop (absolute-positioned inside chat pane).