@sleep2agi/agent-network-dashboard 0.6.3-preview.49 → 0.6.3-preview.50
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 +97 -97
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +5 -5
- 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_client-reference-manifest.js +1 -1
- package/.next/server/app/admin.html +5 -5
- 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 +5 -5
- 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_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_client-reference-manifest.js +1 -1
- package/.next/server/app/logs.html +5 -5
- 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 +5 -5
- 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 +5 -5
- 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 +5 -5
- 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_client-reference-manifest.js +1 -1
- package/.next/server/app/providers.html +5 -5
- 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_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs.html +5 -5
- 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_client-reference-manifest.js +1 -1
- package/.next/server/app/servers.html +5 -5
- 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_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/networks.html +5 -5
- 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_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens.html +5 -5
- 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 +5 -5
- 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 +5 -5
- 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/[root-of-the-server]__05z_qq6._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__05z_qq6._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0jgqpfv._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0jgqpfv._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js.map +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__11wln8.._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__11wln8.._.js.map +1 -0
- package/.next/server/chunks/ssr/{dash-stageb-wt_app_0e7vp44._.js → dash-stageb-wt_app_045dcq5._.js} +2 -2
- package/.next/server/chunks/ssr/dash-stageb-wt_app_045dcq5._.js.map +1 -0
- package/.next/server/chunks/ssr/{dash-stageb-wt_app_0g~a.7i._.js → dash-stageb-wt_app_05u4_h~._.js} +8 -2
- package/.next/server/chunks/ssr/dash-stageb-wt_app_05u4_h~._.js.map +1 -0
- package/.next/server/chunks/ssr/{dash-stageb-wt_app_0qpn85e._.js → dash-stageb-wt_app_0nuizh~._.js} +2 -2
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0nuizh~._.js.map +1 -0
- 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_components_0vzsmx1._.js +5 -5
- package/.next/server/chunks/ssr/dash-stageb-wt_app_components_0vzsmx1._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_TasksView_tsx_0pnz7dd._.js +3 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_TasksView_tsx_0pnz7dd._.js.map +1 -0
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +5 -5
- 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/02.5_nifak.uu.js +1 -0
- package/.next/static/chunks/03hzj129k~odb.js +1 -0
- package/.next/static/chunks/06q7-vhy~1-y3.js +7 -0
- package/.next/static/chunks/0_4ljrbp.stxy.js +1 -0
- package/.next/static/chunks/0cey4i.y0a8~j.js +1 -0
- package/.next/static/chunks/{0i8qbq4v47v3r.js → 0ep78sq90x_yc.js} +1 -1
- package/.next/static/chunks/{0_xyyy_x0k42j.js → 0hm52b0y~k0~z.js} +1 -1
- package/.next/static/chunks/0kncmrm2a_q43.js +1 -0
- package/.next/static/chunks/0mhaf6lrw5nn..css +1 -0
- package/.next/static/chunks/{12jbb28xy2hw..js → 0qf8xjihw3zi..js} +1 -1
- package/.next/static/chunks/0xyu92zdvvghl.js +7 -0
- package/.next/static/chunks/0zj_navpyhltx.js +2 -0
- package/.next/static/chunks/10px7ehmo7eq0.js +1 -0
- package/.next/static/chunks/{0tv-.l-_mw4ye.js → 11zz15q37~bz5.js} +1 -1
- package/.next/static/chunks/14y4x3de1lbug.js +1 -0
- package/.next/static/chunks/15ka68xi.w-i-.js +1 -0
- package/.next/static/chunks/17qkc714xy04..js +1 -0
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/components/ChatPane.tsx +2 -2
- package/app/components/NodeList.tsx +40 -4
- package/app/components/Sidebar.tsx +180 -29
- package/app/components/TaskChatPanel.tsx +1 -1
- package/app/components/TaskDetail.tsx +407 -0
- package/app/components/TaskList.tsx +117 -0
- package/app/nodes/NodesView.tsx +28 -3
- package/app/tasks/TasksView.tsx +353 -0
- package/app/tasks/[id]/page.tsx +23 -176
- package/app/tasks/page.tsx +10 -453
- package/package.json +1 -1
- package/tests/e2e-sidebar-default.spec.ts +141 -0
- package/tests/e2e-sidebar-icon-rail.spec.ts +261 -0
- package/tests/e2e-stage-b-chat-pane.spec.ts +286 -0
- package/tests/e2e-stage-tasks-two-column.spec.ts +353 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0b-b6yh._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0b-b6yh._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0fs-j~c._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0fs-j~c._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0ma5-5u._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0ma5-5u._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0~hwpo-._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0~hwpo-._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_095q0~q._.js +0 -3
- package/.next/server/chunks/ssr/dash-stageb-wt_app_095q0~q._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0e7vp44._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0g~a.7i._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0qpn85e._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0tppjr_._.js +0 -9
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0tppjr_._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0y764mt._.js +0 -3
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0y764mt._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_page_tsx_0.fhtup._.js +0 -3
- package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_page_tsx_0.fhtup._.js.map +0 -1
- package/.next/static/chunks/027yjbjm9afuk.js +0 -1
- package/.next/static/chunks/07wf61~wof.l~.js +0 -1
- package/.next/static/chunks/0ev252k-tf_aa.css +0 -1
- package/.next/static/chunks/0j-jjs3w89dei.js +0 -1
- package/.next/static/chunks/0ko39v-gezo7d.js +0 -1
- package/.next/static/chunks/0l8vf4q2t5x4d.js +0 -7
- package/.next/static/chunks/0xcazs6t8~l4q.js +0 -2
- package/.next/static/chunks/10sr651~7vm42.js +0 -7
- package/.next/static/chunks/13_i5b0qmko7z.js +0 -1
- package/.next/static/chunks/140suz13o9va6.js +0 -1
- package/.next/static/chunks/144cf7ds3_9~b.js +0 -1
- package/.next/static/chunks/17c4wlsbmmgk9.js +0 -1
- package/.next/static/chunks/17cfly-04z1j0.js +0 -1
- /package/.next/static/{HIKVEkFShWE481BxQDjwH → vc_aJHtYwefsVcl2_f14Z}/_buildManifest.js +0 -0
- /package/.next/static/{HIKVEkFShWE481BxQDjwH → vc_aJHtYwefsVcl2_f14Z}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{HIKVEkFShWE481BxQDjwH → vc_aJHtYwefsVcl2_f14Z}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { AliasAvatar } from './AliasAvatar';
|
|
4
|
+
import { previewContent } from './utils';
|
|
5
|
+
import { STATUS_CHIP_CLASS, STATUS_DOT_HEX } from '../lib/status';
|
|
6
|
+
|
|
7
|
+
// Task-list rail for the /tasks two-column layout (类飞书 "list + detail").
|
|
8
|
+
// Rendered inside TasksView's <aside> — this file is intentionally small
|
|
9
|
+
// and only owns the list-row shape, so the same component can back both
|
|
10
|
+
// desktop rail and (later) a mobile drawer without duplicating the row
|
|
11
|
+
// template.
|
|
12
|
+
//
|
|
13
|
+
// #Stage-tasks: mirrors NodeList.tsx narrow-rail pattern. Row click calls
|
|
14
|
+
// onSelect(task_id) so the parent can drive URL/state; row does NOT know
|
|
15
|
+
// about routing.
|
|
16
|
+
|
|
17
|
+
export interface TaskListItem {
|
|
18
|
+
task_id: string;
|
|
19
|
+
network_id?: string;
|
|
20
|
+
from_name: string;
|
|
21
|
+
to_name: string;
|
|
22
|
+
status: string;
|
|
23
|
+
priority: string;
|
|
24
|
+
content: string;
|
|
25
|
+
created_at: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function timeAgo(dateStr: string): string {
|
|
29
|
+
if (!dateStr) return '--';
|
|
30
|
+
const diff = Date.now() - new Date(dateStr.replace(' ', 'T') + 'Z').getTime();
|
|
31
|
+
const s = Math.floor(diff / 1000);
|
|
32
|
+
if (s < 60) return `${s}s`;
|
|
33
|
+
if (s < 3600) return `${Math.floor(s / 60)}m`;
|
|
34
|
+
if (s < 86400) return `${Math.floor(s / 3600)}h`;
|
|
35
|
+
return `${Math.floor(s / 86400)}d`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function TaskList({
|
|
39
|
+
tasks,
|
|
40
|
+
selectedId,
|
|
41
|
+
onSelect,
|
|
42
|
+
}: {
|
|
43
|
+
tasks: TaskListItem[];
|
|
44
|
+
selectedId: string | null;
|
|
45
|
+
onSelect: (task_id: string) => void;
|
|
46
|
+
}) {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
data-testid="task-list-rail"
|
|
50
|
+
className="flex flex-col h-full max-h-screen overflow-y-auto"
|
|
51
|
+
>
|
|
52
|
+
{tasks.length === 0 ? (
|
|
53
|
+
<div className="px-4 py-6 text-xs text-gray-600 text-center">
|
|
54
|
+
没有匹配的任务
|
|
55
|
+
</div>
|
|
56
|
+
) : (
|
|
57
|
+
<ul className="flex flex-col divide-y divide-[#1c1c1f]">
|
|
58
|
+
{tasks.map((t) => {
|
|
59
|
+
const active = t.task_id === selectedId;
|
|
60
|
+
return (
|
|
61
|
+
<li key={t.task_id}>
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
data-task-row
|
|
65
|
+
data-task-id={t.task_id}
|
|
66
|
+
data-selected={active ? 'true' : 'false'}
|
|
67
|
+
onClick={() => onSelect(t.task_id)}
|
|
68
|
+
className={`w-full text-left px-3 py-2.5 transition-colors ${
|
|
69
|
+
active
|
|
70
|
+
? 'bg-cyan-500/10 border-l-2 border-cyan-500'
|
|
71
|
+
: 'border-l-2 border-transparent hover:bg-[#1c1c1f]/40'
|
|
72
|
+
}`}
|
|
73
|
+
>
|
|
74
|
+
<div className="flex items-center gap-2 mb-1">
|
|
75
|
+
{/* status dot + label */}
|
|
76
|
+
<span
|
|
77
|
+
aria-hidden
|
|
78
|
+
className="inline-block w-1.5 h-1.5 rounded-full shrink-0"
|
|
79
|
+
style={{ backgroundColor: STATUS_DOT_HEX[t.status] || '#6b7280' }}
|
|
80
|
+
/>
|
|
81
|
+
<span
|
|
82
|
+
className={`text-[10px] px-1.5 py-0.5 rounded border ${
|
|
83
|
+
STATUS_CHIP_CLASS[t.status] ||
|
|
84
|
+
'bg-gray-500/10 text-gray-400 border-gray-500/20'
|
|
85
|
+
}`}
|
|
86
|
+
>
|
|
87
|
+
{t.status}
|
|
88
|
+
</span>
|
|
89
|
+
{t.priority === 'high' && (
|
|
90
|
+
<span className="text-[10px] text-red-400 shrink-0">HIGH</span>
|
|
91
|
+
)}
|
|
92
|
+
<span className="ml-auto text-[10px] text-gray-600 tabular-nums shrink-0">
|
|
93
|
+
{timeAgo(t.created_at)}
|
|
94
|
+
</span>
|
|
95
|
+
</div>
|
|
96
|
+
<div className="flex items-center gap-1 text-xs text-gray-300 min-w-0 mb-1">
|
|
97
|
+
{t.from_name && <AliasAvatar alias={t.from_name} size={14} />}
|
|
98
|
+
<span className="truncate max-w-[35%]">{t.from_name || '--'}</span>
|
|
99
|
+
<span className="text-gray-600 shrink-0">→</span>
|
|
100
|
+
{t.to_name && <AliasAvatar alias={t.to_name} size={14} />}
|
|
101
|
+
<span className="truncate max-w-[35%]">{t.to_name || '--'}</span>
|
|
102
|
+
</div>
|
|
103
|
+
<div
|
|
104
|
+
className="text-[11px] text-gray-500 line-clamp-2 leading-snug"
|
|
105
|
+
title={t.content}
|
|
106
|
+
>
|
|
107
|
+
{previewContent(t.content) || <span className="italic">(no content)</span>}
|
|
108
|
+
</div>
|
|
109
|
+
</button>
|
|
110
|
+
</li>
|
|
111
|
+
);
|
|
112
|
+
})}
|
|
113
|
+
</ul>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
package/app/nodes/NodesView.tsx
CHANGED
|
@@ -135,6 +135,28 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
|
|
|
135
135
|
window.addEventListener('popstate', onPop);
|
|
136
136
|
return () => window.removeEventListener('popstate', onPop);
|
|
137
137
|
}, []);
|
|
138
|
+
// #Stage D: the shell above us (AppShell) keeps a ~28px HealthBanner in
|
|
139
|
+
// flow even when green, and its root is min-h (content-driven), so a
|
|
140
|
+
// plain h-[100dvh] here overflows the viewport by exactly the banner
|
|
141
|
+
// height and the document scrolls again. AppShell is shared by every
|
|
142
|
+
// page (out of Stage D scope) — instead, size this page to the space
|
|
143
|
+
// actually left below whatever the shell renders above us. The
|
|
144
|
+
// ResizeObserver on body re-measures when the banner appears/collapses/
|
|
145
|
+
// is dismissed. className keeps h-[100dvh] as the SSR first paint.
|
|
146
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
const el = rootRef.current;
|
|
149
|
+
if (!el) return;
|
|
150
|
+
const apply = () => {
|
|
151
|
+
const top = el.getBoundingClientRect().top + window.scrollY;
|
|
152
|
+
el.style.height = `${Math.max(240, window.innerHeight - top)}px`;
|
|
153
|
+
};
|
|
154
|
+
apply();
|
|
155
|
+
window.addEventListener('resize', apply);
|
|
156
|
+
const ro = new ResizeObserver(apply);
|
|
157
|
+
ro.observe(document.body);
|
|
158
|
+
return () => { window.removeEventListener('resize', apply); ro.disconnect(); };
|
|
159
|
+
}, []);
|
|
138
160
|
const [showCreate, setShowCreate] = useState(false);
|
|
139
161
|
// #Stage A (dashboard 3-col, 07-31): persistent narrow node list rail
|
|
140
162
|
// to the left of the existing table. Default on; user can hide to
|
|
@@ -229,9 +251,10 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
|
|
|
229
251
|
|
|
230
252
|
return (
|
|
231
253
|
<div
|
|
254
|
+
ref={rootRef}
|
|
232
255
|
data-testid="nodes-layout"
|
|
233
256
|
data-rail-visible={showRail ? 'true' : 'false'}
|
|
234
|
-
className="
|
|
257
|
+
className="h-[100dvh] max-w-full overflow-hidden bg-[#0b0b0d] text-gray-100 flex"
|
|
235
258
|
>
|
|
236
259
|
{/* #Stage A rail — only mounted when showRail is true AND we're
|
|
237
260
|
past the mobile breakpoint (below sm, rail is hidden by CSS
|
|
@@ -244,12 +267,14 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
|
|
|
244
267
|
{(showRail || chatOpenDesktop) && sessions.length > 0 && (
|
|
245
268
|
<aside
|
|
246
269
|
data-testid="node-list-rail"
|
|
247
|
-
className="hidden sm:flex sm:flex-col w-[280px] shrink-0 min-h-
|
|
270
|
+
className="hidden sm:flex sm:flex-col w-[280px] shrink-0 h-full min-h-0 bg-[#0F1320]"
|
|
248
271
|
>
|
|
249
272
|
<NodeList
|
|
250
273
|
sessions={railSessions}
|
|
251
274
|
selectedAlias={chatAlias}
|
|
252
275
|
onSelect={selectNode}
|
|
276
|
+
search={search}
|
|
277
|
+
onSearchChange={setSearch}
|
|
253
278
|
/>
|
|
254
279
|
</aside>
|
|
255
280
|
)}
|
|
@@ -257,7 +282,7 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
|
|
|
257
282
|
only when NO node is selected on desktop — see chatOpenDesktop
|
|
258
283
|
note above. Mobile keeps it always (chat is an overlay there). */}
|
|
259
284
|
{!chatOpenDesktop && (
|
|
260
|
-
<div className="flex-1 min-w-0 max-w-full overflow-x-hidden p-4 sm:p-6">
|
|
285
|
+
<div className="flex-1 min-w-0 max-w-full h-full overflow-y-auto overflow-x-hidden p-4 sm:p-6">
|
|
261
286
|
{/* #209 R31 (mobile vertical rhythm — goal "大幅提升移动端体验",
|
|
262
287
|
extending R28's Overview pattern + R30's /tasks pattern):
|
|
263
288
|
this header row + the status-bar wrapper + the filter row
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Suspense,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useState,
|
|
8
|
+
useSyncExternalStore,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { useSearchParams } from 'next/navigation';
|
|
11
|
+
import { useNetworkId } from '../lib/network-context';
|
|
12
|
+
import { EmptyState } from '../components/EmptyState';
|
|
13
|
+
import { TaskList, type TaskListItem } from '../components/TaskList';
|
|
14
|
+
import { TaskDetail } from '../components/TaskDetail';
|
|
15
|
+
import {
|
|
16
|
+
TASK_STATUSES,
|
|
17
|
+
STATUS_CHIP_CLASS,
|
|
18
|
+
STATUS_DOT_HEX,
|
|
19
|
+
STATUS_BAR_CLASS,
|
|
20
|
+
} from '../lib/status';
|
|
21
|
+
|
|
22
|
+
// #Stage-tasks (dashboard 类飞书 two-column, 07-31):
|
|
23
|
+
//
|
|
24
|
+
// /tasks moves from one-big-table + inline-expand + drawer to a
|
|
25
|
+
// list-plus-detail layout with a shareable URL /tasks/[id]. Mirrors
|
|
26
|
+
// /nodes NodesView (see app/nodes/NodesView.tsx for the pattern).
|
|
27
|
+
//
|
|
28
|
+
// Deliberately: the old expand-in-place row and the TaskDrawer side
|
|
29
|
+
// sheet are gone from the /tasks page. The right pane replaces both
|
|
30
|
+
// affordances so we don't leave the layout showing "one thing twice"
|
|
31
|
+
// (通信龙 07-31 catch from /nodes: rail added alongside the table left
|
|
32
|
+
// 199 nodes rendered in two places at once). Detail negative
|
|
33
|
+
// assertions in the tests must be paired with a positive assertion so
|
|
34
|
+
// "detail integrated into pane" and "detail feature gone" can't be
|
|
35
|
+
// confused for each other.
|
|
36
|
+
//
|
|
37
|
+
// TaskDrawer.tsx itself is not deleted — no other file imports it,
|
|
38
|
+
// but the boundary of this PR is app/tasks/**, and dead-code sweeps
|
|
39
|
+
// are a separate concern.
|
|
40
|
+
|
|
41
|
+
const MD_QUERY = '(min-width: 768px)';
|
|
42
|
+
function subscribeMd(cb: () => void) {
|
|
43
|
+
const mql = window.matchMedia(MD_QUERY);
|
|
44
|
+
mql.addEventListener('change', cb);
|
|
45
|
+
return () => mql.removeEventListener('change', cb);
|
|
46
|
+
}
|
|
47
|
+
function useIsDesktop(): boolean {
|
|
48
|
+
return useSyncExternalStore(
|
|
49
|
+
subscribeMd,
|
|
50
|
+
() => window.matchMedia(MD_QUERY).matches,
|
|
51
|
+
() => true,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Task IDs on the CommHub side are opaque strings — usually hex, but
|
|
56
|
+
// treat them as arbitrary to match the alias precedent. Decode once
|
|
57
|
+
// defensively (same shape as /nodes NodesView.decodeAliasSegment).
|
|
58
|
+
export function decodeIdSegment(seg: string): string {
|
|
59
|
+
try {
|
|
60
|
+
return decodeURIComponent(seg);
|
|
61
|
+
} catch {
|
|
62
|
+
return seg;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function TasksViewInner({ initialSelectedId = null }: { initialSelectedId?: string | null }) {
|
|
67
|
+
const searchParams = useSearchParams();
|
|
68
|
+
const { networkId } = useNetworkId();
|
|
69
|
+
const [tasks, setTasks] = useState<TaskListItem[]>([]);
|
|
70
|
+
const [count, setCount] = useState(0);
|
|
71
|
+
const [loading, setLoading] = useState(true);
|
|
72
|
+
const [error, setError] = useState('');
|
|
73
|
+
|
|
74
|
+
const [filterStatus, setFilterStatus] = useState(searchParams.get('status') || '');
|
|
75
|
+
const [filterFrom, setFilterFrom] = useState('');
|
|
76
|
+
const [filterTo, setFilterTo] = useState('');
|
|
77
|
+
|
|
78
|
+
// #Stage-tasks: selection is seeded from /tasks/[id] URL, then plain
|
|
79
|
+
// client state kept in sync with pushState (shallow — no route remount,
|
|
80
|
+
// switching tasks swaps the right pane in place).
|
|
81
|
+
const [selectedId, setSelectedId] = useState<string | null>(initialSelectedId);
|
|
82
|
+
const isDesktop = useIsDesktop();
|
|
83
|
+
|
|
84
|
+
const selectTask = (task_id: string) => {
|
|
85
|
+
setSelectedId(task_id);
|
|
86
|
+
try {
|
|
87
|
+
window.history.pushState({}, '', `/tasks/${encodeURIComponent(task_id)}`);
|
|
88
|
+
} catch {}
|
|
89
|
+
};
|
|
90
|
+
const closeDetail = () => {
|
|
91
|
+
setSelectedId(null);
|
|
92
|
+
try {
|
|
93
|
+
window.history.pushState({}, '', '/tasks');
|
|
94
|
+
} catch {}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Back/forward — mirror URL into state.
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const onPop = () => {
|
|
100
|
+
const m = window.location.pathname.match(/^\/tasks\/([^/]+)\/?$/);
|
|
101
|
+
setSelectedId(m ? decodeIdSegment(m[1]) : null);
|
|
102
|
+
};
|
|
103
|
+
window.addEventListener('popstate', onPop);
|
|
104
|
+
return () => window.removeEventListener('popstate', onPop);
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
const fetchTasks = useCallback(async () => {
|
|
108
|
+
try {
|
|
109
|
+
const params = new URLSearchParams();
|
|
110
|
+
if (networkId) params.set('network_id', networkId);
|
|
111
|
+
if (filterStatus) params.set('status', filterStatus);
|
|
112
|
+
if (filterFrom) params.set('from_name', filterFrom);
|
|
113
|
+
if (filterTo) params.set('to_name', filterTo);
|
|
114
|
+
params.set('limit', '100');
|
|
115
|
+
|
|
116
|
+
const res = await fetch(`/api/hub/tasks?${params.toString()}`);
|
|
117
|
+
if (res.status === 401) {
|
|
118
|
+
window.location.assign('/login');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const data = await res.json();
|
|
122
|
+
setTasks(data.tasks || []);
|
|
123
|
+
setCount(data.count ?? (data.tasks?.length || 0));
|
|
124
|
+
setError('');
|
|
125
|
+
} catch (e: unknown) {
|
|
126
|
+
setError(e instanceof Error ? e.message : 'fetch failed');
|
|
127
|
+
} finally {
|
|
128
|
+
setLoading(false);
|
|
129
|
+
}
|
|
130
|
+
}, [filterStatus, filterFrom, filterTo, networkId]);
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
setLoading(true);
|
|
134
|
+
fetchTasks();
|
|
135
|
+
const interval = setInterval(fetchTasks, 5000);
|
|
136
|
+
return () => clearInterval(interval);
|
|
137
|
+
}, [fetchTasks]);
|
|
138
|
+
|
|
139
|
+
const counts: Record<string, number> = {};
|
|
140
|
+
tasks.forEach((t) => {
|
|
141
|
+
counts[t.status] = (counts[t.status] || 0) + 1;
|
|
142
|
+
});
|
|
143
|
+
const total = tasks.length || 1;
|
|
144
|
+
const bars = TASK_STATUSES.map((key) => ({ key, color: STATUS_BAR_CLASS[key] })).filter(
|
|
145
|
+
(b) => counts[b.key],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div
|
|
150
|
+
data-testid="tasks-layout"
|
|
151
|
+
data-selected-id={selectedId ?? ''}
|
|
152
|
+
className="min-h-screen max-w-full overflow-x-hidden bg-[#0b0b0d] text-gray-100 flex"
|
|
153
|
+
>
|
|
154
|
+
{/* Rail — desktop always visible; on mobile it holds the whole
|
|
155
|
+
page (detail becomes a full-screen overlay). No "toggle
|
|
156
|
+
rail" button on tasks: there is no dense one-column table
|
|
157
|
+
to fall back to (unlike /nodes where Vincent's 看全局 view
|
|
158
|
+
exists). Removing the old table is the whole point of this
|
|
159
|
+
refactor — 通信龙 07-31 catch from /nodes: don't leave the
|
|
160
|
+
layout showing "one thing twice". */}
|
|
161
|
+
<aside
|
|
162
|
+
data-testid="task-list-column"
|
|
163
|
+
className={`flex flex-col w-full sm:w-[380px] shrink-0 min-h-screen sticky top-0 border-r border-[#1c1c1f] ${
|
|
164
|
+
selectedId && !isDesktop ? 'hidden' : 'flex'
|
|
165
|
+
}`}
|
|
166
|
+
>
|
|
167
|
+
<div className="px-4 py-3 border-b border-[#1c1c1f]">
|
|
168
|
+
<div className="flex items-center gap-3 mb-3">
|
|
169
|
+
<h1 className="text-xl font-bold text-white lg:ml-0 ml-10">Tasks</h1>
|
|
170
|
+
{tasks.length < count && (
|
|
171
|
+
<span
|
|
172
|
+
className="text-xs bg-blue-900/30 text-blue-400 px-2 py-0.5 rounded-full border border-blue-800/30 tabular-nums"
|
|
173
|
+
title={`Showing ${tasks.length} of ${count} tasks`}
|
|
174
|
+
>
|
|
175
|
+
{tasks.length} / {count}
|
|
176
|
+
</span>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
{tasks.length > 0 && (
|
|
181
|
+
<>
|
|
182
|
+
{/* Status tab strip */}
|
|
183
|
+
<div className="anet-tabstrip-wrap mb-2">
|
|
184
|
+
<div className="anet-tabstrip flex flex-wrap gap-1 bg-[#161618] rounded-lg border border-[#26262b] p-1 overflow-x-auto scrollbar-thin">
|
|
185
|
+
{['', ...TASK_STATUSES].map((s) => {
|
|
186
|
+
const cnt = s === '' ? tasks.length : counts[s] || 0;
|
|
187
|
+
const isActive = filterStatus === s;
|
|
188
|
+
return (
|
|
189
|
+
<button
|
|
190
|
+
key={s || 'all'}
|
|
191
|
+
onClick={() => setFilterStatus(s)}
|
|
192
|
+
disabled={cnt === 0 && s !== '' && !isActive}
|
|
193
|
+
className={`px-2.5 py-1 rounded-md text-xs transition-colors flex items-center gap-1 shrink-0 whitespace-nowrap disabled:opacity-30 disabled:cursor-not-allowed ${
|
|
194
|
+
isActive
|
|
195
|
+
? `${STATUS_CHIP_CLASS[s] || 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20'} border`
|
|
196
|
+
: 'text-gray-500 hover:text-gray-300 hover:bg-[#1c1c1f]/40'
|
|
197
|
+
}`}
|
|
198
|
+
>
|
|
199
|
+
{s && (
|
|
200
|
+
<span
|
|
201
|
+
aria-hidden
|
|
202
|
+
className="inline-block w-1.5 h-1.5 rounded-full shrink-0"
|
|
203
|
+
style={{ backgroundColor: STATUS_DOT_HEX[s] || '#6b7280' }}
|
|
204
|
+
/>
|
|
205
|
+
)}
|
|
206
|
+
<span>{s || 'All'}</span>
|
|
207
|
+
<span
|
|
208
|
+
className={`text-[10px] tabular-nums ${
|
|
209
|
+
isActive ? 'opacity-80' : 'text-gray-600'
|
|
210
|
+
}`}
|
|
211
|
+
>
|
|
212
|
+
{cnt}
|
|
213
|
+
</span>
|
|
214
|
+
</button>
|
|
215
|
+
);
|
|
216
|
+
})}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
{/* From/To filters */}
|
|
221
|
+
<div className="flex flex-wrap items-center gap-1.5 mb-2">
|
|
222
|
+
<div className="flex items-center gap-1 rounded border border-[#26262b] bg-[#161618] px-2 py-1 focus-within:border-blue-500/40">
|
|
223
|
+
<span className="text-[9px] uppercase tracking-wide text-gray-600">From</span>
|
|
224
|
+
<input
|
|
225
|
+
type="text"
|
|
226
|
+
value={filterFrom}
|
|
227
|
+
onChange={(e) => setFilterFrom(e.target.value)}
|
|
228
|
+
placeholder="any"
|
|
229
|
+
className="w-16 bg-transparent text-xs text-white placeholder-gray-700 focus:outline-none"
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
<div className="flex items-center gap-1 rounded border border-[#26262b] bg-[#161618] px-2 py-1 focus-within:border-blue-500/40">
|
|
233
|
+
<span className="text-[9px] uppercase tracking-wide text-gray-600">To</span>
|
|
234
|
+
<input
|
|
235
|
+
type="text"
|
|
236
|
+
value={filterTo}
|
|
237
|
+
onChange={(e) => setFilterTo(e.target.value)}
|
|
238
|
+
placeholder="any"
|
|
239
|
+
className="w-16 bg-transparent text-xs text-white placeholder-gray-700 focus:outline-none"
|
|
240
|
+
/>
|
|
241
|
+
</div>
|
|
242
|
+
{(filterStatus || filterFrom || filterTo) && (
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
onClick={() => {
|
|
246
|
+
setFilterStatus('');
|
|
247
|
+
setFilterFrom('');
|
|
248
|
+
setFilterTo('');
|
|
249
|
+
}}
|
|
250
|
+
className="rounded border border-gray-700 px-2 py-0.5 text-[10px] text-gray-500 hover:text-gray-200 hover:border-gray-600"
|
|
251
|
+
>
|
|
252
|
+
Clear
|
|
253
|
+
</button>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
{/* Status distribution bar (only when no status filter) */}
|
|
258
|
+
{!filterStatus && bars.length > 0 && (
|
|
259
|
+
<div className="flex h-1 rounded-full overflow-hidden bg-gray-800">
|
|
260
|
+
{bars.map((b) => (
|
|
261
|
+
<div
|
|
262
|
+
key={b.key}
|
|
263
|
+
className={b.color}
|
|
264
|
+
style={{ width: `${(counts[b.key] / total) * 100}%` }}
|
|
265
|
+
title={`${b.key}: ${counts[b.key]}`}
|
|
266
|
+
/>
|
|
267
|
+
))}
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
</>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
{error && (
|
|
274
|
+
<div className="bg-red-900/20 border border-red-800/40 text-red-300 px-3 py-1.5 rounded mt-2 text-xs">
|
|
275
|
+
{error}
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<div className="flex-1 min-h-0 overflow-y-auto">
|
|
281
|
+
{loading ? (
|
|
282
|
+
<div className="animate-pulse space-y-1 px-3 py-2">
|
|
283
|
+
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
|
|
284
|
+
<div key={i} className="h-14 bg-gray-800/40 rounded" />
|
|
285
|
+
))}
|
|
286
|
+
</div>
|
|
287
|
+
) : tasks.length === 0 ? (
|
|
288
|
+
<div className="px-3 py-6">
|
|
289
|
+
<EmptyState
|
|
290
|
+
variant="tasks"
|
|
291
|
+
sub={
|
|
292
|
+
filterStatus || filterFrom || filterTo
|
|
293
|
+
? 'No tasks match the current filters. Try clearing them.'
|
|
294
|
+
: 'Tasks will appear here when agents send them via CommHub.'
|
|
295
|
+
}
|
|
296
|
+
/>
|
|
297
|
+
</div>
|
|
298
|
+
) : (
|
|
299
|
+
<TaskList tasks={tasks} selectedId={selectedId} onSelect={selectTask} />
|
|
300
|
+
)}
|
|
301
|
+
</div>
|
|
302
|
+
</aside>
|
|
303
|
+
|
|
304
|
+
{/* Detail — desktop: right pane inside the flex row. Mobile: full-screen
|
|
305
|
+
overlay that hides the list column above. Both variants get onClose:
|
|
306
|
+
on desktop it returns the right pane to the empty state (preserves
|
|
307
|
+
the "← Tasks" return affordance the old full-page detail had);
|
|
308
|
+
on mobile it dismisses the overlay. Either way the URL drops back
|
|
309
|
+
to /tasks. 通信龙 07-31 constraint: 返回入口别丢. */}
|
|
310
|
+
{selectedId && isDesktop && (
|
|
311
|
+
<section
|
|
312
|
+
data-testid="task-detail-column"
|
|
313
|
+
className="flex-1 min-w-0 min-h-screen"
|
|
314
|
+
>
|
|
315
|
+
<TaskDetail taskId={selectedId} onClose={closeDetail} onRetried={fetchTasks} />
|
|
316
|
+
</section>
|
|
317
|
+
)}
|
|
318
|
+
{selectedId && !isDesktop && (
|
|
319
|
+
<section
|
|
320
|
+
data-testid="task-detail-mobile"
|
|
321
|
+
className="fixed inset-0 z-40 bg-[#0b0b0d]"
|
|
322
|
+
>
|
|
323
|
+
<TaskDetail taskId={selectedId} onClose={closeDetail} onRetried={fetchTasks} />
|
|
324
|
+
</section>
|
|
325
|
+
)}
|
|
326
|
+
{!selectedId && isDesktop && (
|
|
327
|
+
<section
|
|
328
|
+
data-testid="task-detail-empty"
|
|
329
|
+
className="flex-1 min-w-0 min-h-screen flex items-center justify-center text-sm text-gray-600"
|
|
330
|
+
>
|
|
331
|
+
请从左侧列表选择一条任务查看详情
|
|
332
|
+
</section>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// useSearchParams needs a Suspense boundary in Next 14+ app-router. Same
|
|
339
|
+
// shape the old page.tsx had (wrapper → Inner). Fallback matches the
|
|
340
|
+
// loaded layout width so the burger button doesn't crash into "Loa…".
|
|
341
|
+
export function TasksView({ initialSelectedId = null }: { initialSelectedId?: string | null }) {
|
|
342
|
+
return (
|
|
343
|
+
<Suspense
|
|
344
|
+
fallback={
|
|
345
|
+
<div className="min-h-screen bg-[#0b0b0d] text-gray-100 p-4 sm:p-6">
|
|
346
|
+
<div className="lg:ml-0 ml-10 text-gray-500 text-sm">Loading tasks…</div>
|
|
347
|
+
</div>
|
|
348
|
+
}
|
|
349
|
+
>
|
|
350
|
+
<TasksViewInner initialSelectedId={initialSelectedId} />
|
|
351
|
+
</Suspense>
|
|
352
|
+
);
|
|
353
|
+
}
|