@sleep2agi/agent-network-dashboard 0.6.3-preview.50 → 0.6.3-preview.52
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 +50 -50
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- 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/ssr/{0ld5_next_dist_00v047f._.js → 0ld5_next_dist_04cce2g._.js} +2 -2
- package/.next/server/chunks/ssr/0ld5_next_dist_04cce2g._.js.map +1 -0
- 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]__0rct4.k._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__00n84ca._.js → [root-of-the-server]__0x9nh~a._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__0x9nh~a._.js.map +1 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_0q55dv1._.js +0 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_0q55dv1._.js.map +1 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_13184u1._.js +0 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_13184u1._.js.map +1 -0
- 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_0hzfl6x._.js → dash-stageb-wt_app_page_tsx_0iyrtvj._.js} +3 -3
- package/.next/server/chunks/ssr/dash-stageb-wt_app_page_tsx_0iyrtvj._.js.map +1 -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/0-ot~88nectgp.js +2 -0
- package/.next/static/chunks/00adqh_rd04uf.js +9 -0
- package/.next/static/chunks/051p0pw4z~6ag.js +1 -0
- package/.next/static/chunks/0aaakzcw7tey0.js +0 -0
- package/.next/static/chunks/{0hm52b0y~k0~z.js → 0lf8e-h91s10h.js} +1 -1
- package/.next/static/chunks/0njt8wek.9aq6.js +1 -0
- package/.next/static/chunks/0ohx4jpm9~61..js +1 -0
- package/.next/static/chunks/0pg4pce.q5ne_.js +9 -0
- package/.next/static/chunks/0u5y.gmi~jkw6.js +1 -0
- package/.next/static/chunks/0wf03_efeii.w.css +1 -0
- package/.next/static/chunks/0ws0p1n62l~ub.js +1 -0
- package/.next/static/chunks/0zljk8~2jqw93.js +4 -0
- package/.next/static/chunks/{06q7-vhy~1-y3.js → 0~rx9_~1wupcp.js} +2 -2
- package/.next/static/chunks/{0ep78sq90x_yc.js → 14x094g58kg61.js} +1 -1
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/components/ChatPane.tsx +133 -17
- package/app/components/NodeList.tsx +208 -46
- package/app/components/NodeListContextMenu.tsx +187 -0
- package/app/components/NodeSettingsPanel.tsx +29 -3
- package/app/components/TaskChatPanel.tsx +156 -31
- package/app/messages/page.tsx +22 -22
- package/app/nodes/NodesView.tsx +110 -11
- package/package.json +1 -1
- package/tests/e2e-9-nodes-team-tag-filter.spec.ts +17 -7
- package/tests/e2e-batch-list-actions.spec.ts +234 -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-stage-a-node-list.spec.ts +18 -7
- package/tests/e2e-stage-b-chat-pane.spec.ts +177 -10
- package/tests/e2e-status-labels.spec.ts +218 -0
- package/.next/server/chunks/ssr/0ld5_next_dist_00v047f._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__00n84ca._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0idij1k._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0idij1k._.js.map +0 -1
- 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/server/chunks/ssr/dash-stageb-wt_0hzfl6x._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_06.7qt9._.js +0 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_app_06.7qt9._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0y~m8-6._.js +0 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0y~m8-6._.js.map +0 -1
- package/.next/static/chunks/02.5_nifak.uu.js +0 -1
- package/.next/static/chunks/03hzj129k~odb.js +0 -1
- package/.next/static/chunks/0cey4i.y0a8~j.js +0 -1
- package/.next/static/chunks/0mhaf6lrw5nn..css +0 -1
- package/.next/static/chunks/0s293geid.j7e.js +0 -4
- package/.next/static/chunks/0zj_navpyhltx.js +0 -2
- package/.next/static/chunks/11zz15q37~bz5.js +0 -7
- package/.next/static/chunks/14y4x3de1lbug.js +0 -1
- package/.next/static/chunks/15ka68xi.w-i-.js +0 -1
- package/.next/static/chunks/17qkc714xy04..js +0 -1
- /package/.next/static/{vc_aJHtYwefsVcl2_f14Z → RiXBfeE2zY5tzXRmF7kBD}/_buildManifest.js +0 -0
- /package/.next/static/{vc_aJHtYwefsVcl2_f14Z → RiXBfeE2zY5tzXRmF7kBD}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{vc_aJHtYwefsVcl2_f14Z → RiXBfeE2zY5tzXRmF7kBD}/_ssgManifest.js +0 -0
|
@@ -311,7 +311,20 @@ function NumberField({ label, value, onChange, placeholder }: { label: string; v
|
|
|
311
311
|
);
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
export function NodeSettingsPanel({ session: s, onClose
|
|
314
|
+
export function NodeSettingsPanel({ session: s, onClose, positioning = 'fixed' }: {
|
|
315
|
+
session: Session;
|
|
316
|
+
onClose: () => void;
|
|
317
|
+
/** 07-31 通信龙 判定: 'fixed' (default) is the legacy fullscreen
|
|
318
|
+
* drawer used by AgentCard (covers whole viewport). 'absolute'
|
|
319
|
+
* scopes the drawer to its nearest positioned ancestor + shrinks
|
|
320
|
+
* to 360px, used by ChatPane so it only covers the chat column
|
|
321
|
+
* (SPEC §12: settings should not steal from the reclaimed 1104px
|
|
322
|
+
* chat width; fourth-column would push chat back to 664px).
|
|
323
|
+
* Absolute mode also drops the black backdrop — clicking outside
|
|
324
|
+
* the drawer inside the chat pane still closes, but the rest of
|
|
325
|
+
* the app stays interactive. */
|
|
326
|
+
positioning?: 'fixed' | 'absolute';
|
|
327
|
+
}) {
|
|
315
328
|
const runtime = s.runtime || '—';
|
|
316
329
|
const initialModel = s.model || s.agent || '';
|
|
317
330
|
// imageCapable: read-only, auto from model (MiniMax-M3 / Claude = yes).
|
|
@@ -640,14 +653,27 @@ export function NodeSettingsPanel({ session: s, onClose }: { session: Session; o
|
|
|
640
653
|
}
|
|
641
654
|
}
|
|
642
655
|
|
|
656
|
+
const scoped = positioning === 'absolute';
|
|
643
657
|
return (
|
|
644
658
|
<>
|
|
645
|
-
|
|
659
|
+
{/* Backdrop — 'fixed' mode covers the whole viewport;
|
|
660
|
+
'absolute' mode scopes to the positioned ancestor
|
|
661
|
+
(ChatPane's <aside>) so it only dims the chat column, not
|
|
662
|
+
the sidebar or rail. */}
|
|
663
|
+
<div
|
|
664
|
+
className={scoped
|
|
665
|
+
? 'absolute inset-0 bg-black/40 z-40'
|
|
666
|
+
: 'fixed inset-0 bg-black/40 z-40'}
|
|
667
|
+
onClick={onClose}
|
|
668
|
+
aria-hidden
|
|
669
|
+
/>
|
|
646
670
|
|
|
647
671
|
<div
|
|
648
672
|
role="dialog"
|
|
649
673
|
aria-label={`${s.alias} 节点设置`}
|
|
650
|
-
className=
|
|
674
|
+
className={scoped
|
|
675
|
+
? 'absolute top-0 right-0 h-full w-full sm:w-[360px] max-w-[360px] bg-[#0f0f11] border-l border-[#26262b] z-50 flex flex-col animate-slide-in'
|
|
676
|
+
: 'fixed top-0 right-0 h-[100dvh] w-full sm:w-[440px] bg-[#0f0f11] border-l border-[#26262b] z-50 flex flex-col shadow-2xl shadow-black/60 animate-slide-in'}
|
|
651
677
|
>
|
|
652
678
|
{/* Header */}
|
|
653
679
|
<div className="flex items-center gap-2.5 px-4 py-3 border-b border-[#26262b]">
|
|
@@ -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.
|
|
@@ -1442,7 +1567,7 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes, active }
|
|
|
1442
1567
|
))}
|
|
1443
1568
|
</div>
|
|
1444
1569
|
<div className="flex flex-wrap items-center justify-between mt-1.5 gap-2 sm:gap-3">
|
|
1445
|
-
<StatusBar status={m.status} />
|
|
1570
|
+
<StatusBar status={m.status} result={m.result} />
|
|
1446
1571
|
<div className="flex items-center gap-2 shrink-0">
|
|
1447
1572
|
{/* R4: WeChat send-feedback loop — spinner while the
|
|
1448
1573
|
optimistic bubble is still in flight; tap-to-resend
|
package/app/messages/page.tsx
CHANGED
|
@@ -146,7 +146,7 @@ export default function MessagesPage() {
|
|
|
146
146
|
}, [filtered]);
|
|
147
147
|
|
|
148
148
|
return (
|
|
149
|
-
<div className="min-h-screen bg-[#
|
|
149
|
+
<div data-testid="messages-shell" className="min-h-screen bg-[#141826] text-gray-100 p-4 sm:p-6">
|
|
150
150
|
{/* #209 R31 (mobile vertical rhythm — same pattern as R28/R30):
|
|
151
151
|
header row + quick-from chips row both mb-6 → mb-4 sm:mb-6.
|
|
152
152
|
#209 R33: header restructured to mirror /nodes R32 — title
|
|
@@ -253,7 +253,7 @@ export default function MessagesPage() {
|
|
|
253
253
|
<button
|
|
254
254
|
type="button"
|
|
255
255
|
onClick={() => setSearch('')}
|
|
256
|
-
className="shrink-0 rounded-full border border-gray-700 px-3 py-1 text-
|
|
256
|
+
className="shrink-0 rounded-full border border-gray-700 px-3 py-1 text-[11px] text-[#64748B] hover:text-gray-200"
|
|
257
257
|
>
|
|
258
258
|
Clear
|
|
259
259
|
</button>
|
|
@@ -336,9 +336,9 @@ export default function MessagesPage() {
|
|
|
336
336
|
Halve it on mobile. */}
|
|
337
337
|
{gapExceeded && (
|
|
338
338
|
<div className="my-2 sm:my-4 flex items-center gap-3">
|
|
339
|
-
<div className="h-px flex-1 bg-[
|
|
340
|
-
<div className="text-[
|
|
341
|
-
<div className="h-px flex-1 bg-[
|
|
339
|
+
<div className="h-px flex-1 bg-white/[0.06]" />
|
|
340
|
+
<div className="text-[13px] font-medium text-[#94A3B8]">{formatDividerLabel(message.created_at)}</div>
|
|
341
|
+
<div className="h-px flex-1 bg-white/[0.06]" />
|
|
342
342
|
</div>
|
|
343
343
|
)}
|
|
344
344
|
|
|
@@ -348,21 +348,21 @@ export default function MessagesPage() {
|
|
|
348
348
|
390px. Desktop unchanged from sm: up. */}
|
|
349
349
|
{variant === 'broadcast' ? (
|
|
350
350
|
<div className={samePrev ? 'mt-0.5 sm:mt-1' : 'mt-2 sm:mt-3'}>
|
|
351
|
-
<div className="rounded-2xl border border-
|
|
351
|
+
<div className="rounded-2xl border border-white/[0.06] border-l-2 border-l-[#7DD3FC] bg-[#1E293B] px-3 py-2 sm:px-4 sm:py-3 shadow-sm w-full">
|
|
352
352
|
<div className="mb-1 sm:mb-2 flex flex-wrap items-center gap-2">
|
|
353
353
|
<span className={`text-xs px-2 py-0.5 rounded-md border ${TYPE_COLORS[message.type || ''] || 'bg-gray-500/10 text-gray-400 border-gray-500/20'}`}>
|
|
354
354
|
{message.type || 'unknown'}
|
|
355
355
|
</span>
|
|
356
|
-
<span className="text-sm font-medium text-
|
|
357
|
-
{message.to_alias && <span className="text-xs text-
|
|
358
|
-
{message.priority === 'high' && <span className="text-xs text-
|
|
359
|
-
<span className="ml-auto text-
|
|
356
|
+
<span className="text-sm font-medium text-[#E2E8F0]">{fromAlias}</span>
|
|
357
|
+
{message.to_alias && <span className="text-xs text-[#CBD5E1]">to {message.to_alias}</span>}
|
|
358
|
+
{message.priority === 'high' && <span className="text-xs text-[#EF4444]">HIGH</span>}
|
|
359
|
+
<span className="ml-auto text-[11px] text-[#64748B]">{message.created_at ? timeAgo(message.created_at) : '--'}</span>
|
|
360
360
|
</div>
|
|
361
361
|
{/* Round 79: break-words + overflow-wrap:anywhere so
|
|
362
362
|
long unbroken runs (URLs, ASCII rules like
|
|
363
363
|
`═══════════════`) wrap instead of pushing the
|
|
364
364
|
chat bubble past the mobile viewport. */}
|
|
365
|
-
<div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm leading-snug sm:leading-relaxed text-
|
|
365
|
+
<div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm leading-snug sm:leading-relaxed text-[#E2E8F0]">
|
|
366
366
|
{renderHighlighted(message.content, search)}
|
|
367
367
|
</div>
|
|
368
368
|
{/* #492: broadcast may carry attachments too. */}
|
|
@@ -377,23 +377,23 @@ export default function MessagesPage() {
|
|
|
377
377
|
<div className="w-8 shrink-0 pt-1">
|
|
378
378
|
{!samePrev && <AliasAvatar alias={fromAlias} size={32} />}
|
|
379
379
|
</div>
|
|
380
|
-
<div className={`min-w-0 max-w-
|
|
380
|
+
<div className={`min-w-0 max-w-[min(640px,65%)] rounded-2xl border border-white/[0.06] px-3 py-2 sm:px-4 sm:py-3 shadow-sm ${
|
|
381
381
|
variant === 'outgoing'
|
|
382
|
-
? '
|
|
383
|
-
: '
|
|
382
|
+
? 'bg-[#1E3A5F]'
|
|
383
|
+
: 'bg-[#1E293B]'
|
|
384
384
|
}`}>
|
|
385
385
|
{!samePrev && (
|
|
386
386
|
<div className="mb-1 sm:mb-2 flex flex-wrap items-center gap-2">
|
|
387
387
|
<span className={`text-xs px-2 py-0.5 rounded-md border ${TYPE_COLORS[message.type || ''] || 'bg-gray-500/10 text-gray-400 border-gray-500/20'}`}>
|
|
388
388
|
{message.type || 'unknown'}
|
|
389
389
|
</span>
|
|
390
|
-
<span className={`text-sm font-medium ${variant === 'outgoing' ? 'text-
|
|
390
|
+
<span className={`text-sm font-medium ${variant === 'outgoing' ? 'text-[#F1F5F9]' : 'text-[#E2E8F0]'}`}>
|
|
391
391
|
{fromAlias}
|
|
392
392
|
</span>
|
|
393
|
-
<span className="text-xs text-
|
|
394
|
-
<span className="text-sm text-
|
|
395
|
-
{message.priority === 'high' && <span className="text-xs text-
|
|
396
|
-
<span className="ml-auto text-
|
|
393
|
+
<span className="text-xs text-[#64748B]">→</span>
|
|
394
|
+
<span className="text-sm text-[#CBD5E1]">{message.to_alias || '--'}</span>
|
|
395
|
+
{message.priority === 'high' && <span className="text-xs text-[#EF4444]">HIGH</span>}
|
|
396
|
+
<span className="ml-auto text-[11px] text-[#64748B]">{message.created_at ? timeAgo(message.created_at) : '--'}</span>
|
|
397
397
|
</div>
|
|
398
398
|
)}
|
|
399
399
|
|
|
@@ -401,7 +401,7 @@ export default function MessagesPage() {
|
|
|
401
401
|
long unbroken runs (URLs, ASCII rules like
|
|
402
402
|
`═══════════════`) wrap instead of pushing the
|
|
403
403
|
chat bubble past the mobile viewport. */}
|
|
404
|
-
<div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm leading-snug sm:leading-relaxed text-
|
|
404
|
+
<div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm leading-snug sm:leading-relaxed text-[#E2E8F0]">
|
|
405
405
|
{renderHighlighted(message.content, search)}
|
|
406
406
|
</div>
|
|
407
407
|
|
|
@@ -417,13 +417,13 @@ export default function MessagesPage() {
|
|
|
417
417
|
|
|
418
418
|
{/* When in a streak, surface the timestamp inline-right so users still know cadence */}
|
|
419
419
|
{samePrev && (
|
|
420
|
-
<div className="mt-1 text-[
|
|
420
|
+
<div className="mt-1 text-[11px] text-[#64748B] text-right">
|
|
421
421
|
{message.created_at ? timeAgo(message.created_at) : ''}
|
|
422
422
|
</div>
|
|
423
423
|
)}
|
|
424
424
|
|
|
425
425
|
{debug && (
|
|
426
|
-
<div className="mt-3 border-t border-white/10 pt-3 text-
|
|
426
|
+
<div className="mt-3 border-t border-white/10 pt-3 text-[11px] text-[#64748B] space-y-1">
|
|
427
427
|
<div>ID: {message.id}</div>
|
|
428
428
|
{message.task_id && <div>Task ID: {message.task_id}</div>}
|
|
429
429
|
<div>Created: {message.created_at}</div>
|
package/app/nodes/NodesView.tsx
CHANGED
|
@@ -4,7 +4,6 @@ import { useState, useRef, useEffect, useSyncExternalStore } from 'react';
|
|
|
4
4
|
import { timeAgo } from '../components/utils';
|
|
5
5
|
import { useSessions, useHealth, useNodeLifecycle } from '../lib/hooks';
|
|
6
6
|
import { useSWRConfig } from 'swr';
|
|
7
|
-
import { TaskChatPanel } from '../components/TaskChatPanel';
|
|
8
7
|
import { CreateNodeWizard } from '../components/CreateNodeWizard';
|
|
9
8
|
import { NodeLifecycleMenu } from '../components/NodeLifecycleMenu';
|
|
10
9
|
import { EmptyState, NodesEmptyState } from '../components/EmptyState';
|
|
@@ -118,10 +117,15 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
|
|
|
118
117
|
const [chatAlias, setChatAlias] = useState<string | null>(initialAlias);
|
|
119
118
|
const isDesktop = useIsDesktop();
|
|
120
119
|
const selectNode = (alias: string) => {
|
|
120
|
+
try {
|
|
121
|
+
const el = document.querySelector('[data-testid="node-list-scroll"]');
|
|
122
|
+
if (el) savedListScrollRef.current = el.scrollTop;
|
|
123
|
+
} catch {}
|
|
121
124
|
setChatAlias(alias);
|
|
122
125
|
try { window.history.pushState({}, '', `/nodes/${encodeURIComponent(alias)}`); } catch {}
|
|
123
126
|
};
|
|
124
127
|
const closeChat = () => {
|
|
128
|
+
userClosedRef.current = true; // ③ explicit close wins over auto-select
|
|
125
129
|
setChatAlias(null);
|
|
126
130
|
try { window.history.pushState({}, '', '/nodes'); } catch {}
|
|
127
131
|
};
|
|
@@ -157,6 +161,42 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
|
|
|
157
161
|
ro.observe(document.body);
|
|
158
162
|
return () => { window.removeEventListener('resize', apply); ro.disconnect(); };
|
|
159
163
|
}, []);
|
|
164
|
+
// #Vincent 插单 (07-31): entering /nodes with nothing selected showed the
|
|
165
|
+
// management table full-bleed — "这样特别难看". Feishu opens INTO a
|
|
166
|
+
// conversation, so desktop /nodes auto-selects the first (sorted) session.
|
|
167
|
+
// Three traps, each deliberately handled:
|
|
168
|
+
// ① URL via history.REPLACE, never push — push would trap the browser
|
|
169
|
+
// back button (/nodes → auto-select → /nodes/x → back → /nodes →
|
|
170
|
+
// auto-select again, forever).
|
|
171
|
+
// ② the table is NOT gone — it's the "manage all nodes" view, reachable
|
|
172
|
+
// via the explicit 管理视图 toggle below (localStorage-persisted; a
|
|
173
|
+
// user who prefers the table keeps it as their default and auto-select
|
|
174
|
+
// stays off).
|
|
175
|
+
// ③ an explicit ✕ close must NOT be answered by an immediate re-select
|
|
176
|
+
// (the ✕ would look broken) — userClosedRef gates auto-select off for
|
|
177
|
+
// the rest of this mount.
|
|
178
|
+
const [manageView, setManageView] = useState<boolean>(() => {
|
|
179
|
+
if (typeof window === 'undefined') return false;
|
|
180
|
+
return window.localStorage.getItem('anet-nodes-manage-view') === '1';
|
|
181
|
+
});
|
|
182
|
+
const autoSelectedRef = useRef(false);
|
|
183
|
+
const userClosedRef = useRef(false);
|
|
184
|
+
const toggleManageView = () => {
|
|
185
|
+
setManageView(v => {
|
|
186
|
+
const next = !v;
|
|
187
|
+
try { window.localStorage.setItem('anet-nodes-manage-view', next ? '1' : '0'); } catch {}
|
|
188
|
+
if (next) {
|
|
189
|
+
// entering manage view: drop any open chat, land on the table
|
|
190
|
+
userClosedRef.current = true;
|
|
191
|
+
if (chatAlias) closeChat();
|
|
192
|
+
} else {
|
|
193
|
+
// back to conversation view: let auto-select pick up again
|
|
194
|
+
userClosedRef.current = false;
|
|
195
|
+
autoSelectedRef.current = false;
|
|
196
|
+
}
|
|
197
|
+
return next;
|
|
198
|
+
});
|
|
199
|
+
};
|
|
160
200
|
const [showCreate, setShowCreate] = useState(false);
|
|
161
201
|
// #Stage A (dashboard 3-col, 07-31): persistent narrow node list rail
|
|
162
202
|
// to the left of the existing table. Default on; user can hide to
|
|
@@ -230,6 +270,15 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
|
|
|
230
270
|
// "rail count == table count" true at all times.
|
|
231
271
|
const railSessions = filtered;
|
|
232
272
|
|
|
273
|
+
const firstAlias = filtered.length > 0 ? filtered[0].alias : null;
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
if (!isDesktop || manageView || chatAlias || userClosedRef.current || autoSelectedRef.current) return;
|
|
276
|
+
if (!firstAlias) return; // empty list: nothing to select, no loop
|
|
277
|
+
autoSelectedRef.current = true;
|
|
278
|
+
setChatAlias(firstAlias);
|
|
279
|
+
try { window.history.replaceState({}, '', `/nodes/${encodeURIComponent(firstAlias)}`); } catch {}
|
|
280
|
+
}, [isDesktop, manageView, chatAlias, firstAlias]);
|
|
281
|
+
|
|
233
282
|
// #Stage C: with a node selected on desktop, the page is LIST + CHAT —
|
|
234
283
|
// the middle table does not render (通信龙 production screenshot catch:
|
|
235
284
|
// rail and table were painting the same 199 nodes twice, squeezing the
|
|
@@ -249,6 +298,31 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
|
|
|
249
298
|
// column rendered.
|
|
250
299
|
const chatOpenDesktop = !!chatAlias && isDesktop && sessions.length > 0;
|
|
251
300
|
|
|
301
|
+
// #Mobile two-level nav (Feishu phone form, 通信龙 07-31, breakpoint md
|
|
302
|
+
// per my own ruling — no third media query):
|
|
303
|
+
// < md unselected = conversation LIST fullscreen (not the table)
|
|
304
|
+
// < md selected = chat as a full PAGE with a back arrow
|
|
305
|
+
// manage mode = the old table, reachable via the same toggle
|
|
306
|
+
// Desktop auto-selects; mobile deliberately does NOT (the list IS the
|
|
307
|
+
// first screen — auto-select effect above is gated on isDesktop).
|
|
308
|
+
const mobileListLevel = !isDesktop && !chatAlias && !manageView && sessions.length > 0;
|
|
309
|
+
const mobileChatLevel = !isDesktop && !!chatAlias;
|
|
310
|
+
|
|
311
|
+
// #Mobile back restores the list scroll position (判据: enter from row
|
|
312
|
+
// ~50, come back, offset ≤200px — snapping to top is the annoying bug).
|
|
313
|
+
// Saved at selection time; restored when the list level remounts.
|
|
314
|
+
// Desktop is exempt: its rail never unmounts while chatting, and the
|
|
315
|
+
// user may have scrolled it meanwhile.
|
|
316
|
+
const savedListScrollRef = useRef(0);
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
if (isDesktop || chatAlias) return;
|
|
319
|
+
const id = requestAnimationFrame(() => {
|
|
320
|
+
const el = document.querySelector('[data-testid="node-list-scroll"]');
|
|
321
|
+
if (el) el.scrollTop = savedListScrollRef.current;
|
|
322
|
+
});
|
|
323
|
+
return () => cancelAnimationFrame(id);
|
|
324
|
+
}, [isDesktop, chatAlias]);
|
|
325
|
+
|
|
252
326
|
return (
|
|
253
327
|
<div
|
|
254
328
|
ref={rootRef}
|
|
@@ -264,10 +338,10 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
|
|
|
264
338
|
#Stage C: a selected node FORCES the rail on (even for dense-view
|
|
265
339
|
users with showRail=false) — in the list+chat layout the rail is
|
|
266
340
|
the only way to switch nodes, so it must be there. */}
|
|
267
|
-
{(showRail || chatOpenDesktop) && sessions.length > 0 && (
|
|
341
|
+
{(mobileListLevel || (isDesktop && (showRail || chatOpenDesktop) && sessions.length > 0)) && (
|
|
268
342
|
<aside
|
|
269
343
|
data-testid="node-list-rail"
|
|
270
|
-
className="
|
|
344
|
+
className="flex flex-col w-full md:w-[280px] shrink-0 h-full min-h-0 bg-[#0F1320]"
|
|
271
345
|
>
|
|
272
346
|
<NodeList
|
|
273
347
|
sessions={railSessions}
|
|
@@ -276,13 +350,37 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
|
|
|
276
350
|
search={search}
|
|
277
351
|
onSearchChange={setSearch}
|
|
278
352
|
/>
|
|
353
|
+
{/* #Vincent 插单 ②: explicit doorway between conversation-first
|
|
354
|
+
and table-first ("manage all nodes") modes. Lives in the
|
|
355
|
+
aside (NodesView's JSX) — NOT in NodeList, which IM马 is
|
|
356
|
+
editing concurrently. */}
|
|
357
|
+
<button
|
|
358
|
+
type="button"
|
|
359
|
+
onClick={toggleManageView}
|
|
360
|
+
data-testid="manage-view-toggle"
|
|
361
|
+
className="shrink-0 border-t border-[rgba(255,255,255,0.06)] bg-[#0F1320] px-3 py-2 text-left text-[11px] text-gray-500 hover:text-gray-200 transition-colors"
|
|
362
|
+
>
|
|
363
|
+
{manageView ? t('≡ 对话视图', '≡ Conversation view') : t('☷ 管理视图', '☷ Manage view')}
|
|
364
|
+
</button>
|
|
279
365
|
</aside>
|
|
280
366
|
)}
|
|
281
367
|
{/* #Stage C: middle column (header + filters + table/grid) renders
|
|
282
368
|
only when NO node is selected on desktop — see chatOpenDesktop
|
|
283
369
|
note above. Mobile keeps it always (chat is an overlay there). */}
|
|
284
|
-
{!chatOpenDesktop && (
|
|
285
|
-
<div className="flex-1 min-w-0 max-w-full h-full overflow-y-auto overflow-x-hidden p-4 sm:p-6">
|
|
370
|
+
{(isDesktop ? !chatOpenDesktop : (manageView && !chatAlias)) && (
|
|
371
|
+
<div className="flex-1 min-w-0 max-w-full h-full overflow-y-auto overflow-x-hidden p-4 sm:p-6 pb-[calc(5rem+env(safe-area-inset-bottom))] lg:pb-6">
|
|
372
|
+
{/* #Mobile manage mode: way back to the conversation list — the rail
|
|
373
|
+
(which hosts the toggle) is hidden at this level on phones. */}
|
|
374
|
+
{!isDesktop && manageView && (
|
|
375
|
+
<button
|
|
376
|
+
type="button"
|
|
377
|
+
onClick={toggleManageView}
|
|
378
|
+
data-testid="manage-view-toggle-mobile"
|
|
379
|
+
className="md:hidden mb-3 rounded-lg border border-[rgba(255,255,255,0.08)] bg-[#0F1320] px-3 py-1.5 text-[12px] text-gray-400 hover:text-gray-200"
|
|
380
|
+
>
|
|
381
|
+
{t('≡ 返回对话视图', '≡ Back to conversations')}
|
|
382
|
+
</button>
|
|
383
|
+
)}
|
|
286
384
|
{/* #209 R31 (mobile vertical rhythm — goal "大幅提升移动端体验",
|
|
287
385
|
extending R28's Overview pattern + R30's /tasks pattern):
|
|
288
386
|
this header row + the status-bar wrapper + the filter row
|
|
@@ -702,18 +800,19 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
|
|
|
702
800
|
</div>
|
|
703
801
|
)}
|
|
704
802
|
|
|
705
|
-
{/* #Stage B
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
two are mutually exclusive via isDesktop so only ONE
|
|
709
|
-
TaskChatPanel is ever mounted (no double SSE / mark-read). */}
|
|
710
|
-
{chatAlias && !isDesktop && <TaskChatPanel alias={chatAlias} onClose={closeChat} />}
|
|
803
|
+
{/* (#Stage B's mobile overlay is gone — < md chat is now a full
|
|
804
|
+
PAGE, rendered as ChatPane variant="page" below. isDesktop
|
|
805
|
+
still guarantees only ONE chat body is ever mounted.) */}
|
|
711
806
|
</div>
|
|
712
807
|
)}
|
|
713
808
|
{/* #Stage B: persistent right chat column (md+) — see ChatPane.tsx
|
|
714
809
|
(ChatPopover minus the floating shell; TaskChatPanel inline body).
|
|
715
810
|
Switching nodes swaps `alias` in place (no remount). */}
|
|
716
811
|
{chatAlias && isDesktop && <ChatPane alias={chatAlias} onClose={closeChat} />}
|
|
812
|
+
{/* #Mobile two-level nav: chat is a full PAGE (not the old overlay).
|
|
813
|
+
Back arrow and browser back both land on the list (closeChat /
|
|
814
|
+
popstate) — 判据: 行为一致. */}
|
|
815
|
+
{mobileChatLevel && <ChatPane alias={chatAlias!} variant="page" onBack={closeChat} onClose={closeChat} />}
|
|
717
816
|
</div>
|
|
718
817
|
);
|
|
719
818
|
}
|
package/package.json
CHANGED
|
@@ -80,16 +80,23 @@ const HUB_HEALTH_EMPTY = {
|
|
|
80
80
|
sse_sessions: {},
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
// Login rate limit is 10/15min/IP (app/lib/rate-limit.ts): with 4 spec
|
|
84
|
+
// files in one combined run, per-test logins blow the budget and late
|
|
85
|
+
// tests land on the sign-in page (false red). Login once per worker.
|
|
86
|
+
let cachedSessionCookie: string | null = null;
|
|
83
87
|
async function login(context: Page['context'] extends () => infer C ? C : never) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
if (!cachedSessionCookie) {
|
|
89
|
+
const res = await context.request.post(`${BASE}/api/auth/login`, {
|
|
90
|
+
data: { password: process.env.ANET_DASHBOARD_PASSWORD || 'admin123' },
|
|
91
|
+
});
|
|
92
|
+
const setCookie = res.headers()['set-cookie'] || '';
|
|
93
|
+
const m = setCookie.match(/anet_dashboard_session=([^;]+)/);
|
|
94
|
+
if (m) cachedSessionCookie = decodeURIComponent(m[1]);
|
|
95
|
+
}
|
|
96
|
+
if (cachedSessionCookie) {
|
|
90
97
|
await context.addCookies([{
|
|
91
98
|
name: 'anet_dashboard_session',
|
|
92
|
-
value:
|
|
99
|
+
value: cachedSessionCookie,
|
|
93
100
|
domain: new URL(BASE).hostname,
|
|
94
101
|
path: '/',
|
|
95
102
|
}]);
|
|
@@ -97,6 +104,9 @@ async function login(context: Page['context'] extends () => infer C ? C : never)
|
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
async function mockCommon(page: Page, statusBody: object, nodesBody: object) {
|
|
107
|
+
// #Vincent auto-select (07-31): filter suite exercises the manage/table
|
|
108
|
+
// view — opt in explicitly now that /nodes defaults to conversation-first.
|
|
109
|
+
await page.addInitScript(() => window.localStorage.setItem('anet-nodes-manage-view', '1'));
|
|
100
110
|
await page.route('**/api/hub/status**', r =>
|
|
101
111
|
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(statusBody) }));
|
|
102
112
|
await page.route('**/api/hub/nodes**', r =>
|