@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.
- 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 +48 -48
- 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.js.nft.json +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 +13 -13
- package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/server/app/_not-found.segments/_index.segment.rsc +8 -8
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin/page.js.nft.json +1 -1
- 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 +15 -15
- package/.next/server/app/admin.segments/_full.segment.rsc +15 -15
- package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
- package/.next/server/app/admin.segments/_index.segment.rsc +8 -8
- package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +15 -15
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/server/app/index.segments/_index.segment.rsc +8 -8
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login/page.js.nft.json +1 -1
- 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 +15 -15
- package/.next/server/app/login.segments/_full.segment.rsc +15 -15
- package/.next/server/app/login.segments/_head.segment.rsc +4 -4
- package/.next/server/app/login.segments/_index.segment.rsc +8 -8
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/login.segments/login.segment.rsc +3 -3
- package/.next/server/app/logs/page.js.nft.json +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 +15 -15
- package/.next/server/app/logs.segments/_full.segment.rsc +15 -15
- package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/logs.segments/_index.segment.rsc +8 -8
- package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
- package/.next/server/app/messages/page.js.nft.json +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 +15 -15
- package/.next/server/app/messages.segments/_full.segment.rsc +15 -15
- package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
- package/.next/server/app/messages.segments/_index.segment.rsc +8 -8
- package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
- package/.next/server/app/node/page.js.nft.json +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 +15 -15
- package/.next/server/app/node.segments/_full.segment.rsc +15 -15
- package/.next/server/app/node.segments/_head.segment.rsc +4 -4
- package/.next/server/app/node.segments/_index.segment.rsc +8 -8
- package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/node.segments/node.segment.rsc +3 -3
- package/.next/server/app/nodes/[alias]/page.js.nft.json +1 -1
- package/.next/server/app/nodes/[alias]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes/page.js.nft.json +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 +15 -15
- package/.next/server/app/nodes.segments/_full.segment.rsc +15 -15
- package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/_index.segment.rsc +8 -8
- package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/providers/page.js.nft.json +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 +15 -15
- package/.next/server/app/providers.segments/_full.segment.rsc +15 -15
- package/.next/server/app/providers.segments/_head.segment.rsc +4 -4
- package/.next/server/app/providers.segments/_index.segment.rsc +8 -8
- package/.next/server/app/providers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/providers.segments/providers.segment.rsc +3 -3
- package/.next/server/app/server-logs/page.js.nft.json +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 +15 -15
- package/.next/server/app/server-logs.segments/_full.segment.rsc +15 -15
- package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/_index.segment.rsc +8 -8
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
- package/.next/server/app/servers/page.js.nft.json +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 +15 -15
- package/.next/server/app/servers.segments/_full.segment.rsc +15 -15
- package/.next/server/app/servers.segments/_head.segment.rsc +4 -4
- package/.next/server/app/servers.segments/_index.segment.rsc +8 -8
- package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/servers.segments/servers.segment.rsc +3 -3
- package/.next/server/app/settings/networks/page.js.nft.json +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 +15 -15
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
- package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
- package/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens/page.js.nft.json +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 +15 -15
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
- package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
- package/.next/server/app/settings.html +2 -2
- package/.next/server/app/settings.rsc +15 -15
- package/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
- package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks/page.js.nft.json +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 +15 -15
- package/.next/server/app/tasks.segments/_full.segment.rsc +15 -15
- package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/_index.segment.rsc +8 -8
- package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
- 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 +11 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0dcez4x._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__0jgqpfv._.js → [root-of-the-server]__0gyqilt._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__0gyqilt._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0j-def.._.js +11 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0j-def.._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__11wln8.._.js → [root-of-the-server]__0oavw6j._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__0oavw6j._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0rct4.k._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__02o~-py._.js.map → [root-of-the-server]__0rct4.k._.js.map} +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_05u4_h~._.js +2 -2
- package/.next/server/chunks/ssr/dash-stageb-wt_app_05u4_h~._.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 +2 -2
- 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/051a-ut_4_f~3.js +9 -0
- package/.next/static/chunks/{0advoo2tzdcyi.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/0lc115vavown3.css +1 -0
- package/.next/static/chunks/{0z_4d13crd_af.js → 0n_7389-uuzik.js} +1 -1
- package/.next/static/chunks/0oai8y23-yy20.js +9 -0
- package/.next/static/chunks/{0dkk_l872ocgm.js → 0ohx4jpm9~61..js} +1 -1
- package/.next/static/chunks/{0uk67ty_0h_36.js → 0p3f6dx723h6w.js} +1 -1
- package/.next/static/chunks/0sy8dlbnq8b.5.js +2 -0
- package/.next/static/chunks/{0fz7zk.ek.154.js → 0u5y.gmi~jkw6.js} +1 -1
- package/.next/static/chunks/0ws0p1n62l~ub.js +1 -0
- package/.next/static/chunks/{06q7-vhy~1-y3.js → 0~rx9_~1wupcp.js} +2 -2
- package/.next/static/{JL_QjNKv1KhsCF68ZK28x → 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 +66 -25
- package/app/components/ChatPaneOverflowMenu.tsx +152 -0
- package/app/components/NodeList.tsx +14 -4
- package/app/components/TaskChatPanel.tsx +172 -31
- package/app/messages/page.tsx +22 -22
- package/app/nodes/NodesView.tsx +52 -11
- package/app/tasks/TasksView.tsx +12 -2
- package/package.json +1 -1
- package/proxy.ts +50 -1
- package/tests/e2e-9-nodes-team-tag-filter.spec.ts +14 -7
- package/tests/e2e-batch-list-actions.spec.ts +6 -0
- package/tests/e2e-chatpane-header-menu.spec.ts +259 -0
- package/tests/e2e-messages-spec-visual-align.spec.ts +244 -0
- package/tests/e2e-pin-mute-attr-always-present.spec.ts +171 -0
- package/tests/e2e-proxy-auth-boundary.spec.ts +196 -0
- package/tests/e2e-stage-a-node-list.spec.ts +14 -7
- package/tests/e2e-stage-b-chat-pane.spec.ts +74 -10
- package/tests/e2e-status-labels.spec.ts +218 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__02o~-py._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0jgqpfv._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js +0 -9
- package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__11wln8.._.js.map +0 -1
- package/.next/static/chunks/05~lgfzsv.p~p.js +0 -2
- package/.next/static/chunks/092zqb~4un32l.css +0 -1
- package/.next/static/chunks/11zz15q37~bz5.js +0 -7
- package/.next/static/chunks/17qkc714xy04..js +0 -1
- /package/.next/static/{JL_QjNKv1KhsCF68ZK28x → wbJm2a-VxMJqS77WvU7--}/_buildManifest.js +0 -0
- /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
|
-
|
|
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:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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={() =>
|
|
79
|
-
aria-label={t('
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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?
|
|
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
|
-
|
|
106
|
-
aria-label={t('
|
|
107
|
-
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)')}
|
|
108
140
|
data-testid="chat-pane-search"
|
|
109
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
326
|
-
const
|
|
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
|
|
329
|
-
{
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
</
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|