@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.
- package/.next/BUILD_COMMIT +1 -1
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/diagnostics/route-bundle-stats.json +17 -17
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +2 -2
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
- package/.next/server/app/admin.html +2 -2
- package/.next/server/app/admin.rsc +2 -2
- package/.next/server/app/admin.segments/_full.segment.rsc +2 -2
- package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
- package/.next/server/app/admin.segments/_index.segment.rsc +2 -2
- package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +3 -3
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +2 -2
- package/.next/server/app/login.rsc +3 -3
- package/.next/server/app/login.segments/_full.segment.rsc +3 -3
- package/.next/server/app/login.segments/_head.segment.rsc +1 -1
- package/.next/server/app/login.segments/_index.segment.rsc +2 -2
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/login.segments/login.segment.rsc +1 -1
- package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/logs.html +2 -2
- package/.next/server/app/logs.rsc +2 -2
- package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
- package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
- package/.next/server/app/logs.segments/_index.segment.rsc +2 -2
- package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
- package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
- package/.next/server/app/messages.html +2 -2
- package/.next/server/app/messages.rsc +2 -2
- package/.next/server/app/messages.segments/_full.segment.rsc +2 -2
- package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
- package/.next/server/app/messages.segments/_index.segment.rsc +2 -2
- package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
- package/.next/server/app/node/page_client-reference-manifest.js +1 -1
- package/.next/server/app/node.html +2 -2
- package/.next/server/app/node.rsc +3 -3
- package/.next/server/app/node.segments/_full.segment.rsc +3 -3
- package/.next/server/app/node.segments/_head.segment.rsc +1 -1
- package/.next/server/app/node.segments/_index.segment.rsc +2 -2
- package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/node.segments/node.segment.rsc +1 -1
- package/.next/server/app/nodes/[alias]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes.html +2 -2
- package/.next/server/app/nodes.rsc +3 -3
- package/.next/server/app/nodes.segments/_full.segment.rsc +3 -3
- package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
- package/.next/server/app/nodes.segments/_index.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/providers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/providers.html +2 -2
- package/.next/server/app/providers.rsc +2 -2
- package/.next/server/app/providers.segments/_full.segment.rsc +2 -2
- package/.next/server/app/providers.segments/_head.segment.rsc +1 -1
- package/.next/server/app/providers.segments/_index.segment.rsc +2 -2
- package/.next/server/app/providers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/providers.segments/providers.segment.rsc +1 -1
- package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs.html +2 -2
- package/.next/server/app/server-logs.rsc +2 -2
- package/.next/server/app/server-logs.segments/_full.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
- package/.next/server/app/server-logs.segments/_index.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
- package/.next/server/app/servers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/servers.html +2 -2
- package/.next/server/app/servers.rsc +2 -2
- package/.next/server/app/servers.segments/_full.segment.rsc +2 -2
- package/.next/server/app/servers.segments/_head.segment.rsc +1 -1
- package/.next/server/app/servers.segments/_index.segment.rsc +2 -2
- package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/servers.segments/servers.segment.rsc +1 -1
- package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/networks.html +2 -2
- package/.next/server/app/settings/networks.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens.html +2 -2
- package/.next/server/app/settings/tokens.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
- package/.next/server/app/settings.html +2 -2
- package/.next/server/app/settings.rsc +3 -3
- package/.next/server/app/settings.segments/_full.segment.rsc +3 -3
- package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
- package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks.html +2 -2
- package/.next/server/app/tasks.rsc +3 -3
- package/.next/server/app/tasks.segments/_full.segment.rsc +3 -3
- package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
- package/.next/server/app/tasks.segments/_index.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
- package/.next/server/chunks/[root-of-the-server]__0t--g3k._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0t--g3k._.js.map +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js.map +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0dcez4x._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0dcez4x._.js.map +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0j-def.._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0j-def.._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_TasksView_tsx_0pnz7dd._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_TasksView_tsx_0pnz7dd._.js.map +1 -1
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/{0pg4pce.q5ne_.js → 051a-ut_4_f~3.js} +3 -3
- package/.next/static/chunks/{051p0pw4z~6ag.js → 0hhij-plsekos.js} +1 -1
- package/.next/static/chunks/{0_4ljrbp.stxy.js → 0i465ex5.l573.js} +1 -1
- package/.next/static/chunks/{10px7ehmo7eq0.js → 0jbkmykrnr~q6.js} +1 -1
- package/.next/static/chunks/{0wf03_efeii.w.css → 0lc115vavown3.css} +1 -1
- package/.next/static/chunks/{14x094g58kg61.js → 0n_7389-uuzik.js} +1 -1
- package/.next/static/chunks/{00adqh_rd04uf.js → 0oai8y23-yy20.js} +3 -3
- package/.next/static/chunks/{0lf8e-h91s10h.js → 0p3f6dx723h6w.js} +1 -1
- package/.next/static/chunks/0sy8dlbnq8b.5.js +2 -0
- package/.next/static/{RiXBfeE2zY5tzXRmF7kBD → wbJm2a-VxMJqS77WvU7--}/_clientMiddlewareManifest.js +4 -0
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/CHANGELOG.md +143 -0
- package/app/components/ChatPane.tsx +40 -23
- package/app/components/ChatPaneOverflowMenu.tsx +152 -0
- package/app/components/TaskChatPanel.tsx +16 -0
- package/app/tasks/TasksView.tsx +12 -2
- package/package.json +1 -1
- package/proxy.ts +50 -1
- package/tests/e2e-batch-list-actions.spec.ts +6 -0
- package/tests/e2e-chatpane-header-menu.spec.ts +259 -0
- package/tests/e2e-proxy-auth-boundary.spec.ts +196 -0
- package/.next/static/chunks/0-ot~88nectgp.js +0 -2
- /package/.next/static/{RiXBfeE2zY5tzXRmF7kBD → wbJm2a-VxMJqS77WvU7--}/_buildManifest.js +0 -0
- /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:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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={() =>
|
|
103
|
-
aria-label={t('
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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?
|
|
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
|
-
|
|
130
|
-
aria-label={t('
|
|
131
|
-
title={t('
|
|
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
|
-
|
|
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;
|
package/app/tasks/TasksView.tsx
CHANGED
|
@@ -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
|
-
|
|
76
|
-
|
|
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
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: [
|
|
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).
|