@sleep2agi/agent-network-dashboard 0.6.3-preview.43 → 0.6.3-preview.45
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 -0
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/diagnostics/route-bundle-stats.json +78 -78
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +2 -2
- package/.next/server/app/_not-found.rsc +12 -12
- package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
- package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
- 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 +1 -1
- 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 +14 -14
- package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
- package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
- package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
- package/.next/server/app/admin.segments/_tree.segment.rsc +1 -1
- 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 +14 -14
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/server/app/index.segments/_index.segment.rsc +7 -7
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- 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 +14 -14
- package/.next/server/app/login.segments/_full.segment.rsc +14 -14
- package/.next/server/app/login.segments/_head.segment.rsc +4 -4
- package/.next/server/app/login.segments/_index.segment.rsc +7 -7
- package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
- 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 +14 -14
- package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
- package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
- package/.next/server/app/logs.segments/_tree.segment.rsc +1 -1
- 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 +14 -14
- package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
- package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
- package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
- package/.next/server/app/messages.segments/_tree.segment.rsc +1 -1
- 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 +14 -14
- package/.next/server/app/node.segments/_full.segment.rsc +14 -14
- package/.next/server/app/node.segments/_head.segment.rsc +4 -4
- package/.next/server/app/node.segments/_index.segment.rsc +7 -7
- package/.next/server/app/node.segments/_tree.segment.rsc +1 -1
- 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/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 +14 -14
- package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
- package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
- package/.next/server/app/nodes.segments/_tree.segment.rsc +1 -1
- 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 +14 -14
- package/.next/server/app/providers.segments/_full.segment.rsc +14 -14
- package/.next/server/app/providers.segments/_head.segment.rsc +4 -4
- package/.next/server/app/providers.segments/_index.segment.rsc +7 -7
- package/.next/server/app/providers.segments/_tree.segment.rsc +1 -1
- 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 +14 -14
- package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
- package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +1 -1
- 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 +14 -14
- package/.next/server/app/servers.segments/_full.segment.rsc +14 -14
- package/.next/server/app/servers.segments/_head.segment.rsc +4 -4
- package/.next/server/app/servers.segments/_index.segment.rsc +7 -7
- package/.next/server/app/servers.segments/_tree.segment.rsc +1 -1
- 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 +14 -14
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +1 -1
- 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 +14 -14
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +1 -1
- 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 +14 -14
- package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
- package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
- package/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- 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 +14 -14
- package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
- package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
- package/.next/server/app/tasks.segments/_tree.segment.rsc +1 -1
- 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]__0jns048._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0jns048._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0va6h-y._.js +3 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0va6h-y._.js.map +1 -1
- package/.next/server/chunks/ssr/{[root-of-the-server]__056tw5g._.js → [root-of-the-server]__0z0lvdf._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__0z0lvdf._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__13svd64._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__13svd64._.js.map +1 -0
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_0f4savf._.js +3 -3
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_0f4savf._.js.map +1 -1
- package/.next/server/chunks/ssr/{dash-492_agent-network-dashboard_app_12yiaen._.js → dash-492_agent-network-dashboard_app_07foku.._.js} +2 -2
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_07foku.._.js.map +1 -0
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_0fv9~x1._.js +1 -1
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_0fv9~x1._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_0jey2is._.js +3 -0
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_0jey2is._.js.map +1 -0
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_0tbukk6._.js +3 -0
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_0tbukk6._.js.map +1 -0
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_10nv4wt._.js +3 -0
- package/.next/server/chunks/ssr/{dash-492_agent-network-dashboard_app_02~ml1y._.js.map → dash-492_agent-network-dashboard_app_10nv4wt._.js.map} +1 -1
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_components_07gcm8v._.js +3 -3
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_components_07gcm8v._.js.map +1 -1
- 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/static/chunks/0.t9q6b8-tp7o.js +1 -0
- package/.next/static/chunks/{07hmiozabjis~.js → 00.lep~gx75xc.js} +1 -1
- package/.next/static/chunks/0casvy.vqveum.js +7 -0
- package/.next/static/chunks/0e3gktr80d3im.js +2 -0
- package/.next/static/chunks/0gv~xarphwy7~.js +1 -0
- package/.next/static/chunks/0jgo1j.i.wd5q.js +7 -0
- package/.next/static/chunks/0k_3kdj.1c2m5.js +1 -0
- package/.next/static/chunks/0uj3h67aerffm.js +1 -0
- package/.next/static/chunks/0~rml5aiu3yw7.js +7 -0
- package/.next/static/chunks/0~v0m8f1.aob5.js +4 -0
- package/.next/static/chunks/16._eot2r-z1r.js +1 -0
- package/.next/static/chunks/16kecv_vyjyyv.js +1 -0
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/components/ChatPopover.tsx +9 -2
- package/app/components/HealthBanner.tsx +21 -2
- package/app/components/Sidebar.tsx +21 -2
- package/app/components/StatsBar.tsx +32 -14
- package/app/components/TaskChatPanel.tsx +11 -3
- package/app/components/TopoGraph.tsx +25 -11
- package/app/lib/presence.ts +98 -0
- package/app/page.tsx +23 -14
- package/package.json +1 -1
- package/tests/e2e-515-presence-predicate.spec.ts +75 -0
- package/tests/e2e-515-presence.spec.ts +148 -0
- package/tests/e2e-53-blind-vs-empty.spec.ts +262 -0
- package/tests/e2e-53-presence-status.spec.ts +52 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__056tw5g._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__06g2-qq._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__06g2-qq._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0qv41lg._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0qv41lg._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_02~ml1y._.js +0 -3
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_04nejob._.js +0 -3
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_04nejob._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_0kehfih._.js +0 -3
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_0kehfih._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_0vvr81v._.js +0 -3
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_0vvr81v._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_0~9qq2o._.js +0 -3
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_0~9qq2o._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-492_agent-network-dashboard_app_12yiaen._.js.map +0 -1
- package/.next/static/chunks/00kg.oomong9m.js +0 -1
- package/.next/static/chunks/02a6-zbyr.zoy.js +0 -2
- package/.next/static/chunks/06ww0tn_jfcfd.js +0 -1
- package/.next/static/chunks/0_90al8hntttb.js +0 -1
- package/.next/static/chunks/0idb.71vziyhz.js +0 -7
- package/.next/static/chunks/0qw7i_y5i75.q.js +0 -1
- package/.next/static/chunks/0szzm0ma-335n.js +0 -4
- package/.next/static/chunks/0xxfl.bqy.bd4.js +0 -7
- package/.next/static/chunks/0yscbgm2a_d1u.js +0 -1
- package/.next/static/chunks/0zj956.f-k1jj.js +0 -7
- package/.next/static/chunks/17ns4oqzib99e.js +0 -1
- /package/.next/static/{BXw6vds1QR_zb-M-KgpT7 → EEoQxe-lggOsAx0h8hIFx}/_buildManifest.js +0 -0
- /package/.next/static/{BXw6vds1QR_zb-M-KgpT7 → EEoQxe-lggOsAx0h8hIFx}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{BXw6vds1QR_zb-M-KgpT7 → EEoQxe-lggOsAx0h8hIFx}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { test, expect, Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
const BASE = process.env.TEST_URL || 'http://localhost:3100';
|
|
4
|
+
|
|
5
|
+
// #53 DOM behaviour — the "blind" vs "truly empty" fixtures MUST render
|
|
6
|
+
// distinct UIs. Same-render is exactly the regression #53 was opened
|
|
7
|
+
// for (unified predicate collapses "can't see" and "nothing there" into
|
|
8
|
+
// a confident, consistent, and wrong 0 online).
|
|
9
|
+
//
|
|
10
|
+
// Acceptance criterion from the issue:
|
|
11
|
+
// > 两个 fixture 必须都在,且断言互不相同 —— 否则又回到「两种情形一个画面」
|
|
12
|
+
//
|
|
13
|
+
// PR #54 review round-2 added a third fixture — BLIND (populated fleet)
|
|
14
|
+
// — because /api/hub/status and /health have INDEPENDENT auth gates, so
|
|
15
|
+
// "status returned sessions, health omitted sse_sessions" is a real
|
|
16
|
+
// reachable combination that only the populated-fleet fixture covers.
|
|
17
|
+
//
|
|
18
|
+
// Assertion matrix (each column must have at least one row unique to
|
|
19
|
+
// itself, and each row must be independently red-provable — a targeted
|
|
20
|
+
// mutation of the code that assertion protects should make ONLY that
|
|
21
|
+
// row's test go red, not just the earliest gate in that test):
|
|
22
|
+
//
|
|
23
|
+
// | BLIND(empty) BLIND(populated) EMPTY
|
|
24
|
+
// "? online" strip | ✓ — ✗ (not.toMatch)
|
|
25
|
+
// "0 online" strip | ✗ — ✓
|
|
26
|
+
// "presence unavail" | — ✓ ✗
|
|
27
|
+
// HealthBanner text | ✓ ✓ ✗
|
|
28
|
+
// agent-alpha visible | — ✓ — ← proves the
|
|
29
|
+
// | grid actually
|
|
30
|
+
// | mounted, so
|
|
31
|
+
// | "cell isn't a
|
|
32
|
+
// | number" can't
|
|
33
|
+
// | pass on an
|
|
34
|
+
// | unrendered grid
|
|
35
|
+
// Online cell === '?' | — ✓ —
|
|
36
|
+
// "Spin up..." pitch | ✗ ✗ — (may or may not)
|
|
37
|
+
//
|
|
38
|
+
// Witnessed-red evidence for each fixture:
|
|
39
|
+
// - BLIND(empty) ← mutate presenceStatus → always 'ready': banner
|
|
40
|
+
// gate at "Unable to read presence" fails.
|
|
41
|
+
// - EMPTY ← the "always 'ready'" mutation doesn't red this
|
|
42
|
+
// test because EMPTY *wants* 'ready'. The
|
|
43
|
+
// regression EMPTY protects is the OPPOSITE
|
|
44
|
+
// direction: "someone treats sse_sessions === {}
|
|
45
|
+
// as if the map were missing". Two targeted
|
|
46
|
+
// mutations of presence.ts, each catches EMPTY
|
|
47
|
+
// at a different assertion — the pair is what
|
|
48
|
+
// proves EMPTY isn't relying on one accidental
|
|
49
|
+
// assertion:
|
|
50
|
+
//
|
|
51
|
+
// (a) `if (sseSessions && Object.keys(sseSessions)
|
|
52
|
+
// .length > 0) return 'ready';` (requires
|
|
53
|
+
// non-empty map to be 'ready')
|
|
54
|
+
// → {} + conns=0 falls through to the conns
|
|
55
|
+
// check → returns 'loading' (NOT 'blind',
|
|
56
|
+
// because EMPTY fixture has conns=0).
|
|
57
|
+
// → page.tsx sets presenceUnknown=true.
|
|
58
|
+
// → StatsBar renders '? online' not '0 online'.
|
|
59
|
+
// → RED at L223: expect(bodyText).toMatch(
|
|
60
|
+
// /\b0\s+online\b/i) — Expected pattern
|
|
61
|
+
// /\b0\s+online\b/i, received "agent
|
|
62
|
+
// network..." (the '0 online' text is
|
|
63
|
+
// gone from the strip).
|
|
64
|
+
//
|
|
65
|
+
// (b) `if (sseSessions && Object.keys(sseSessions)
|
|
66
|
+
// .length > 0) return 'ready'; if (sseSessions
|
|
67
|
+
// !== undefined) return 'blind';` (forces
|
|
68
|
+
// empty-map to 'blind' directly)
|
|
69
|
+
// → {} → 'blind' regardless of conns.
|
|
70
|
+
// → HealthBanner fires the red presence-blind
|
|
71
|
+
// banner.
|
|
72
|
+
// → RED at L217: expect(bodyText.toLowerCase())
|
|
73
|
+
// .not.toContain('unable to read presence')
|
|
74
|
+
// — Expected NOT to contain, received
|
|
75
|
+
// "agent network... unable to read presence
|
|
76
|
+
// — hub reports 0 live SSE...".
|
|
77
|
+
//
|
|
78
|
+
// Both mutations are covered by real runs, not
|
|
79
|
+
// code inspection. Together they show EMPTY's
|
|
80
|
+
// assertion set is defended from multiple angles
|
|
81
|
+
// of the "collapse map={} with map=missing"
|
|
82
|
+
// failure mode — the exact regression #53 opened
|
|
83
|
+
// against.
|
|
84
|
+
// - BLIND(populated) ← 🔴 mutate ONLY StatsBar (make onlineDisplay
|
|
85
|
+
// ignore presenceUnknown and always render numeric
|
|
86
|
+
// `online`): L152 fails with
|
|
87
|
+
// `Expected: "?" Received: "0"`. This proves
|
|
88
|
+
// the '?' assertion targets StatsBar's presence-
|
|
89
|
+
// unknown branch specifically, not merely the
|
|
90
|
+
// earlier banner-visible gate.
|
|
91
|
+
|
|
92
|
+
const HUB_STATUS_ANONYMOUS = {
|
|
93
|
+
// Hub server, when called anonymously / with wrong-scope token,
|
|
94
|
+
// returns 0 sessions AND omits sse_sessions on /health.
|
|
95
|
+
sessions: [],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// PR #54 review — the two endpoints (/api/hub/status and /health) have
|
|
99
|
+
// independent auth gates, so "status returned sessions, but health's
|
|
100
|
+
// sse_sessions is missing" is a real reachable combination. This
|
|
101
|
+
// fixture exercises the StatCard populated path — a distinct render
|
|
102
|
+
// branch from the empty-fleet strip that the two anonymous fixtures
|
|
103
|
+
// above hit.
|
|
104
|
+
const HUB_STATUS_POPULATED = {
|
|
105
|
+
sessions: [
|
|
106
|
+
{ alias: 'agent-alpha', status: 'working', network_id: 'net_x' },
|
|
107
|
+
{ alias: 'agent-beta', status: 'idle', network_id: 'net_x' },
|
|
108
|
+
{ alias: 'agent-gamma', status: 'idle', network_id: 'net_x' },
|
|
109
|
+
{ alias: 'agent-delta', status: 'working', network_id: 'net_x' },
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Health payload for the BLIND fixture — omits `sse_sessions` entirely
|
|
114
|
+
// but reports live connections. That contradiction is the signal.
|
|
115
|
+
const HUB_HEALTH_BLIND = {
|
|
116
|
+
ok: true,
|
|
117
|
+
version: '0.9.0-test',
|
|
118
|
+
sse_connections: 97,
|
|
119
|
+
// Note: sse_sessions deliberately NOT included. That's the whole
|
|
120
|
+
// point of the fixture — the map is missing while conns > 0.
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Health payload for the EMPTY fixture — map is present and empty,
|
|
124
|
+
// conns is 0. Truly no nodes connected.
|
|
125
|
+
const HUB_HEALTH_EMPTY = {
|
|
126
|
+
ok: true,
|
|
127
|
+
version: '0.9.0-test',
|
|
128
|
+
sse_connections: 0,
|
|
129
|
+
sse_sessions: {},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
async function login(context: Page['context'] extends () => infer C ? C : never) {
|
|
133
|
+
const res = await context.request.post(`${BASE}/api/auth/login`, {
|
|
134
|
+
data: { password: process.env.ANET_DASHBOARD_PASSWORD || 'admin123' },
|
|
135
|
+
});
|
|
136
|
+
const setCookie = res.headers()['set-cookie'] || '';
|
|
137
|
+
const match = setCookie.match(/anet_dashboard_session=([^;]+)/);
|
|
138
|
+
if (match) {
|
|
139
|
+
const domain = new URL(BASE).hostname;
|
|
140
|
+
await context.addCookies([{
|
|
141
|
+
name: 'anet_dashboard_session',
|
|
142
|
+
value: decodeURIComponent(match[1]),
|
|
143
|
+
domain,
|
|
144
|
+
path: '/',
|
|
145
|
+
}]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function mockCommon(page: Page, statusBody: object, healthBody: object) {
|
|
150
|
+
await page.route('**/api/hub/status**', route =>
|
|
151
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(statusBody) }));
|
|
152
|
+
await page.route('**/api/hub/health', route =>
|
|
153
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(healthBody) }));
|
|
154
|
+
// Cheap stubs — HealthBanner also polls /api/hub/stats + /tasks, so
|
|
155
|
+
// return empty happy shapes to keep the amber "N failed" and green
|
|
156
|
+
// "all systems go" branches inert.
|
|
157
|
+
await page.route('**/api/hub/stats**', route =>
|
|
158
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true, tasks: { by_status: [] }, nodes: { total: 0 } }) }));
|
|
159
|
+
await page.route('**/api/hub/tasks**', route =>
|
|
160
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tasks: [], _hint: {} }) }));
|
|
161
|
+
await page.route('**/api/hub/nodes**', route =>
|
|
162
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ nodes: [] }) }));
|
|
163
|
+
await page.route('**/api/hub/messages**', route =>
|
|
164
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [] }) }));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
test.describe('#53 blind vs empty — different UIs', () => {
|
|
168
|
+
test('BLIND: sse_sessions absent + conns > 0 → alert + "?" online + NO empty-state pitch', async ({ page, context }) => {
|
|
169
|
+
await login(context);
|
|
170
|
+
await mockCommon(page, HUB_STATUS_ANONYMOUS, HUB_HEALTH_BLIND);
|
|
171
|
+
await page.goto(`${BASE}/`);
|
|
172
|
+
// Wait for the HealthBanner blind message to appear.
|
|
173
|
+
await expect(page.locator('text=/Unable to read presence/i').first()).toBeVisible({ timeout: 15000 });
|
|
174
|
+
|
|
175
|
+
const bodyText = await page.locator('body').innerText();
|
|
176
|
+
|
|
177
|
+
// The "?" online — in the empty-fleet strip (StatsBar renders the
|
|
178
|
+
// strip when total===0). Under blind, the strip reads "? online"
|
|
179
|
+
// instead of "0 online". This is the DOM signal that must NOT
|
|
180
|
+
// match the truly-empty fixture below.
|
|
181
|
+
expect(bodyText).toMatch(/\?\s+online\b/);
|
|
182
|
+
expect(bodyText).not.toMatch(/\b0\s+online\b/);
|
|
183
|
+
|
|
184
|
+
// MUST NOT pitch "Spin up your first agent" in this state.
|
|
185
|
+
expect(bodyText).not.toContain('Spin up your first agent');
|
|
186
|
+
|
|
187
|
+
// Positive: HealthBanner alert copy is present.
|
|
188
|
+
expect(bodyText.toLowerCase()).toContain('unable to read presence');
|
|
189
|
+
// Positive: the alert names the "live SSE" side of the
|
|
190
|
+
// contradiction. Reviewer note: an earlier draft also accepted
|
|
191
|
+
// /97/, but that digit can incidentally match other numbers on
|
|
192
|
+
// the page (versions, other counters) — tightening to the phrase
|
|
193
|
+
// that only the banner uses.
|
|
194
|
+
expect(bodyText).toMatch(/live SSE/i);
|
|
195
|
+
|
|
196
|
+
await page.screenshot({ path: 'test-results/e2e-53-blind.png', fullPage: false });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('BLIND (populated fleet): StatCard grid renders "?" for Online + "presence unavailable" subtitle', async ({ page, context }) => {
|
|
200
|
+
await login(context);
|
|
201
|
+
await mockCommon(page, HUB_STATUS_POPULATED, HUB_HEALTH_BLIND);
|
|
202
|
+
await page.goto(`${BASE}/`);
|
|
203
|
+
await expect(page.locator('text=/Unable to read presence/i').first()).toBeVisible({ timeout: 15000 });
|
|
204
|
+
await page.waitForTimeout(1500);
|
|
205
|
+
|
|
206
|
+
const bodyText = await page.locator('body').innerText();
|
|
207
|
+
|
|
208
|
+
// Positive proof the StatCard grid actually rendered — otherwise
|
|
209
|
+
// "Online cell isn't a number" would spuriously pass just because
|
|
210
|
+
// the whole grid failed to mount. Assert we see at least one of
|
|
211
|
+
// the fixture aliases, which appear in the AgentCard grid below
|
|
212
|
+
// the StatsBar.
|
|
213
|
+
expect(bodyText).toContain('agent-alpha');
|
|
214
|
+
|
|
215
|
+
// The StatCard "Online" cell — under populated + blind, the value
|
|
216
|
+
// is '?' and the subtitle drops to 'presence unavailable' (both
|
|
217
|
+
// set by StatsBar via the presenceUnknown prop). This is the
|
|
218
|
+
// primary reachable path in real traffic: hub returned sessions
|
|
219
|
+
// (status endpoint auth held), health omitted sse_sessions (its
|
|
220
|
+
// own auth gate tripped). Two independent auth gates, one
|
|
221
|
+
// rendering combination.
|
|
222
|
+
const onlineCardValue = await page.getByText('Online', { exact: true }).first()
|
|
223
|
+
.evaluate(el => el.previousElementSibling?.textContent ?? null);
|
|
224
|
+
expect(onlineCardValue?.trim()).toBe('?');
|
|
225
|
+
|
|
226
|
+
// Subtitle underneath the card.
|
|
227
|
+
expect(bodyText.toLowerCase()).toContain('presence unavailable');
|
|
228
|
+
|
|
229
|
+
// Empty-state pitch must NOT appear even though total > 0 (it's
|
|
230
|
+
// gated on sessions.length === 0), but assert it anyway — the
|
|
231
|
+
// gate condition on presenceState covers a different path and we
|
|
232
|
+
// want either coverage to catch a regression.
|
|
233
|
+
expect(bodyText).not.toContain('Spin up your first agent');
|
|
234
|
+
|
|
235
|
+
await page.screenshot({ path: 'test-results/e2e-53-populated-blind.png', fullPage: false });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('EMPTY: sse_sessions = {} + conns = 0 → NO presence-blind alert + numeric 0 online', async ({ page, context }) => {
|
|
239
|
+
await login(context);
|
|
240
|
+
await mockCommon(page, HUB_STATUS_ANONYMOUS, HUB_HEALTH_EMPTY);
|
|
241
|
+
await page.goto(`${BASE}/`);
|
|
242
|
+
// Wait for the page to settle. Something needs to render — pick a
|
|
243
|
+
// stable, non-count anchor.
|
|
244
|
+
await expect(page.locator('h1', { hasText: 'Agent Network' }).first()).toBeVisible({ timeout: 15000 });
|
|
245
|
+
await page.waitForTimeout(1500);
|
|
246
|
+
|
|
247
|
+
const bodyText = await page.locator('body').innerText();
|
|
248
|
+
|
|
249
|
+
// The presence-blind alert MUST NOT fire when the map is truly
|
|
250
|
+
// present-and-empty — that would be a false alarm the other way.
|
|
251
|
+
expect(bodyText.toLowerCase()).not.toContain('unable to read presence');
|
|
252
|
+
|
|
253
|
+
// In truly-empty state, presenceStatus === 'ready', so the online
|
|
254
|
+
// cell is a real number (0). Confirm it's NOT "?".
|
|
255
|
+
// (Empty-fleet path uses a thin strip, not the StatCard grid, so
|
|
256
|
+
// we grep the strip text.)
|
|
257
|
+
expect(bodyText).toMatch(/\b0\s+online\b/i);
|
|
258
|
+
expect(bodyText).not.toContain('presence unavailable');
|
|
259
|
+
|
|
260
|
+
await page.screenshot({ path: 'test-results/e2e-53-empty.png', fullPage: false });
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { presenceStatus } from '../app/lib/presence';
|
|
3
|
+
|
|
4
|
+
// #53 — three-state ambient presence. Companion to the
|
|
5
|
+
// e2e-53-blind-vs-empty DOM test: this one nails down the predicate
|
|
6
|
+
// contract every consumer inherits from presenceStatus().
|
|
7
|
+
//
|
|
8
|
+
// Two hand-computed fixtures cover the two states that must NEVER
|
|
9
|
+
// collapse to the same UI:
|
|
10
|
+
// - sseSessions === undefined & health.sse_connections > 0 → 'blind'
|
|
11
|
+
// (hub says there are live SSE, but the auth-gated map is missing
|
|
12
|
+
// from THIS response — anonymous / wrong scope; must NOT render 0)
|
|
13
|
+
// - sseSessions === {} & health.sse_connections = 0 → 'ready'
|
|
14
|
+
// (map is present and truly empty; safe to render 0 online)
|
|
15
|
+
//
|
|
16
|
+
// Also:
|
|
17
|
+
// - sseSessions undefined & no health → 'loading' (boot)
|
|
18
|
+
// - sseSessions undefined & health.sse_connections = 0 → 'loading'
|
|
19
|
+
// (conservative — both look identical to us and neither should
|
|
20
|
+
// inflate)
|
|
21
|
+
|
|
22
|
+
test.describe('#53 presenceStatus — undefined vs empty must NOT collapse', () => {
|
|
23
|
+
test('sseSessions absent + hub reports live connections → blind', () => {
|
|
24
|
+
expect(presenceStatus(undefined, { sse_connections: 97 })).toBe('blind');
|
|
25
|
+
expect(presenceStatus(undefined, { sse_connections: 1 })).toBe('blind');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('sseSessions absent + hub reports 0 connections → loading (conservative)', () => {
|
|
29
|
+
// We can't tell "no map + 0 conns" from "hub-connection is stale on
|
|
30
|
+
// its own reporting" — treat as loading and don't render "0 online".
|
|
31
|
+
expect(presenceStatus(undefined, { sse_connections: 0 })).toBe('loading');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('sseSessions absent + no health at all → loading (boot)', () => {
|
|
35
|
+
expect(presenceStatus(undefined, null)).toBe('loading');
|
|
36
|
+
expect(presenceStatus(undefined, undefined)).toBe('loading');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('sseSessions present (even empty) → ready — trust the count', () => {
|
|
40
|
+
// The whole point of #53: "map is {} " means "the caller CAN see
|
|
41
|
+
// presence, and there's nothing to see". That is a DIFFERENT
|
|
42
|
+
// condition from "map is missing entirely" and must render
|
|
43
|
+
// differently.
|
|
44
|
+
expect(presenceStatus({}, { sse_connections: 0 })).toBe('ready');
|
|
45
|
+
expect(presenceStatus({ 'net_a:one': 1 }, { sse_connections: 1 })).toBe('ready');
|
|
46
|
+
// Even a self-contradictory response (map says 0 but conns > 0)
|
|
47
|
+
// is still 'ready' at the map level — the map is authoritative
|
|
48
|
+
// for who's online; the count-vs-map mismatch is a hub-side bug,
|
|
49
|
+
// not a presence-blindness signal.
|
|
50
|
+
expect(presenceStatus({}, { sse_connections: 5 })).toBe('ready');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../dash-492/agent-network-dashboard/app/components/utils.ts","../../../../../../dash-492/agent-network-dashboard/app/components/AliasAvatar.tsx","../../../../../../dash-492/agent-network-dashboard/app/lib/avatars.ts","../../../../../../dash-492/agent-network-dashboard/app/components/EmptyState.tsx","../../../../../../dash-492/agent-network-dashboard/app/lib/status.ts","../../../../../../dash-492/agent-network-dashboard/app/components/CollapsibleSearch.tsx"],"sourcesContent":["/** Round 82: strip the most common markdown markup from agent-to-agent\n * task / message bodies so single-line table previews read cleanly.\n * Full original content stays in the expanded row / drawer / title=. */\nexport function previewContent(raw: string | null | undefined): string {\n if (!raw) return '--';\n return raw\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1') // images:  → alt\n .replace(/\\[([^\\]]+)\\]\\([^)]*\\)/g, '$1') // links: [text](href) → text\n .replace(/`([^`]+)`/g, '$1') // inline code: `x` → x\n .replace(/\\s+/g, ' ') // collapse whitespace\n .trim() || '--';\n}\n\n// Round 44 / Loop: timeAgo now delegates to the shared lib/time helper.\n// The old in-line `replace + 'Z'` parse appended a Z to already-ISO\n// inputs (producing \"…ZZ\", browser-fragile) — strict-superset semantics\n// preserved for SQL-style inputs, fixed for ISO. Plus a 'just now' fallback\n// when clock skew puts the timestamp in the future. Five callers benefit:\n// /admin · /logs · /settings/networks · InboxPanel · /messages.\nimport { relativeAgo } from '../lib/time';\nexport function timeAgo(dateStr: string): string {\n return relativeAgo(dateStr) ?? '--';\n}\n\nexport function statusColor(status: string, hasSse: boolean): string {\n if (hasSse && status === 'working') return 'bg-green-500';\n if (hasSse && status === 'idle') return 'bg-emerald-400';\n if (hasSse) return 'bg-blue-400';\n if (status === 'offline') return 'bg-gray-500';\n return 'bg-yellow-400';\n}\n","'use client';\n\nimport { useState } from 'react';\nimport { useAvatarUrl } from '../lib/avatars';\n\n/** Hue-hashed avatar pill for agent aliases. Same alias → same color across\n * every page (Messages, Nodes, TopoGraph). Use the `size` prop for inline\n * pills (16/20) vs card headers (28). Round 21 introduced the palette;\n * round 22 promotes it from app/messages to a shared component. */\n\nconst AVATAR_HUES = [180, 200, 220, 270, 300, 330, 30, 90];\n\nexport function aliasAvatarColors(alias: string): { bg: string; ring: string; text: string } {\n let h = 0;\n for (let i = 0; i < alias.length; i++) h = (h * 31 + alias.charCodeAt(i)) >>> 0;\n const hue = AVATAR_HUES[h % AVATAR_HUES.length];\n return {\n bg: `hsl(${hue} 55% 22%)`,\n ring: `hsl(${hue} 60% 45%)`,\n text: `hsl(${hue} 80% 78%)`,\n };\n}\n\nexport function aliasInitial(alias?: string): string {\n if (!alias) return '·';\n const ch = alias.trim().match(/[\\p{L}\\p{N}]/u)?.[0] || alias.trim()[0] || '·';\n return ch.toUpperCase();\n}\n\nfunction isGrokAlias(alias: string) {\n return /\\bgrok\\b|grok-build|grok测试员|grok-demo/i.test(alias);\n}\n\ninterface AliasAvatarProps {\n alias: string;\n size?: number;\n className?: string;\n}\n\nexport function AliasAvatar({ alias, size = 28, className = '' }: AliasAvatarProps) {\n // R24 (Vincent: 头像换血): a custom image (user-set or designed default)\n // wins over every generated style — ONE seam, all 13 avatar surfaces.\n // Shimmer while loading, silent fallback to the pill on error.\n const url = useAvatarUrl(alias);\n const [imgState, setImgState] = useState<'loading' | 'ready' | 'error'>('loading');\n // Reset when the url changes — React's endorsed adjust-during-render\n // pattern (no effect, no extra commit).\n const [prevUrl, setPrevUrl] = useState(url);\n if (url !== prevUrl) { setPrevUrl(url); setImgState('loading'); }\n if (url && imgState !== 'error') {\n return (\n <span\n className={`anet-alias-avatar relative inline-flex shrink-0 overflow-hidden rounded-full border border-[var(--border)] ${imgState === 'loading' ? 'animate-pulse bg-[var(--bg-elevated)]' : ''} ${className}`}\n style={{ width: size, height: size }}\n title={alias}\n aria-hidden\n >\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={url}\n alt=\"\"\n loading=\"lazy\"\n onLoad={() => setImgState('ready')}\n onError={() => setImgState('error')}\n className={`h-full w-full rounded-full object-cover transition-opacity duration-150 ${imgState === 'ready' ? 'opacity-100' : 'opacity-0'}`}\n />\n </span>\n );\n }\n if (isGrokAlias(alias)) {\n return (\n <span\n className={`anet-alias-avatar inline-flex items-center justify-center rounded-full border border-emerald-500/45 bg-emerald-950/70 shrink-0 ${className}`}\n style={{\n width: size,\n height: size,\n backgroundImage: 'url(/vendors/grok.svg)',\n backgroundPosition: 'center',\n backgroundRepeat: 'no-repeat',\n backgroundSize: '68% 68%',\n }}\n title={alias}\n aria-hidden\n />\n );\n }\n\n const c = aliasAvatarColors(alias);\n const fs = Math.max(9, Math.round(size * 0.42));\n return (\n <span\n className={`anet-alias-avatar inline-flex items-center justify-center rounded-full border shrink-0 font-semibold ${className}`}\n style={{\n width: size,\n height: size,\n fontSize: fs,\n backgroundColor: c.bg,\n borderColor: c.ring,\n color: c.text,\n }}\n title={alias}\n aria-hidden\n >\n {aliasInitial(alias)}\n </span>\n );\n}\n","'use client';\n\nimport { useSyncExternalStore } from 'react';\n\n/* Loop R24 (Vincent 亲点: 头像换血) — custom avatar infrastructure.\n *\n * Resolution order, per alias:\n * 1. user override — localStorage map, settable in the node settings UI\n * 2. designed default — /avatars/manifest.json (static, alias → image URL);\n * the design-side illustration set lands there later with ZERO code\n * change (drop files under public/avatars/ + list them in the manifest)\n * 3. shared pool — manifest \"_pool\": [\"/avatars/…\", …]; aliases without an\n * explicit entry get a stable hash-picked illustration from the pool\n * 4. '' → caller falls back to the hue-hashed initial pill\n *\n * Hub-side persistence (cross-device) needs an RFC-024 whitelist extension\n * (`avatar_url` is not an editable flag today — update would return\n * invalid_patch); when the hub grows it, hydrate the store from the hub here\n * without touching any consumer.\n */\n\nconst STORE_KEY = 'anet_avatars_v1';\nconst AVATAR_EVENT = 'anet-avatars-updated';\n\nlet manifest: Record<string, string> | null = null;\nlet pool: string[] = [];\nlet manifestRequested = false;\n\n/* Stable per-alias index so a given agent keeps the same pool avatar across\n * sessions/devices (djb2 over code units — no Math.random, no ordering). */\nfunction poolPick(alias: string): string {\n if (!pool.length) return '';\n let h = 5381;\n for (let i = 0; i < alias.length; i++) h = ((h << 5) + h + alias.charCodeAt(i)) >>> 0;\n return pool[h % pool.length];\n}\n\nfunction fetchManifestOnce() {\n if (manifestRequested || typeof window === 'undefined') return;\n manifestRequested = true;\n fetch('/avatars/manifest.json', { cache: 'no-cache' })\n .then((r) => (r.ok ? r.json() : null))\n .then((j) => {\n if (j && typeof j === 'object' && !Array.isArray(j)) {\n const clean: Record<string, string> = {};\n for (const [k, v] of Object.entries(j as Record<string, unknown>)) {\n if (k.startsWith('_')) continue; // meta keys (\"_pool\", …)\n if (typeof v === 'string' && v.startsWith('/')) clean[k] = v;\n }\n const rawPool = (j as Record<string, unknown>)._pool;\n pool = Array.isArray(rawPool)\n ? rawPool.filter((p): p is string => typeof p === 'string' && p.startsWith('/'))\n : [];\n manifest = clean;\n window.dispatchEvent(new Event(AVATAR_EVENT));\n }\n })\n .catch(() => {});\n}\n\nfunction readMap(): Record<string, string> {\n if (typeof window === 'undefined') return {};\n try {\n const parsed = JSON.parse(window.localStorage.getItem(STORE_KEY) || '{}');\n return parsed && typeof parsed === 'object' ? parsed : {};\n } catch {\n return {};\n }\n}\n\nexport function getAvatarUrl(alias?: string | null): string {\n if (!alias) return '';\n return readMap()[alias] || manifest?.[alias] || poolPick(alias) || '';\n}\n\n/** Empty/whitespace url removes the override (falls back to manifest/pill). */\nexport function setAvatarUrl(alias: string, url: string) {\n if (typeof window === 'undefined' || !alias) return;\n try {\n const m = readMap();\n if (url.trim()) m[alias] = url.trim();\n else delete m[alias];\n window.localStorage.setItem(STORE_KEY, JSON.stringify(m));\n window.dispatchEvent(new Event(AVATAR_EVENT));\n } catch {}\n}\n\nlet version = 0;\nfunction subscribe(callback: () => void) {\n fetchManifestOnce();\n const bump = () => { version++; callback(); };\n window.addEventListener(AVATAR_EVENT, bump);\n window.addEventListener('storage', bump);\n return () => {\n window.removeEventListener(AVATAR_EVENT, bump);\n window.removeEventListener('storage', bump);\n };\n}\n\n/** Monotonic version for callers that resolve many aliases via plain\n * getAvatarUrl() inside render loops (map callbacks can't call hooks —\n * same idiom as chat-mute's useMuteVersion). Subscribing also kicks off\n * the lazy manifest fetch and re-renders the caller when it lands. */\nexport function useAvatarsVersion(): number {\n return useSyncExternalStore(subscribe, () => version, () => 0);\n}\n\nexport function useAvatarUrl(alias?: string | null): string {\n return useSyncExternalStore(subscribe, () => getAvatarUrl(alias), () => '');\n}\n","'use client';\n\nimport { useState } from 'react';\nimport Link from 'next/link';\n\nexport type EmptyVariant = 'nodes' | 'tasks' | 'messages' | 'logs' | 'tokens' | 'networks' | 'generic';\n\ninterface EmptyStateProps {\n /** Picks the icon glyph and default copy if title/sub omitted. */\n variant?: EmptyVariant;\n /** Optional override headline. */\n title?: string;\n /** Optional override sub-copy. */\n sub?: string;\n /** Optional CTA: text + href (internal) or onClick. */\n cta?: { label: string; href?: string; onClick?: () => void };\n /** Compact mode for in-card empty states (smaller padding). */\n compact?: boolean;\n}\n\nconst DEFAULTS: Record<EmptyVariant, { title: string; sub: string }> = {\n nodes: { title: 'No agents in this network', sub: 'Agent sessions will appear here once they connect to the CommHub.' },\n tasks: { title: 'No tasks yet', sub: 'Tasks will appear here when agents send them via CommHub.' },\n messages: { title: 'No messages', sub: 'Messages between agents will appear here.' },\n logs: { title: 'No audit logs', sub: 'Events will appear here when users register, login, or perform actions.' },\n tokens: { title: 'No API tokens', sub: 'Create one to authenticate CLI tools and external integrations.' },\n networks: { title: 'No networks found', sub: 'Create one or sign in with V3 auth to see your networks.' },\n generic: { title: 'Nothing here yet', sub: 'Data will appear here once available.' },\n};\n\n/**\n * Minimal monochrome SVG glyphs per variant. No filled shapes, no gradients,\n * no AI-decoration — just thin-stroke line art that fades into the page.\n * 64×64 viewBox; rendered at 56×56 (compact 40×40).\n */\nfunction Glyph({ variant, size }: { variant: EmptyVariant; size: number }) {\n const s = { width: size, height: size };\n const baseProps = {\n fill: 'none',\n stroke: 'currentColor',\n strokeWidth: 1.25,\n strokeLinecap: 'round' as const,\n strokeLinejoin: 'round' as const,\n };\n\n switch (variant) {\n case 'nodes':\n // Mesh with dashed edges = \"nodes will land here\"\n return (\n <svg viewBox=\"0 0 64 64\" {...s}>\n <g {...baseProps}>\n <circle cx=\"32\" cy=\"14\" r=\"4\" />\n <circle cx=\"14\" cy=\"44\" r=\"4\" />\n <circle cx=\"50\" cy=\"44\" r=\"4\" />\n <line x1=\"32\" y1=\"18\" x2=\"14\" y2=\"40\" strokeDasharray=\"3 3\" opacity=\"0.6\" />\n <line x1=\"32\" y1=\"18\" x2=\"50\" y2=\"40\" strokeDasharray=\"3 3\" opacity=\"0.6\" />\n <line x1=\"18\" y1=\"44\" x2=\"46\" y2=\"44\" strokeDasharray=\"3 3\" opacity=\"0.6\" />\n </g>\n </svg>\n );\n case 'tasks':\n // Empty checkbox list\n return (\n <svg viewBox=\"0 0 64 64\" {...s}>\n <g {...baseProps}>\n <rect x=\"14\" y=\"14\" width=\"36\" height=\"6\" rx=\"1.5\" />\n <rect x=\"14\" y=\"26\" width=\"36\" height=\"6\" rx=\"1.5\" opacity=\"0.6\" />\n <rect x=\"14\" y=\"38\" width=\"36\" height=\"6\" rx=\"1.5\" opacity=\"0.35\" />\n </g>\n </svg>\n );\n case 'messages':\n // Speech bubble outline\n return (\n <svg viewBox=\"0 0 64 64\" {...s}>\n <g {...baseProps}>\n <path d=\"M14 18 h36 a3 3 0 0 1 3 3 v18 a3 3 0 0 1 -3 3 h-18 l-8 6 v-6 h-10 a3 3 0 0 1 -3 -3 v-18 a3 3 0 0 1 3 -3 z\" />\n <line x1=\"22\" y1=\"28\" x2=\"42\" y2=\"28\" opacity=\"0.5\" />\n <line x1=\"22\" y1=\"34\" x2=\"36\" y2=\"34\" opacity=\"0.5\" />\n </g>\n </svg>\n );\n case 'logs':\n // Document with lines\n return (\n <svg viewBox=\"0 0 64 64\" {...s}>\n <g {...baseProps}>\n <path d=\"M18 12 h22 l8 8 v32 a2 2 0 0 1 -2 2 h-28 a2 2 0 0 1 -2 -2 v-38 a2 2 0 0 1 2 -2 z\" />\n <path d=\"M40 12 v8 h8\" opacity=\"0.6\" />\n <line x1=\"24\" y1=\"32\" x2=\"40\" y2=\"32\" opacity=\"0.55\" />\n <line x1=\"24\" y1=\"38\" x2=\"40\" y2=\"38\" opacity=\"0.4\" />\n <line x1=\"24\" y1=\"44\" x2=\"34\" y2=\"44\" opacity=\"0.25\" />\n </g>\n </svg>\n );\n case 'tokens':\n // Key outline\n return (\n <svg viewBox=\"0 0 64 64\" {...s}>\n <g {...baseProps}>\n <circle cx=\"22\" cy=\"32\" r=\"8\" />\n <line x1=\"30\" y1=\"32\" x2=\"54\" y2=\"32\" />\n <line x1=\"44\" y1=\"32\" x2=\"44\" y2=\"38\" />\n <line x1=\"50\" y1=\"32\" x2=\"50\" y2=\"40\" />\n </g>\n </svg>\n );\n case 'networks':\n // Globe-ish concentric ovals\n return (\n <svg viewBox=\"0 0 64 64\" {...s}>\n <g {...baseProps}>\n <circle cx=\"32\" cy=\"32\" r=\"18\" />\n <ellipse cx=\"32\" cy=\"32\" rx=\"18\" ry=\"9\" opacity=\"0.55\" />\n <line x1=\"14\" y1=\"32\" x2=\"50\" y2=\"32\" opacity=\"0.55\" />\n <line x1=\"32\" y1=\"14\" x2=\"32\" y2=\"50\" opacity=\"0.55\" />\n </g>\n </svg>\n );\n case 'generic':\n default:\n // Soft sparkle outline\n return (\n <svg viewBox=\"0 0 64 64\" {...s}>\n <g {...baseProps}>\n <circle cx=\"32\" cy=\"32\" r=\"14\" strokeDasharray=\"3 3\" opacity=\"0.6\" />\n <circle cx=\"32\" cy=\"32\" r=\"3\" />\n </g>\n </svg>\n );\n }\n}\n\nexport function EmptyState({ variant = 'generic', title, sub, cta, compact = false }: EmptyStateProps) {\n const d = DEFAULTS[variant];\n const headline = title ?? d.title;\n const subcopy = sub ?? d.sub;\n const iconSize = compact ? 40 : 56;\n\n return (\n <div className={`text-center ${compact ? 'py-8' : 'py-16'} px-4`} role=\"status\">\n <div className=\"anet-empty-glyph inline-flex items-center justify-center mb-4 text-gray-500\" aria-hidden>\n <Glyph variant={variant} size={iconSize} />\n </div>\n <h3 className={`font-medium text-gray-300 ${compact ? 'text-sm' : 'text-base'}`}>{headline}</h3>\n {subcopy && (\n <p className={`text-gray-500 ${compact ? 'text-xs mt-1.5' : 'text-sm mt-2'} max-w-md mx-auto leading-relaxed`}>\n {subcopy}\n </p>\n )}\n {cta && (\n <div className=\"mt-4\">\n {cta.href ? (\n <Link href={cta.href} className=\"inline-flex items-center gap-1.5 text-sm font-medium text-cyan-400 hover:text-cyan-300\">\n {cta.label}\n <span aria-hidden>→</span>\n </Link>\n ) : (\n <button onClick={cta.onClick} className=\"inline-flex items-center gap-1.5 text-sm font-medium text-cyan-400 hover:text-cyan-300\">\n {cta.label}\n <span aria-hidden>→</span>\n </button>\n )}\n </div>\n )}\n </div>\n );\n}\n\n/** Round 105 (issue #90): centered card shell so the Overview empty state\n * reads as an intentional, layout-aligned card instead of bare text\n * floating in the content column. */\nfunction EmptyCard({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"max-w-2xl mx-auto rounded-xl border border-[#26262b] bg-[#161618] shadow-lg shadow-black/20\">\n {children}\n </div>\n );\n}\n\n/**\n * Overview-specific variant. Three cases:\n * 1. network-mismatch — agents exist globally, none in this network\n * 2. agents-offline — 0 registered now BUT task history exists, so this\n * is NOT a first run (round 105 / issue #90: showing \"Spin up your\n * first agent\" next to \"acked: 499\" was misleading)\n * 3. true first-run — 0 agents, 0 history → quickstart command\n */\nexport function NodesEmptyState({\n hint,\n taskHistoryCount = 0,\n}: {\n hint?: { global_count?: number; filtered_network?: string };\n taskHistoryCount?: number;\n}) {\n if (hint?.global_count) {\n return (\n <EmptyCard>\n <EmptyState\n variant=\"nodes\"\n title=\"No agents in this network\"\n sub={`Server has ${hint.global_count} nodes globally, but none are registered to the current network. Switch network or contact admin.`}\n />\n </EmptyCard>\n );\n }\n\n // Round 105 (issue #90): there's task history but no agents online right\n // now — they finished or disconnected. Don't pitch a first-run setup;\n // point at the history instead.\n if (taskHistoryCount > 0) {\n return (\n <EmptyCard>\n <EmptyState\n variant=\"nodes\"\n title=\"No agents online\"\n sub={`Every agent in this network is currently offline. ${taskHistoryCount.toLocaleString()} task${taskHistoryCount === 1 ? '' : 's'} in history — they may have finished their work or disconnected.`}\n cta={{ label: 'View task history', href: '/tasks' }}\n />\n </EmptyCard>\n );\n }\n\n // Round 52: true empty state (zero agents anywhere) — show the\n // quickstart command inline so users don't have to leave the dashboard\n // to figure out how to spin up their first agent.\n return (\n <EmptyCard>\n <div className=\"text-center py-16 px-4\" role=\"status\">\n <div className=\"anet-empty-glyph inline-flex items-center justify-center mb-4 text-gray-500\" aria-hidden>\n <svg viewBox=\"0 0 64 64\" width={56} height={56}>\n <g stroke=\"currentColor\" strokeWidth=\"1.5\" fill=\"none\">\n <rect x=\"10\" y=\"20\" width=\"44\" height=\"28\" rx=\"2\" />\n <rect x=\"20\" y=\"32\" width=\"6\" height=\"8\" opacity=\"0.5\" />\n <rect x=\"32\" y=\"32\" width=\"6\" height=\"8\" opacity=\"0.5\" />\n <rect x=\"44\" y=\"32\" width=\"4\" height=\"8\" opacity=\"0.5\" />\n <line x1=\"10\" y1=\"26\" x2=\"54\" y2=\"26\" opacity=\"0.4\" />\n <circle cx=\"14\" cy=\"23\" r=\"0.8\" fill=\"currentColor\" />\n </g>\n </svg>\n </div>\n <h3 className=\"font-medium text-gray-300 text-base\">Spin up your first agent</h3>\n <p className=\"text-gray-500 text-sm mt-2 max-w-md mx-auto leading-relaxed\">\n Run this in a fresh terminal to register an agent with this CommHub:\n </p>\n <div className=\"mt-4 inline-block\">\n <QuickstartCommand cmd=\"npx --yes @sleep2agi/agent-network init\" />\n </div>\n {/* #214 F6: expectation management — without this line, users\n copy the command and stare at an empty screen not knowing\n what to wait for. The page polls every 5s, so the node\n really does appear by itself. */}\n <p className=\"text-gray-600 text-xs mt-3 max-w-md mx-auto\">\n Once the agent connects, it appears here automatically within a\n few seconds — no refresh needed.\n </p>\n <div className=\"mt-3\">\n <a\n href=\"https://anet.sh\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 text-xs font-medium text-gray-500 hover:text-cyan-300\"\n >\n Full quickstart guide\n <span aria-hidden>→</span>\n </a>\n </div>\n </div>\n </EmptyCard>\n );\n}\n\n/** Code block + inline copy button used by the empty-Overview first-run\n * CTA. State lives here so the parent stays stateless. */\nfunction QuickstartCommand({ cmd }: { cmd: string }) {\n const [copied, setCopied] = useState(false);\n const onCopy = async () => {\n try {\n await navigator.clipboard.writeText(cmd);\n setCopied(true);\n setTimeout(() => setCopied(false), 1800);\n } catch {}\n };\n return (\n <div className=\"anet-empty-cmd flex items-start sm:items-center gap-2 bg-[#0e0e10] border border-[#26262b] rounded-lg pl-4 pr-1.5 py-1.5 text-xs sm:text-sm\">\n {/* #209 R44: long quickstart commands (e.g. the `npm install -g …`\n variant) overflowed the empty-state card horizontally on phones\n because <code> defaults to white-space:pre. break-all on mobile\n lets them wrap inside the box; sm: up restores normal wrapping\n so desktop monospace lines stay clean. items-start on phones\n aligns the Copy button to the top so a wrapped 2-line command\n doesn't bottom-anchor the button. */}\n <code className=\"text-cyan-300 font-mono select-all min-w-0 break-all sm:break-normal\">{cmd}</code>\n <button\n type=\"button\"\n onClick={onCopy}\n aria-label={copied ? 'Copied' : 'Copy command'}\n className=\"shrink-0 rounded-md px-2 py-1.5 text-[11px] text-gray-500 hover:text-gray-200 hover:bg-[#1c1c1f] transition-colors\"\n >\n {copied ? (\n <span className=\"flex items-center gap-1 text-green-400\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d=\"M20 6 9 17l-5-5\" />\n </svg>\n Copied\n </span>\n ) : (\n <span className=\"flex items-center gap-1\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\" />\n <path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\" />\n </svg>\n Copy\n </span>\n )}\n </button>\n </div>\n );\n}\n","/** Single source of truth for task status colors. Both `/tasks` (chip\n * strip, badge, distribution bar) and the Overview Recent Activity row\n * used to ship their own incomplete maps; round 66 consolidates them\n * here so adding a new status updates every consumer at once.\n *\n * Order in `TASK_STATUSES` is chronological-ish: lifecycle progress on\n * the left, terminal-good (`closed`) in the middle, terminal-bad\n * (`failed`/`cancelled`/`expired`) on the right. */\n\nexport const TASK_STATUSES = [\n 'created',\n 'delivered',\n 'acked',\n 'running',\n 'replied',\n 'closed',\n 'failed',\n 'cancelled',\n 'expired',\n] as const;\n\n/** Pill / chip background+text+border. Tailwind classes, not inlined hex,\n * because chips are static class names safe from purge. */\n/** #217 D3 (OpenWebUI-style color restraint): the 9-hue rainbow is\n * collapsed to a semantic triad — green = actively running, red =\n * failed, gray = everything else (in-flight states in lighter gray,\n * terminal states dimmer). Color now means something instead of\n * decorating every enum value. */\nexport const STATUS_CHIP_CLASS: Record<string, string> = {\n created: 'bg-gray-500/10 text-gray-400 border-gray-500/20',\n delivered: 'bg-gray-500/10 text-gray-300 border-gray-500/20',\n acked: 'bg-gray-500/10 text-gray-300 border-gray-500/20',\n running: 'bg-green-500/10 text-green-300 border-green-500/20',\n replied: 'bg-gray-500/10 text-gray-300 border-gray-500/20',\n closed: 'bg-gray-500/10 text-gray-500 border-gray-500/20',\n failed: 'bg-red-500/10 text-red-300 border-red-500/20',\n cancelled: 'bg-gray-500/10 text-gray-500 border-gray-500/20',\n expired: 'bg-gray-500/10 text-gray-500 border-gray-500/20',\n};\n\n/** Inline hex dots — used wherever Tailwind would purge dynamic class\n * names (`bg-${family}-400`). Style attribute carries the color. */\nexport const STATUS_DOT_HEX: Record<string, string> = {\n created: '#9ca3af',\n delivered: '#9ca3af',\n acked: '#9ca3af',\n running: '#4ade80',\n replied: '#9ca3af',\n closed: '#6b7280',\n failed: '#f87171',\n cancelled: '#6b7280',\n expired: '#6b7280',\n};\n\n/** Session lifecycle (distinct from task lifecycle above). Shared by\n * /nodes status pills and /admin Online Sessions row chips so an\n * agent in `blocked` state reads the same color everywhere.\n * Round 91 — extracted from app/nodes/page.tsx. */\n/** D3: idle drops from blue to gray — only working (green), blocked\n * (amber, actionable) and error (red) earn color. */\nexport const SESSION_STATUS_CHIP_CLASS: Record<string, string> = {\n working: 'bg-green-500/10 text-green-300 border-green-500/20',\n idle: 'bg-gray-500/10 text-gray-300 border-gray-500/20',\n blocked: 'bg-yellow-500/10 text-yellow-300 border-yellow-500/20',\n error: 'bg-red-500/10 text-red-300 border-red-500/20',\n offline: 'bg-gray-500/10 text-gray-500 border-gray-500/20',\n};\n\n/** Text-only color for session status. Used where a chip background\n * would be too heavy — e.g. /node detail header status label. */\nexport const SESSION_STATUS_TEXT_CLASS: Record<string, string> = {\n working: 'text-green-400',\n idle: 'text-gray-400',\n blocked: 'text-yellow-400',\n error: 'text-red-400',\n offline: 'text-gray-500',\n};\n\n/** Solid bar segment background for the Tasks distribution bar. */\nexport const STATUS_BAR_CLASS: Record<string, string> = {\n created: 'bg-gray-500',\n delivered: 'bg-gray-400',\n acked: 'bg-gray-300',\n running: 'bg-green-500',\n replied: 'bg-gray-400',\n closed: 'bg-gray-600',\n failed: 'bg-red-500',\n cancelled: 'bg-gray-600',\n expired: 'bg-gray-600',\n};\n","'use client';\n\n/**\n * CollapsibleSearch\n * ──────────────────\n * #209 R34 — Reusable WeChat-style search-toggle pattern.\n *\n * Two pieces of UI from one component:\n * 1. A magnifier button (`w-9 h-9 rounded-full`) that lives in a page\n * header — render it via `Button`.\n * 2. A collapsible search row that reveals below the header on open —\n * render it via `Row`.\n *\n * Used together they replicate WeChat's chat-list search affordance:\n * the search field is hidden by default behind a small circle at the\n * top-right of the page header. Tapping it slides the input open,\n * autofocuses, and lets Escape clear + close. A cyan dot appears in\n * the corner of the icon when there is an active search term but the\n * row is collapsed, so the user always knows a filter is in effect.\n *\n * Owner of the search string is the parent (this component is\n * uncontrolled w.r.t. value — pure UI). That keeps the filter logic\n * with the page so it's easy to plumb additional behaviour like\n * `from:alias` shortcuts or highlight rendering.\n *\n * Shipped as a single component with two named children so callers\n * can place `<Button>` inside their existing header flex row and\n * `<Row>` just after, without restructuring around a tighter API.\n *\n * First adopters: /nodes (R32), /messages (R33). R34 ships\n * /nodes + /messages migrated to use this component; future pages\n * can add search by importing it instead of copy-pasting the JSX.\n */\n\nimport { useState, useCallback, useRef } from 'react';\n\ninterface CollapsibleSearchAPI {\n /** Round magnifier button. Place in your page header (e.g. right edge). */\n Button: () => React.JSX.Element;\n /** Collapsible search input row. Place immediately after the header. */\n Row: () => React.JSX.Element | null;\n /** Current search value — pass to your filter logic. */\n value: string;\n /** Programmatically clear + close (useful for \"no results\" reset link). */\n reset: () => void;\n}\n\ninterface UseCollapsibleSearchProps {\n /** Controlled value + setter from the parent — owns the search string. */\n value: string;\n onChange: (next: string) => void;\n /** Input placeholder. */\n placeholder?: string;\n /** Accessible label for the button (also drives `title=`). */\n label?: string;\n /** Hide everything when false (e.g. when there is nothing to search). */\n enabled?: boolean;\n}\n\nexport function useCollapsibleSearch(props: UseCollapsibleSearchProps): CollapsibleSearchAPI {\n const { value } = props;\n const [open, setOpen] = useState(false);\n\n /* Loop R16 (R14 同类根因扫查): Button/Row were useCallback-RECREATED on\n every open/value change. Callers render them as JSX types\n (<ctl.Row />), so each keystroke produced a new component type →\n React unmounted and remounted the <input> mid-typing (measured: a\n data-probe tag on the node vanished after typing two chars). The\n controlled value + autoFocus masked it in English, but any IME\n composition (Chinese!) got cut off per keystroke. Fix: ONE stable\n component type per hook instance; renders read live state/props from\n refs, so updates re-render in place instead of remounting. */\n /* eslint-disable react-hooks/refs -- render-phase ref mirroring + reads\n are the POINT of the R16 fix: one stable component type per hook\n instance rendering live values (the useCallback version remounted the\n input per keystroke and broke IME composition). */\n const propsRef = useRef(props);\n propsRef.current = props;\n const openRef = useRef(open);\n openRef.current = open;\n\n const reset = useCallback(() => {\n propsRef.current.onChange('');\n setOpen(false);\n }, []);\n\n const [stable] = useState(() => {\n const Button = () => {\n const { value, label = 'Search', enabled = true } = propsRef.current;\n const open = openRef.current;\n if (!enabled) return <></>;\n const active = open || value;\n return (\n <button\n type=\"button\"\n onClick={() => setOpen(v => !v)}\n aria-label={open ? `Close ${label.toLowerCase()}` : `Open ${label.toLowerCase()}`}\n aria-pressed={open}\n title={open ? `Close ${label.toLowerCase()}` : label}\n className={`relative shrink-0 inline-flex items-center justify-center rounded-full border w-9 h-9 transition-colors ${\n active\n ? 'border-cyan-500/40 bg-cyan-500/10 text-cyan-300'\n : 'border-[#26262b] bg-[#161618] text-gray-400 hover:text-gray-200 hover:border-[#3a3a41]'\n }`}\n >\n <svg className=\"w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\" />\n </svg>\n {value && !open && (\n <span aria-hidden className=\"absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full bg-cyan-400 ring-2 ring-[#0b0b0d]\" />\n )}\n </button>\n );\n };\n const Row = () => {\n const { value, onChange, placeholder = 'Search…', enabled = true } = propsRef.current;\n const open = openRef.current;\n if (!enabled) return null;\n if (!open && !value) return null;\n return (\n <div className=\"mb-3 sm:mb-4 flex items-center gap-2\">\n <input\n type=\"text\"\n value={value}\n onChange={e => onChange(e.target.value)}\n onKeyDown={e => { if (e.key === 'Escape') reset(); }}\n placeholder={placeholder}\n autoFocus={open}\n className=\"flex-1 bg-[#161618] border border-[#26262b] rounded-lg px-3 py-2 text-base sm:text-sm text-white placeholder-gray-600 focus:border-cyan-500/40 focus:outline-none\"\n />\n <button\n type=\"button\"\n onClick={reset}\n className=\"shrink-0 text-xs text-gray-500 hover:text-gray-300 px-2 py-2\"\n aria-label=\"Clear and close search\"\n >\n Cancel\n </button>\n </div>\n );\n };\n return { Button, Row };\n });\n /* eslint-enable react-hooks/refs */\n\n return { Button: stable.Button, Row: stable.Row, value, reset };\n}\n"],"names":["previewContent","raw","replace","trim","timeAgo","dateStr","statusColor","status","hasSse","STORE_KEY","AVATAR_EVENT","manifest","pool","manifestRequested","poolPick","alias","length","h","i","charCodeAt","fetchManifestOnce","readMap","getAvatarUrl","setAvatarUrl","url","version","subscribe","callback","bump","window","addEventListener","removeEventListener","useAvatarsVersion","useAvatarUrl","DEFAULTS","nodes","title","sub","tasks","messages","logs","tokens","networks","generic","Glyph","variant","size","s","width","height","baseProps","fill","stroke","strokeWidth","strokeLinecap","strokeLinejoin","viewBox","cx","cy","r","x1","y1","x2","y2","strokeDasharray","opacity","x","y","rx","d","ry","EmptyState","cta","compact","headline","subcopy","iconSize","className","role","href","label","onClick","EmptyCard","children","NodesEmptyState","hint","taskHistoryCount","global_count","toLocaleString","QuickstartCommand","cmd","target","rel","copied","setCopied","onCopy","navigator","clipboard","writeText","setTimeout","type","TASK_STATUSES","STATUS_CHIP_CLASS","created","delivered","acked","running","replied","closed","failed","cancelled","expired","STATUS_DOT_HEX","SESSION_STATUS_CHIP_CLASS","working","idle","blocked","error","offline","SESSION_STATUS_TEXT_CLASS","STATUS_BAR_CLASS","useCollapsibleSearch","props","value","open","setOpen","propsRef","current","openRef","reset","onChange","stable","Button","enabled","active","v","toLowerCase","Row","placeholder","e","onKeyDown","key","autoFocus"],"mappings":"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"}
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
module.exports=[18622,(a,b,c)=>{b.exports=a.x("next/dist/compiled/next-server/app-page-turbo.runtime.prod.js",()=>require("next/dist/compiled/next-server/app-page-turbo.runtime.prod.js"))},56704,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/work-async-storage.external.js",()=>require("next/dist/server/app-render/work-async-storage.external.js"))},32319,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/work-unit-async-storage.external.js",()=>require("next/dist/server/app-render/work-unit-async-storage.external.js"))},24725,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/after-task-async-storage.external.js",()=>require("next/dist/server/app-render/after-task-async-storage.external.js"))},20635,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/action-async-storage.external.js",()=>require("next/dist/server/app-render/action-async-storage.external.js"))},43285,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/dynamic-access-async-storage.external.js",()=>require("next/dist/server/app-render/dynamic-access-async-storage.external.js"))},11998,(a,b,c)=>{"use strict";b.exports=a.r(18622)},5400,(a,b,c)=>{"use strict";b.exports=a.r(11998).vendored["react-ssr"].ReactJsxRuntime},82956,(a,b,c)=>{"use strict";b.exports=a.r(11998).vendored["react-ssr"].React},72022,(a,b,c)=>{"use strict";b.exports=a.r(11998).vendored.contexts.AppRouterContext},67341,(a,b,c)=>{"use strict";b.exports=a.r(11998).vendored["react-ssr"].ReactServerDOMTurbopackClient},60447,(a,b,c)=>{"use strict";b.exports=a.r(11998).vendored.contexts.HooksClientContext},82169,(a,b,c)=>{"use strict";b.exports=a.r(11998).vendored.contexts.ServerInsertedHtml},98005,a=>{"use strict";function b(a){if(!a)return null;let b=Date.parse(a.includes("T")?a:a.replace(" ","T")+"Z");return Number.isFinite(b)?b:null}a.s(["formatWeChatTime",0,function(a){let c=b(a);if(null===c)return"";let d=new Date(c),e=new Date,f="u">typeof navigator&&(navigator.language||"").toLowerCase().startsWith("zh"),g=`${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}`,h=a=>new Date(a.getFullYear(),a.getMonth(),a.getDate()).getTime(),i=Math.round((h(e)-h(d))/864e5);if(0===i)return g;if(1===i)return f?`昨天 ${g}`:`Yesterday ${g}`;if(i>1&&i<7){let a=new Intl.DateTimeFormat(f?"zh-CN":void 0,{weekday:f?"long":"short"}).format(d);return`${a} ${g}`}let j=new Intl.DateTimeFormat(f?"zh-CN":void 0,{month:f?"long":"short",day:"numeric",...d.getFullYear()!==e.getFullYear()?{year:"numeric"}:{}}).format(d);return`${j} ${g}`},"parseHubTime",0,b,"relativeAgo",0,function(a){let c=b(a);if(null===c)return null;let d=Math.floor((Date.now()-c)/1e3);return d<0?"just now":d<60?`${d}s ago`:d<3600?`${Math.floor(d/60)}m ago`:d<86400?`${Math.floor(d/3600)}h ago`:`${Math.floor(d/86400)}d ago`}])},53705,a=>{"use strict";var b=a.i(5400),c=a.i(82956);let d=(0,c.createContext)({networkId:"",setNetworkId:()=>{}});a.s(["NetworkProvider",0,function({children:a}){let[e,f]=(0,c.useState)("");return(0,c.useEffect)(()=>{let a=sessionStorage.getItem("anet_network_id");a&&f(a)},[]),(0,b.jsx)(d.Provider,{value:{networkId:e,setNetworkId:a=>{f(a),sessionStorage.setItem("anet_network_id",a)}},children:a})},"useNetworkId",0,function(){return(0,c.useContext)(d)}])},60810,51563,a=>{"use strict";var b=a.i(82956),c=a.i(10571),d=a.i(53705);let e="anet-chat-mute-updated";function f(a){return!!a&&new Set().has(a)}let g=0;function h(a){let b=()=>{g++,a()};return window.addEventListener(e,b),window.addEventListener("storage",b),()=>{window.removeEventListener(e,b),window.removeEventListener("storage",b)}}function i(){return(0,b.useSyncExternalStore)(h,()=>g,()=>0)}a.s(["isMuted",0,f,"toggleMute",0,function(a){},"useMuteVersion",0,i,"useMuted",0,function(a){return(0,b.useSyncExternalStore)(h,()=>!!a&&f(a),()=>!1)}],51563);let j="anet-chat-read-updated",k=new Set(["","dashboard","admin","api","hub"]),l=a=>k.has((a||"").trim().toLowerCase());function m(a){if(!a)return 0;let b=a.includes("T")?a:a.replace(" ","T"),c=Date.parse(/Z$|[+-]\d{2}:\d{2}$/.test(b)?b:`${b}Z`);return Number.isFinite(c)?c:0}let n="__baseline",o=async a=>{let b=await fetch(a);if(401===b.status)throw window.location.assign("/login"),Error("unauthorized");return b.json()};function p(a,b){a&&new Date().toISOString()}a.s(["badgeLabel",0,function(a){return a<=0?"":a>99?"99+":String(a)},"markChatRead",0,p,"useChatUnread",0,function(){let{networkId:a}=(0,d.useNetworkId)(),{data:e}=(0,c.default)(function(a,b){if(!b)return a;let c=a.includes("?")?"&":"?";return`${a}${c}network_id=${encodeURIComponent(b)}`}("/api/hub/messages?limit=200",a),o,{refreshInterval:5e3,dedupingInterval:3e3}),[g,h]=(0,b.useState)(0);(0,b.useEffect)(()=>{let b=b=>{b.key&&b.key!==`anet_chat_read_v1:${a||"global"}`||h(a=>"number"==typeof a?a+1:1)},c=b=>{let c=b.detail;(c?.networkId===void 0||c.networkId===a)&&h(a=>"number"==typeof a?a+1:1)};return window.addEventListener("storage",b),window.addEventListener(j,c),()=>{window.removeEventListener("storage",b),window.removeEventListener(j,c)}},[a,h]);let k=(0,b.useMemo)(()=>({}),[a,g]),q=(0,b.useMemo)(()=>{let a=new Map,b=new Map,c=new Set(Object.keys(k).filter(a=>a!==n)),d=m(k[n]),f=(e,f)=>{c.has(e)&&f>(b.get(e)||0)&&b.set(e,f),f>Math.max(m(k[e]),d)&&a.set(e,(a.get(e)||0)+1)};for(let a of e?.messages||[]){let b=a.from_alias?.trim();if(!b||l(b)||!l(a.to_alias))continue;let c=m(a.created_at);c&&f(b,c)}return{counts:a,lastActivity:b}},[e,k]),r=(0,b.useCallback)(a=>!!a&&(q.counts.get(a)||0)>0,[q]),s=(0,b.useCallback)(a=>a&&q.counts.get(a)||0,[q]),t=(0,b.useCallback)(a=>a&&q.lastActivity.get(a)||0,[q]),u=i(),{totalUnread:v,hasMutedUnread:w}=(0,b.useMemo)(()=>{let a=0,b=!1;for(let[c,d]of q.counts){if(f(c)){d>0&&(b=!0);continue}a+=d}return{totalUnread:a,hasMutedUnread:b}},[q,u]);return{unreadMap:q,hasUnread:r,unreadCount:s,lastActivityAt:t,totalUnread:v,hasMutedUnread:w,clearUnread:(0,b.useCallback)(b=>{p(b,a)},[a])}}],60810)},19315,a=>{"use strict";var b=a.i(5400),c=a.i(82956);let d=new Set(["cyber","light","slack"]);function e(a){if("u"<typeof document)return;let b=document.documentElement;"slack"===a?(b.setAttribute("data-theme","light"),b.setAttribute("data-skin","slack")):(b.setAttribute("data-theme",a),b.removeAttribute("data-skin"))}a.s(["THEMES",0,d,"ThemeProvider",0,function({children:a}){return(0,c.useEffect)(()=>{if("u"<typeof document)return;let a="cyber";try{let b=localStorage.getItem("anet-theme");b&&d.has(b)&&(a=b)}catch{}e(a)},[]),(0,b.jsx)(b.Fragment,{children:a})},"applyTheme",0,e])},6497,a=>{"use strict";var b=a.i(5400),c=a.i(82956),d=a.i(10571);let e=async a=>{let b=await fetch(a);if(401===b.status)throw Error("unauthorized");if(!b.ok)throw Error(`hub ${b.status}`);return b.json()};function f(a){return a>=85?{fill:"#ef4444",track:"rgb(239 68 68 / 0.16)"}:a>=60?{fill:"#f59e0b",track:"rgb(245 158 11 / 0.16)"}:{fill:"#10b981",track:"rgb(16 185 129 / 0.16)"}}function g({pct:a,label:c}){let d=f(a),e=Math.max(0,Math.min(100,a));return(0,b.jsxs)("div",{className:"space-y-0.5",children:[(0,b.jsxs)("div",{className:"flex justify-between text-[10px] text-[var(--fg-muted)] font-mono",children:[(0,b.jsx)("span",{children:c}),(0,b.jsxs)("span",{className:"tabular-nums",children:[Math.round(e),"%"]})]}),(0,b.jsx)("div",{className:"h-1.5 rounded-full overflow-hidden",style:{background:d.track},children:(0,b.jsx)("div",{className:"h-full transition-[width] duration-300 ease-out",style:{width:`${e}%`,background:d.fill}})})]})}function h({values:a,tint:c,label:d}){if(!a||a.length<2)return null;let e=100/Math.max(1,a.length-1),f=a.map((a,b)=>{let c=18-Math.max(0,Math.min(100,a))/100*18;return`${(b*e).toFixed(1)},${c.toFixed(1)}`}),g=f.join(" "),i=`M0,18 L${f.join(" L")} L100,18 Z`;return(0,b.jsxs)("svg",{width:"100%",height:18,viewBox:"0 0 100 18",preserveAspectRatio:"none","aria-label":`${d} 5-min history`,"data-server-sparkline":d,children:[(0,b.jsx)("path",{d:i,fill:c,opacity:"0.18"}),(0,b.jsx)("polyline",{points:g,fill:"none",stroke:c,strokeWidth:"1.2"})]})}function i({cpu:a,mem:c,disk:d}){let e=[a,c,d].filter(a=>"number"==typeof a);if(0===e.length)return null;let g=Math.max(...e),h=f(g);return(0,b.jsx)("span",{className:"inline-block w-2 h-2 rounded-full shrink-0",style:{background:h.fill,boxShadow:`0 0 0 2px ${h.track}`},"data-server-health-badge":g>=85?"red":g>=60?"amber":"green",title:`worst: ${Math.round(g)}%`})}function j({agents:a}){return a&&0!==a.length?(0,b.jsx)("ul",{className:"space-y-1","data-server-agent-list":!0,children:a.map(a=>{let c="working"===a.status?"#10b981":"idle"===a.status?"#06b6d4":"#6b7280";return(0,b.jsxs)("li",{className:"flex items-center gap-1.5 text-[10px] font-mono","data-server-agent":a.alias,children:[(0,b.jsx)("span",{className:"w-1.5 h-1.5 rounded-full shrink-0",style:{background:c}}),(0,b.jsx)("span",{className:"font-semibold truncate",style:{color:"var(--fg)"},children:a.alias}),a.runtime&&(0,b.jsxs)("span",{className:"text-[9px] text-[var(--fg-dim)] shrink-0",children:["· ",a.runtime]}),"number"==typeof a.progress&&a.progress>0&&(0,b.jsxs)("span",{className:"ml-auto text-[9px] tabular-nums text-[var(--fg-muted)]",children:[Math.round(100*a.progress),"%"]})]},a.alias)})}):(0,b.jsx)("div",{className:"text-[9px] text-[var(--fg-dim)] font-mono italic px-1 py-1","data-server-agents-missing":"true",children:"agent rollup not reported by hub"})}function k(a=!0){let{data:b,error:c}=(0,d.default)(a?"/api/hub/servers":null,e,{refreshInterval:5e3,dedupingInterval:3e3}),f=b?.servers??[],g=b?.unavailable===!0,h=f.filter(a=>"online"===a.status).length,i=a&&!b&&!c;return{data:b,error:c,servers:f,unavailable:g,onlineCount:h,loading:i}}function l({enabled:a=!0,className:d=""}){let[e,f]=(0,c.useState)(new Set);(0,c.useEffect)(()=>{try{let a=localStorage.getItem("anet-servers-drawer-expanded");a&&f(new Set(JSON.parse(a)))}catch{}},[]);let{data:m,error:n,servers:o,unavailable:p,loading:q}=k(a);return(0,b.jsxs)("div",{className:`min-h-0 overflow-y-auto px-2 py-2 space-y-2 ${d}`,"data-servers-body":!0,children:[q&&(0,b.jsx)("div",{className:"text-[10px] text-[var(--fg-muted)] font-mono text-center py-4",children:"loading servers…"}),n&&!m&&(0,b.jsx)("div",{className:"rounded-md border px-2.5 py-2 text-[10px] font-mono",style:{background:"rgb(239 68 68 / 0.06)",borderColor:"rgb(239 68 68 / 0.25)",color:"#ef4444"},children:"hub unreachable · retrying every 5s"}),p&&(0,b.jsxs)("div",{className:"text-[10px] text-[var(--fg-muted)] font-mono text-center py-3 leading-relaxed",children:["host telemetry not available",(0,b.jsx)("br",{}),(0,b.jsx)("span",{className:"text-[var(--fg-dim)]",children:"upgrade commhub-server ≥ 0.8.1-preview.2"})]}),!q&&!n&&!p&&0===o.length&&(0,b.jsx)("div",{className:"text-[10px] text-[var(--fg-muted)] font-mono text-center py-4",children:"no servers reporting yet"}),o.map(a=>{let c="offline"===a.status,d=null!=a.cpu_load_1min&&a.cpu_cores>0?a.cpu_load_1min/a.cpu_cores*100:null,k=null!=a.mem_used_gb&&null!=a.mem_total_gb&&a.mem_total_gb>0?a.mem_used_gb/a.mem_total_gb*100:null,l=null!=a.disk_used_gb&&null!=a.disk_total_gb&&a.disk_total_gb>0?a.disk_used_gb/a.disk_total_gb*100:null,m=e.has(a.hostname);return(0,b.jsxs)("div",{className:"rounded-lg border px-2.5 py-2 space-y-1.5",style:{background:"var(--bg)",borderColor:"var(--border)",opacity:c?.55:1},"data-server-card":a.hostname,"data-server-expanded":m?"true":"false",title:a.ip?`${a.hostname} \xb7 ${a.ip}`:a.hostname,children:[(0,b.jsxs)("button",{type:"button",onClick:()=>{let b;return b=a.hostname,f(a=>{let c=new Set(a);c.has(b)?c.delete(b):c.add(b);try{localStorage.setItem("anet-servers-drawer-expanded",JSON.stringify([...c]))}catch{}return c})},"aria-expanded":m,className:"w-full flex items-center justify-between gap-2 text-left","data-server-card-toggle":a.hostname,children:[(0,b.jsxs)("div",{className:"min-w-0 flex items-center gap-1.5",children:[!c&&(0,b.jsx)(i,{cpu:d,mem:k,disk:l}),(0,b.jsx)("span",{className:"font-mono text-[12px] font-semibold truncate",style:{color:"var(--fg)"},children:a.hostname}),a.note&&(0,b.jsxs)("span",{className:"text-[9px] text-[var(--fg-dim)]",children:["(",a.note,")"]})]}),(0,b.jsxs)("span",{className:"flex items-center gap-1 shrink-0",children:[(0,b.jsxs)("span",{className:"text-[10px] text-[var(--fg-muted)] font-mono tabular-nums",children:[a.agent_count," agent",1===a.agent_count?"":"s"]}),(0,b.jsx)("svg",{width:"10",height:"10",viewBox:"0 0 10 10",style:{transform:m?"rotate(90deg)":"rotate(0deg)",transition:"transform 150ms ease-out"},"aria-hidden":!0,children:(0,b.jsx)("path",{d:"M3 1.5L7 5L3 8.5",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})})]})]}),!c&&null!=d&&(0,b.jsx)(g,{pct:d,label:`CPU ${a.cpu_load_1min.toFixed(2)}/${a.cpu_cores}`}),!c&&null!=k&&(0,b.jsx)(g,{pct:k,label:`RAM ${a.mem_used_gb.toFixed(1)}/${a.mem_total_gb.toFixed(1)}G`}),c&&(0,b.jsx)("div",{className:"text-[10px] text-[var(--fg-dim)] font-mono italic",children:"CPU n/a · RAM n/a · offline"}),m&&!c&&(0,b.jsxs)("div",{className:"pt-1 mt-1 border-t space-y-1.5",style:{borderColor:"var(--border)"},children:[null!=l?(0,b.jsx)(g,{pct:l,label:`DISK ${a.disk_used_gb.toFixed(1)}/${a.disk_total_gb.toFixed(1)}G`}):(0,b.jsx)("div",{className:"text-[9px] text-[var(--fg-dim)] font-mono italic","data-server-disk-missing":"true",children:"disk metric not reported by hub"}),a.cpu_history&&a.cpu_history.length>=2&&(0,b.jsxs)("div",{className:"space-y-0.5",children:[(0,b.jsx)("div",{className:"text-[9px] text-[var(--fg-muted)] font-mono",children:"CPU · 5-min"}),(0,b.jsx)(h,{values:a.cpu_history,tint:"#10b981",label:"CPU"})]}),a.mem_history&&a.mem_history.length>=2&&(0,b.jsxs)("div",{className:"space-y-0.5",children:[(0,b.jsx)("div",{className:"text-[9px] text-[var(--fg-muted)] font-mono",children:"RAM · 5-min"}),(0,b.jsx)(h,{values:a.mem_history,tint:"#06b6d4",label:"MEM"})]}),a.disk_history&&a.disk_history.length>=2&&(0,b.jsxs)("div",{className:"space-y-0.5",children:[(0,b.jsx)("div",{className:"text-[9px] text-[var(--fg-muted)] font-mono",children:"DISK · 5-min"}),(0,b.jsx)(h,{values:a.disk_history,tint:"#f59e0b",label:"DISK"})]}),(0,b.jsxs)("div",{className:"pt-1",children:[(0,b.jsx)("div",{className:"text-[9px] text-[var(--fg-muted)] font-mono mb-0.5",children:"agents"}),(0,b.jsx)(j,{agents:a.agents})]})]})]},a.hostname)}),o.length>0&&(0,b.jsx)("div",{className:"pt-1 text-[9px] text-[var(--fg-dim)] font-mono text-center",children:"live · refreshing every 5s"})]})}a.s(["ServersDrawer",0,function(){let[a,d]=(0,c.useState)(!1);(0,c.useEffect)(()=>{try{"1"===localStorage.getItem("anet-servers-drawer")&&d(!0)}catch{}},[]);let{servers:e,onlineCount:f}=k(a);return(0,b.jsxs)("aside",{className:"fixed right-0 top-20 z-40 hidden lg:flex flex-col rounded-l-xl border border-r-0 shadow-xl shadow-black/30 anet-fade-in",style:{background:"var(--bg-secondary)",borderColor:"var(--border)",width:a?296:40,maxHeight:"calc(100vh - 6rem)",transition:"width 200ms ease-out"},"aria-label":"Servers panel",children:[(0,b.jsxs)("button",{onClick:()=>d(a=>{let b=!a;try{localStorage.setItem("anet-servers-drawer",b?"1":"0")}catch{}return b}),"aria-expanded":a,"aria-label":a?"Collapse servers panel":"Expand servers panel",className:"flex items-center gap-2 px-2.5 py-2 border-b text-[11px] font-semibold tracking-wide select-none",style:{color:"var(--fg)",borderColor:"var(--border)"},children:[(0,b.jsxs)("svg",{className:"w-4 h-4 shrink-0",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.8",strokeLinecap:"round",strokeLinejoin:"round",children:[(0,b.jsx)("rect",{x:"3",y:"4",width:"18",height:"6",rx:"1.5"}),(0,b.jsx)("rect",{x:"3",y:"14",width:"18",height:"6",rx:"1.5"}),(0,b.jsx)("circle",{cx:"7",cy:"7",r:"0.7",fill:"currentColor"}),(0,b.jsx)("circle",{cx:"7",cy:"17",r:"0.7",fill:"currentColor"})]}),a&&(0,b.jsxs)(b.Fragment,{children:[(0,b.jsx)("span",{className:"flex-1 text-left",children:"Servers"}),(0,b.jsxs)("span",{className:"text-[10px] text-[var(--fg-muted)] font-mono",children:[f,"/",e.length]})]})]}),a&&(0,b.jsx)(l,{enabled:a,className:"flex-1"})]})},"ServersPanel",0,l])}];
|
|
2
|
-
|
|
3
|
-
//# sourceMappingURL=%5Broot-of-the-server%5D__06g2-qq._.js.map
|