@nelsonlongxiang/dsh-open-a2a-net 0.5.14 → 0.6.24

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.
Files changed (86) hide show
  1. package/README.md +243 -121
  2. package/README_zh.md +144 -84
  3. package/assets/nexusDist/assets/index-034h1cNc.js +3 -0
  4. package/assets/nexusDist/assets/index-PG9EieGX.css +1 -0
  5. package/assets/nexusDist/index.html +26 -0
  6. package/assets/stageDist/assets/index-Bj7S2pA1.css +1 -0
  7. package/assets/stageDist/assets/index-C-28k_o5.js +974 -0
  8. package/assets/stageDist/assets/interiors-CQGBjffT.png +0 -0
  9. package/assets/stageDist/assets/office-tileset-CHQT-uYL.png +0 -0
  10. package/assets/stageDist/index.html +13 -0
  11. package/lib/a2a-client.js +190 -13
  12. package/lib/canvas-store.js +218 -0
  13. package/lib/card.js +25 -0
  14. package/lib/client/A2aControl.js +45 -137
  15. package/lib/client/locales.js +16 -40
  16. package/lib/client.js +164 -467
  17. package/lib/client.js.map +1 -1
  18. package/lib/group-store.js +6 -0
  19. package/lib/idempotency-store.js +291 -0
  20. package/lib/index.js +4217 -259
  21. package/lib/joined-store.js +11 -0
  22. package/lib/layout-store.js +155 -0
  23. package/lib/peer-store.js +98 -13
  24. package/lib/receipt-ladder.js +35 -0
  25. package/lib/receipt.js +102 -0
  26. package/lib/self-suppress.js +62 -0
  27. package/lib/session-transcript.js +150 -0
  28. package/lib/skill/SKILL.md +77 -0
  29. package/lib/skill/references/discovery.md +38 -0
  30. package/lib/skill/references/membership.md +50 -0
  31. package/lib/skill/references/pitfalls.md +42 -0
  32. package/lib/skill/references/routing.md +43 -0
  33. package/lib/skill-registration.js +52 -0
  34. package/lib/stage-mount.js +32 -0
  35. package/lib/task-ledger.js +540 -0
  36. package/lib/team-store.js +122 -0
  37. package/lib/teams-bridge.js +18 -0
  38. package/lib/transport-caps.js +31 -0
  39. package/lib/types/a2a-client.d.ts +72 -1
  40. package/lib/types/canvas-store.d.ts +102 -0
  41. package/lib/types/client/A2aControl.d.ts +18 -28
  42. package/lib/types/client/locales.d.ts +1 -1
  43. package/lib/types/group-store.d.ts +6 -0
  44. package/lib/types/idempotency-store.d.ts +164 -0
  45. package/lib/types/index.d.ts +210 -6
  46. package/lib/types/joined-store.d.ts +9 -0
  47. package/lib/types/layout-store.d.ts +63 -0
  48. package/lib/types/peer-store.d.ts +45 -2
  49. package/lib/types/receipt-ladder.d.ts +25 -0
  50. package/lib/types/receipt.d.ts +51 -0
  51. package/lib/types/self-suppress.d.ts +41 -0
  52. package/lib/types/session-transcript.d.ts +37 -0
  53. package/lib/types/skill-registration.d.ts +3 -0
  54. package/lib/types/stage-mount.d.ts +29 -0
  55. package/lib/types/task-ledger.d.ts +308 -0
  56. package/lib/types/team-store.d.ts +55 -0
  57. package/lib/types/teams-bridge.d.ts +112 -0
  58. package/lib/types/transport-caps.d.ts +29 -0
  59. package/lib/types/types.d.ts +43 -0
  60. package/nexus-stage/package-lock.json +2262 -0
  61. package/nexus-stage/package.json +21 -0
  62. package/nexus-stage/scripts/__pycache__/mockhost.cpython-312.pyc +0 -0
  63. package/nexus-stage/scripts/mockhost.py +253 -0
  64. package/nexus-stage/src/canvas-ops.ts +175 -0
  65. package/nexus-stage/src/canvas-wire.ts +156 -0
  66. package/nexus-stage/src/edges.ts +72 -0
  67. package/nexus-stage/src/fault.ts +56 -0
  68. package/nexus-stage/src/federation.ts +234 -0
  69. package/nexus-stage/src/layout-doc.ts +143 -0
  70. package/nexus-stage/src/layout-wire.ts +158 -0
  71. package/nexus-stage/src/main.ts +361 -0
  72. package/nexus-stage/src/planning-menus.ts +352 -0
  73. package/nexus-stage/src/planning-nodelist.ts +294 -0
  74. package/nexus-stage/src/planning-types.ts +111 -0
  75. package/nexus-stage/src/planning-view.ts +1313 -0
  76. package/nexus-stage/src/planning.css +280 -0
  77. package/nexus-stage/src/seat.ts +35 -0
  78. package/nexus-stage/src/stage-name.ts +13 -0
  79. package/nexus-stage/src/tokens.ts +32 -0
  80. package/nexus-stage/src/viewport.ts +83 -0
  81. package/nexus-stage/src/workbench.css +267 -0
  82. package/nexus-stage/src/workbench.ts +1530 -0
  83. package/nexus-stage/src/world.ts +509 -0
  84. package/nexus-stage/tsconfig.json +25 -0
  85. package/nexus-stage/vite.config.ts +16 -0
  86. package/package.json +83 -22
@@ -1,18 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
- * A2A network control: a footer action opening this host's network panel.
4
- * Probes the host's session-node state route at registration time (the seat
5
- * renders only on a host that serves it). The panel has three sections:
6
- * session nodes (live rows show title, recent-activity excerpt, and team with
7
- * a join/leave toggle and open the session on row click — the wake action on
8
- * cold rows opens the session through the standard flow), the peer fleet
9
- * (tracked URLs with quality scores), and the recent routing activity ring
10
- * (inbound and outbound outcomes). While open, the panel polls so listings
11
- * visibly follow each turn.
3
+ * A2A network control: a footer action opening this host's recent network
4
+ * activity feed. The feed is the panel's whole content — in-flight routes,
5
+ * owed receipts, and the routing activity ring. Session/team management
6
+ * lives in the planning view (design: docs/design/panel-activity-feed.md):
7
+ * the stage links row above the feed is the explicit navigation to it.
8
+ * Probes the host's state route at registration time (the seat renders only
9
+ * on a host that serves it). While open, the feed polls so it visibly
10
+ * follows each turn; the closed trigger's unread badge rides a slow poll.
12
11
  */
13
12
  import { useCallback, useEffect, useRef, useState } from 'react';
14
13
  import clsx from 'clsx';
15
- import { Button, IconGlobeOutline14 } from '@deepseek-ai/dsh-client-ui-primitives';
14
+ import { Button, IconGlobeOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives';
16
15
  import css from './A2aControl.module.css';
17
16
  /** Fetch the host's network state; undefined means the route is absent. */
18
17
  async function fetchState() {
@@ -24,11 +23,10 @@ async function fetchState() {
24
23
  return undefined;
25
24
  return {
26
25
  sessions: body.sessions,
27
- groups: Array.isArray(body.groups) ? body.groups : [],
28
- peers: Array.isArray(body.peers) ? body.peers : [],
29
26
  activity: Array.isArray(body.activity) ? body.activity : [],
30
27
  inFlight: Array.isArray(body.inFlight) ? body.inFlight : [],
31
- remote: Array.isArray(body.remote) ? body.remote : [],
28
+ tasks: Array.isArray(body.tasks) ? body.tasks : [],
29
+ ...(typeof body.tasksStaleCount === 'number' ? { tasksStaleCount: body.tasksStaleCount } : {}),
32
30
  ...(typeof body.version === 'string' ? { version: body.version } : {}),
33
31
  };
34
32
  }
@@ -42,46 +40,24 @@ function relativeTime(ts) {
42
40
  return `${String(minutes)}m`;
43
41
  return `${String(Math.round(minutes / 60))}h`;
44
42
  }
45
- /**
46
- * One session row: the facts block opens the session, the toggle joins or
47
- * leaves, and the tag button opens the group picker (assign / create /
48
- * clear).
49
- */
50
- function SessionRow({ row, t, listById, openSession, busy, onToggle, pickerFor, setPickerFor, groups, newGroupName, setNewGroupName, onAssign }) {
51
- const cold = row.live === false;
52
- const name = cold ? (listById[row.id]?.displayTitle ?? row.label) : (row.name ?? row.label);
53
- const description = cold ? t('a2a.cold') : (row.description ?? '');
54
- const pickerOpen = pickerFor === row.id;
55
- return (_jsxs("div", { className: clsx(css.row, row.joined && css.joined), children: [_jsxs("button", { type: "button", className: css.facts, title: cold ? t('a2a.wake') : name, onClick: () => { openSession(row.id); }, children: [_jsxs("span", { className: css.nameRow, children: [_jsx("span", { className: clsx(css.stateDot, cold ? 'cold' : 'live', row.joined && 'joined'), "aria-hidden": true }), _jsx("span", { className: css.name, children: name }), row.group !== undefined ? _jsx("span", { className: css.groupTag, children: row.group }) : null] }), _jsx("span", { className: css.description, children: description }), _jsx("span", { className: css.team, children: row.team })] }), _jsx("button", { type: "button", className: css.groupButton, title: t('a2a.group'), "aria-label": t('a2a.group'), onClick: () => { setPickerFor(pickerOpen ? null : row.id); }, children: "#" }), cold
56
- ? (_jsx(Button, { variant: "outline", size: "sm", disabled: busy === row.id, onClick: () => { onToggle(row); }, children: t('a2a.leave') }))
57
- : (_jsx(Button, { variant: row.joined ? 'outline' : 'primary', size: "sm", disabled: busy === row.id, onClick: () => { onToggle(row); }, children: row.joined ? t('a2a.leave') : t('a2a.join') })), pickerOpen && (_jsxs("div", { className: css.groupPicker, children: [_jsx("button", { type: "button", className: css.groupOption, onClick: () => { onAssign(row.id, ''); }, children: t('a2a.groupClear') }), groups.map((group) => (_jsx("button", { type: "button", className: clsx(css.groupOption, row.group === group && css.groupOptionActive), onClick: () => { onAssign(row.id, group); }, children: group }, group))), _jsxs("div", { className: css.groupNew, children: [_jsx("input", { className: css.groupInput, value: newGroupName, placeholder: t('a2a.groupNew'), maxLength: 40, onChange: (event) => { setNewGroupName(event.target.value); }, onKeyDown: (event) => {
58
- if (event.key === 'Enter' && newGroupName.trim() !== '')
59
- onAssign(row.id, newGroupName.trim());
60
- } }), _jsx(Button, { variant: "primary", size: "sm", disabled: newGroupName.trim() === '', onClick: () => { if (newGroupName.trim() !== '')
61
- onAssign(row.id, newGroupName.trim()); }, children: t('a2a.groupNewGo') })] })] }))] }));
43
+ /** One feed row: direction glyph (aria-labelled), team, peer host, age, fail mark. */
44
+ function FeedRow({ dir, team, peer, ts, ok, target, openSession, t }) {
45
+ const fresh = Date.now() - ts < 5_000;
46
+ return (_jsxs("div", { className: clsx(css.activityRow, fresh && css.fresh), "data-ok": ok === undefined ? undefined : String(ok), "data-jump": target !== undefined, title: target !== undefined ? t('a2a.jump') : undefined, onClick: () => { if (target !== undefined)
47
+ openSession(target.id); }, role: target !== undefined ? 'button' : undefined, children: [_jsx("span", { className: clsx(css.activityDir, dir), "aria-label": dir === 'in' ? 'inbound' : 'outbound', children: dir === 'in' ? '←' : '→' }), _jsx("span", { className: css.activityTeam, title: team, children: team }), _jsx("span", { className: css.activityPeer, title: peer, children: peer === '' || peer === 'local' ? '' : peer.replace(/^https?:\/\//, '').replace(/\/.*$/, '') }), ok === false ? _jsx("span", { className: css.failMark, "aria-label": "failed", children: "\u2715" }) : null, _jsx("span", { className: css.activityTime, children: relativeTime(ts) })] }));
62
48
  }
63
49
  /**
64
- * Render the network toggle and its network panel.
50
+ * Render the network toggle and its activity feed.
65
51
  * @param props - composed slot props.
66
52
  * @returns the control element tree.
67
53
  */
68
- export function A2aControl({ wide, t, useSessions, openSession }) {
54
+ export function A2aControl({ wide, t, openSession }) {
69
55
  const [open, setOpen] = useState(false);
70
- const [state, setState] = useState({ sessions: [], groups: [], peers: [], activity: [], inFlight: [], remote: [] });
71
- const [busy, setBusy] = useState(null);
56
+ const [state, setState] = useState({ sessions: [], activity: [], inFlight: [], tasks: [] });
72
57
  const [seenActivity, setSeenActivity] = useState(0);
73
- // Panel-local search: filters session rows by name/team/description.
74
- const [search, setSearch] = useState('');
75
- // Group UI: the row whose group picker is open, and collapsed groups.
76
- const [pickerFor, setPickerFor] = useState(null);
77
- const [newGroupName, setNewGroupName] = useState('');
78
- const [collapsed, setCollapsed] = useState(new Set());
79
58
  const stopped = useRef(false);
80
59
  useEffect(() => () => { stopped.current = true; }, []);
81
60
  const wrap = useRef(null);
82
- // Cold rows carry no host-side facts; the client's own session list holds
83
- // the durable title for exactly those sessions.
84
- const listById = useSessions(state2 => state2.byId);
85
61
  // The popover anchors to the trigger's viewport rect: the sidebar column
86
62
  // clips its descendants (the collapse slide), so an in-column absolute
87
63
  // popover is cut at the column edge whenever the column is narrower than
@@ -128,8 +104,7 @@ export function A2aControl({ wide, t, useSessions, openSession }) {
128
104
  useEffect(() => {
129
105
  if (!open)
130
106
  return;
131
- // While the panel is open, titles, activity excerpts, and the routing
132
- // ring keep moving; poll so the listing visibly follows.
107
+ // While the feed is open, it polls so the ring visibly follows turns.
133
108
  const poll = setInterval(refresh, 2_000);
134
109
  return () => { clearInterval(poll); };
135
110
  }, [open, refresh]);
@@ -143,97 +118,30 @@ export function A2aControl({ wide, t, useSessions, openSession }) {
143
118
  document.addEventListener('pointerdown', onPointerDown);
144
119
  return () => { document.removeEventListener('pointerdown', onPointerDown); };
145
120
  }, [open]);
146
- /** Join or leave one session, then refresh the listing. */
147
- const toggle = (row) => {
148
- setBusy(row.id);
149
- void fetch(`/__dsh_a2a/${row.joined ? 'leave' : 'join'}`, {
150
- method: 'POST',
151
- headers: { 'Content-Type': 'application/json' },
152
- body: JSON.stringify({ id: row.id }),
153
- })
154
- .then(() => { refresh(); })
155
- .catch(() => { })
156
- .finally(() => { if (!stopped.current)
157
- setBusy(null); });
158
- };
159
- /** Assign one session to a group ('' clears), then refresh. */
160
- const assign = (id, name) => {
161
- void fetch('/__dsh_a2a/groups', {
162
- method: 'POST',
163
- headers: { 'Content-Type': 'application/json' },
164
- body: JSON.stringify({ action: 'assign', id, name }),
165
- })
166
- .then(() => { setPickerFor(null); setNewGroupName(''); refresh(); })
167
- .catch(() => { });
168
- };
169
- const { sessions, groups, peers, activity, inFlight, remote } = state;
170
- // Search filters by name, team, or description (case-insensitive).
171
- const needle = search.trim().toLowerCase();
172
- const matches = (row) => {
173
- if (needle === '')
174
- return true;
175
- const name = row.live === false ? (listById[row.id]?.displayTitle ?? row.label) : (row.name ?? row.label);
176
- return name.toLowerCase().includes(needle)
177
- || row.team.toLowerCase().includes(needle)
178
- || (row.description ?? '').toLowerCase().includes(needle);
179
- };
180
- const visible = sessions.filter(matches);
181
- const ungrouped = visible.filter(row => row.group === undefined);
182
- const grouped = groups
183
- .map(name => ({ name, rows: visible.filter(row => row.group === name) }))
184
- .filter(group => group.rows.length > 0);
185
- return (_jsxs("div", { ref: wrap, className: clsx(css.wrap, wide ? css.wide : css.rail), children: [_jsx(Button, { variant: "ghost", size: "sm", title: t('a2a.label'), "aria-label": t('a2a.label'), "aria-expanded": open, onClick: () => { toggleOpen(); }, children: _jsxs("span", { className: css.trigger, children: [_jsx(IconGlobeOutline14, {}), wide ? _jsx("span", { children: t('a2a.label') }) : null, unread > 0 ? _jsx("span", { className: css.unreadBadge, "aria-label": String(unread), children: unread > 9 ? '9+' : String(unread) }) : null] }) }), open
186
- ? (_jsxs("div", { className: css.popover, role: "dialog", "aria-label": t('a2a.title'), style: anchor === null ? undefined : { left: `${String(anchor.left)}px`, bottom: `${String(anchor.bottom)}px` }, children: [_jsxs("div", { className: css.title, children: [t('a2a.title'), state.version !== undefined ? _jsxs("span", { className: css.versionTag, title: state.version, children: ["v", state.version] }) : null] }), _jsx("input", { className: css.searchInput, type: "search", value: search, placeholder: t('a2a.search'), "aria-label": t('a2a.search'), onChange: (event) => { setSearch(event.target.value); } }), sessions.length === 0
187
- ? _jsx("div", { className: css.empty, children: t('a2a.empty') })
188
- : visible.length === 0
189
- ? _jsx("div", { className: css.empty, children: t('a2a.searchEmpty') })
190
- : (_jsxs(_Fragment, { children: [ungrouped.length > 0 && (_jsxs("div", { className: css.sessionGroup, children: [_jsxs("button", { type: "button", className: css.groupHead, onClick: () => {
191
- setCollapsed((current) => {
192
- const next = new Set(current);
193
- if (next.has(''))
194
- next.delete('');
195
- else
196
- next.add('');
197
- return next;
198
- });
199
- }, children: [_jsx("span", { className: clsx(css.groupChevron, collapsed.has('') && css.groupChevronClosed), "aria-hidden": true, children: "\u25BE" }), t('a2a.groupDefault'), " \u00B7 ", String(ungrouped.length)] }), !collapsed.has('') && ungrouped.map((row) => (_jsx(SessionRow, { row: row, t: t, listById: listById, openSession: openSession, busy: busy, onToggle: toggle, pickerFor: pickerFor, setPickerFor: setPickerFor, groups: groups, newGroupName: newGroupName, setNewGroupName: setNewGroupName, onAssign: assign }, row.id)))] })), grouped.map(({ name, rows }) => {
200
- const isCollapsed = collapsed.has(name);
201
- return (_jsxs("div", { className: css.sessionGroup, children: [_jsxs("button", { type: "button", className: css.groupHead, onClick: () => {
202
- setCollapsed((current) => {
203
- const next = new Set(current);
204
- if (next.has(name))
205
- next.delete(name);
206
- else
207
- next.add(name);
208
- return next;
209
- });
210
- }, children: [_jsx("span", { className: clsx(css.groupChevron, isCollapsed && css.groupChevronClosed), "aria-hidden": true, children: "\u25BE" }), name, " \u00B7 ", String(rows.length)] }), !isCollapsed && rows.map((row) => (_jsx(SessionRow, { row: row, t: t, listById: listById, openSession: openSession, busy: busy, onToggle: toggle, pickerFor: pickerFor, setPickerFor: setPickerFor, groups: groups, newGroupName: newGroupName, setNewGroupName: setNewGroupName, onAssign: assign }, row.id)))] }, name));
211
- })] })), _jsx("div", { className: css.note, children: t('a2a.note') }), (needle === '' ? remote : remote.filter(row => row.team.toLowerCase().includes(needle) || row.name.toLowerCase().includes(needle) || (row.origin ?? '').toLowerCase().includes(needle) || (row.workspace ?? '').toLowerCase().includes(needle))).length > 0 && (_jsxs(_Fragment, { children: [_jsx("div", { className: css.sectionTitle, children: t('a2a.remote') }), Object.entries((needle === '' ? remote : remote.filter(row => row.team.toLowerCase().includes(needle) || row.name.toLowerCase().includes(needle) || (row.origin ?? '').toLowerCase().includes(needle) || (row.workspace ?? '').toLowerCase().includes(needle)))
212
- .reduce((byOrigin, row) => {
213
- const key = row.origin ?? '';
214
- byOrigin[key] = [...(byOrigin[key] ?? []), row];
215
- return byOrigin;
216
- }, {})).map(([origin, rows]) => (_jsxs("div", { className: css.sessionGroup, children: [_jsxs("button", { type: "button", className: css.groupHead, onClick: () => {
217
- setCollapsed((current) => {
218
- const next = new Set(current);
219
- if (next.has('remote:' + origin))
220
- next.delete('remote:' + origin);
221
- else
222
- next.add('remote:' + origin);
223
- return next;
224
- });
225
- }, children: [_jsx("span", { className: clsx(css.groupChevron, collapsed.has('remote:' + origin) && css.groupChevronClosed), "aria-hidden": true, children: "\u25BE" }), origin === '' ? t('a2a.remoteUnknownOrigin') : origin, " \u00B7 ", String(rows.length)] }), !collapsed.has('remote:' + origin) && rows.map((row) => (_jsx("div", { className: css.row, title: row.workspace ?? undefined, children: _jsxs("div", { className: css.facts, children: [_jsxs("span", { className: css.nameRow, children: [_jsx("span", { className: css.stateDot, "data-peer": true, "aria-hidden": true }), _jsx("span", { className: css.name, children: row.name === '' ? row.team : row.name }), row.workspace !== undefined ? _jsx("span", { className: css.groupTag, children: row.workspace }) : null] }), _jsx("span", { className: css.team, children: row.team })] }) }, row.team)))] }, origin)))] })), _jsx("div", { className: css.sectionTitle, children: t('a2a.peers') }), peers.length === 0
226
- ? _jsx("div", { className: css.empty, children: t('a2a.peersEmpty') })
227
- : (_jsx("div", { className: css.peerGrid, children: peers.map((peer) => (_jsxs("span", { className: css.peerChip, title: peer.url, children: [_jsx("span", { className: css.peerHost, children: peer.url.replace(/^https?:\/\//, '').replace(/\/.*$/, '') }), typeof peer.score === 'number' ? _jsx("span", { className: css.peerScore, children: String(Math.round(peer.score)) }) : null] }, peer.url))) })), _jsx("div", { className: css.sectionTitle, children: t('a2a.activity') }), inFlight.length > 0 && (_jsx("div", { className: css.inFlightList, "aria-label": t('a2a.inFlight'), children: inFlight.map((route) => (_jsxs("div", { className: clsx(css.inFlightRow, Date.now() - route.startedAt > 120_000 && css.inFlightStale), title: Date.now() - route.startedAt > 120_000 ? t('a2a.inFlightStale') : undefined, children: [_jsx("span", { className: css.inFlightPulse, "aria-hidden": true }), _jsx("span", { className: clsx(css.activityDir, 'out'), children: '→' }), _jsx("span", { className: css.activityTeam, title: route.team, children: route.team }), _jsx("span", { className: css.activityPeer, children: route.peer === 'local' ? '' : route.peer.replace(/^https?:\/\//, '').replace(/\/.*$/, '') }), _jsx("span", { className: css.activityTime, children: relativeTime(route.startedAt) })] }, `${route.team}-${String(route.startedAt)}`))) })), activity.length === 0
228
- ? _jsx("div", { className: css.empty, children: t('a2a.activityEmpty') })
229
- : (_jsx("div", { className: css.activityList, children: [...activity].reverse().slice(0, 10).map((entry, index) => {
230
- // A same-host session team resolves to its session row;
231
- // clicking the activity line jumps there (the linkage
232
- // into an ongoing collaboration).
233
- const target = sessions.find(row => row.team === entry.team);
234
- const fresh = Date.now() - entry.ts < 5_000;
235
- return (_jsxs("div", { className: clsx(css.activityRow, fresh && css.fresh), "data-ok": entry.ok, "data-jump": target !== undefined, title: target !== undefined ? t('a2a.jump') : undefined, onClick: () => { if (target !== undefined)
236
- openSession(target.id); }, role: target !== undefined ? 'button' : undefined, children: [_jsx("span", { className: clsx(css.activityDir, entry.dir), children: entry.dir === 'in' ? '←' : '→' }), _jsx("span", { className: css.activityTeam, title: entry.team, children: entry.team }), _jsx("span", { className: css.activityPeer, title: entry.peer, children: entry.peer === '' ? '' : entry.peer.replace(/^https?:\/\//, '').replace(/\/.*$/, '') }), _jsx("span", { className: css.activityTime, children: relativeTime(entry.ts) })] }, `${String(entry.ts)}-${String(index)}`));
237
- }) }))] }))
121
+ // 0.6.10: the receive face of click-to-reach (0.6.7 BroadcastChannel,
122
+ // 0.6.8 ?dsh-a2a-open deep link) is removed by ruling — 禁止任何跳转,
123
+ // 全部使用响应式布局. The workbench's 快派 form is the only cross-surface
124
+ // reach primitive and it stays on the workbench page; this panel's
125
+ // FeedRow openSession remains (it switches a conversation in place inside
126
+ // the GUI — no navigation).
127
+ const { sessions, activity, inFlight, tasks } = state;
128
+ return (_jsxs("div", { ref: wrap, className: clsx(css.wrap, wide ? css.wide : css.rail), children: [_jsx(Button, { variant: "ghost", size: "sm", title: t('a2a.label'), "aria-label": t('a2a.label'), "aria-expanded": open, onClick: () => { toggleOpen(); }, children: _jsxs("span", { className: css.trigger, children: [_jsx(IconGlobeOutlineRegular, {}), wide ? _jsx("span", { children: t('a2a.label') }) : null, unread > 0 ? _jsx("span", { className: css.unreadBadge, "aria-label": String(unread), children: unread > 9 ? '9+' : String(unread) }) : null] }) }), open
129
+ ? (_jsxs("div", { className: css.popover, role: "dialog", "aria-label": t('a2a.title'), style: anchor === null ? undefined : { left: `${String(anchor.left)}px`, bottom: `${String(anchor.bottom)}px` }, children: [_jsxs("div", { className: css.title, children: [t('a2a.title'), state.version !== undefined ? _jsxs("span", { className: css.versionTag, title: state.version, children: ["v", state.version] }) : null] }), _jsxs("div", { className: css.stageRow, children: [_jsx("span", { className: css.stageTitle, children: t('a2a.stageTitle') }), _jsx("span", { className: css.stageLinks, children: _jsx("a", { className: css.stageLink, href: "/__dsh_a2a_nexus/", target: "_blank", rel: "noreferrer", title: t('a2a.stageWorkbench'), children: t('a2a.stageWorkbench') }) })] }), _jsxs("div", { "aria-live": "polite", children: [inFlight.length > 0 && (_jsx("div", { className: css.inFlightList, "aria-label": t('a2a.inFlight'), children: inFlight.map((route) => (_jsxs("div", { className: clsx(css.inFlightRow, Date.now() - route.startedAt > 120_000 && css.inFlightStale), title: Date.now() - route.startedAt > 120_000 ? t('a2a.inFlightStale') : undefined, children: [_jsx("span", { className: css.inFlightPulse, "aria-hidden": true }), _jsx(FeedRow, { dir: "out", team: route.team, peer: route.peer, ts: route.startedAt, openSession: openSession, t: t })] }, `${route.team}-${String(route.startedAt)}`))) })), (() => {
130
+ // C-ruling: render only settleable debt; the stale tail is a
131
+ // count line, never a row-per-noise listing.
132
+ const settleableTasks = tasks.filter(task => task.settleable !== false);
133
+ const staleCount = state.tasksStaleCount ?? (tasks.length - settleableTasks.length);
134
+ if (settleableTasks.length === 0 && staleCount === 0)
135
+ return null;
136
+ return (_jsxs(_Fragment, { children: [settleableTasks.length > 0 && (_jsxs(_Fragment, { children: [_jsx("div", { className: css.sectionTitle, children: t('a2a.tasks') }), _jsx("div", { className: css.inFlightList, "aria-label": t('a2a.tasks'), children: settleableTasks.map((task) => (_jsx("div", { className: css.inFlightRow, title: t('a2a.tasksNote'), children: _jsx(FeedRow, { dir: "out", team: task.team, peer: task.peer, ts: task.startedAt, openSession: openSession, t: t }) }, task.taskId))) })] })), staleCount > 0 && (_jsxs("div", { className: css.staleNote, title: t('a2a.tasksNote'), children: [t('a2a.tasksStale'), " \u00B7 ", String(staleCount)] }))] }));
137
+ })(), _jsx("div", { className: css.sectionTitle, children: t('a2a.activity') }), activity.length === 0
138
+ ? _jsx("div", { className: css.empty, children: t('a2a.activityEmpty') })
139
+ : (_jsx("div", { className: css.activityList, children: [...activity].reverse().slice(0, 10).map((entry, index) => {
140
+ // A same-host session team resolves to its session row;
141
+ // clicking the activity line jumps there (the linkage
142
+ // into an ongoing collaboration).
143
+ const target = sessions.find(row => row.team === entry.team);
144
+ return (_jsx(FeedRow, { dir: entry.dir, team: entry.team, peer: entry.peer, ts: entry.ts, ok: entry.ok, ...(target !== undefined ? { target } : {}), openSession: openSession, t: t }, `${String(entry.ts)}-${String(index)}`));
145
+ }) }))] })] }))
238
146
  : null] }));
239
147
  }
@@ -2,54 +2,30 @@
2
2
  /** Simplified Chinese dictionary. */
3
3
  export const zh = {
4
4
  'a2a.label': 'A2A 网络',
5
- 'a2a.title': 'A2A 网络面板',
6
- 'a2a.empty': '没有可加入的会话',
7
- 'a2a.join': '加入',
8
- 'a2a.leave': '退出',
9
- 'a2a.cold': '未加载(重启后待唤醒)',
10
- 'a2a.wake': '唤醒',
11
- 'a2a.note': '只有加入的会话才会被网络中的其他节点发现;点击会话行可跳转。',
12
- 'a2a.remote': '远程节点(按 host 分组)',
13
- 'a2a.remoteUnknownOrigin': '未知来源',
14
- 'a2a.peers': '对等节点',
15
- 'a2a.peersEmpty': '尚无已知对等节点(配 peers 种子入网)',
16
- 'a2a.activity': '路由活动',
17
- 'a2a.activityEmpty': '暂无路由记录',
5
+ 'a2a.title': 'A2A 网络动态',
6
+ 'a2a.activity': '最近动态',
7
+ 'a2a.activityEmpty': '暂无网络动态——加入网络即可开始协作',
18
8
  'a2a.inFlight': '进行中的路由',
19
9
  'a2a.inFlightStale': '等待回执超时(对端处理慢或回执丢失);180 秒后自动解除并按已送达处理',
10
+ 'a2a.tasks': '欠回执的异步任务',
11
+ 'a2a.tasksNote': '异步欠账分三段:未决(等回执)→ 死信(超期自动标注,目标恢复后仍可补回执结算)→ 归档(结算留痕不蒸发)',
12
+ 'a2a.tasksStale': '对话式已送达未答(非欠账,24h 自动死信)',
20
13
  'a2a.jump': '点击跳转到该会话',
21
- 'a2a.search': '搜索会话(名称 / team / 摘要)',
22
- 'a2a.searchEmpty': '没有匹配的会话',
23
- 'a2a.group': '设置分组',
24
- 'a2a.groupDefault': '未分组',
25
- 'a2a.groupClear': '清除分组',
26
- 'a2a.groupNew': '新建分组…',
27
- 'a2a.groupNewGo': '创建并归入',
14
+ 'a2a.stageTitle': 'A2A 工作台(目录 · 台账 · 活动 · 规划)',
15
+ 'a2a.stageWorkbench': '打开工作台',
28
16
  };
29
17
  /** English dictionary. */
30
18
  export const en = {
31
19
  'a2a.label': 'A2A network',
32
- 'a2a.title': 'A2A network panel',
33
- 'a2a.empty': 'No joinable sessions',
34
- 'a2a.join': 'Join',
35
- 'a2a.leave': 'Leave',
36
- 'a2a.cold': 'not loaded (waiting to wake after a restart)',
37
- 'a2a.wake': 'Wake',
38
- 'a2a.note': 'Only joined sessions are discoverable by other nodes; click a row to open the session.',
39
- 'a2a.remote': 'Remote teams (by host)',
40
- 'a2a.remoteUnknownOrigin': 'Unknown origin',
41
- 'a2a.peers': 'Peers',
42
- 'a2a.peersEmpty': 'No known peers yet (set peers seeds to join the mesh)',
43
- 'a2a.activity': 'Routing activity',
44
- 'a2a.activityEmpty': 'No routes yet',
20
+ 'a2a.title': 'A2A network activity',
21
+ 'a2a.activity': 'Recent activity',
22
+ 'a2a.activityEmpty': 'No network activity yet — join sessions to the network to start collaborating',
45
23
  'a2a.inFlight': 'Routes in flight',
46
24
  'a2a.inFlightStale': 'Reply wait past 120s (slow or lost receipt); auto-releases as delivered at 180s',
25
+ 'a2a.tasks': 'Owed receipts',
26
+ 'a2a.tasksNote': 'Async debts in three tiers: pending (awaiting receipt) → dead-lettered (auto-flagged past the stale TTL; a revived target can still settle late) → archived (settled records kept for audit)',
27
+ 'a2a.tasksStale': 'Delivered conversationally, no formal receipt (not debt; auto-dead-letters at the TTL)',
47
28
  'a2a.jump': 'Click to open this session',
48
- 'a2a.search': 'Search sessions (name / team / excerpt)',
49
- 'a2a.searchEmpty': 'No sessions match',
50
- 'a2a.group': 'Set group',
51
- 'a2a.groupDefault': 'Ungrouped',
52
- 'a2a.groupClear': 'Clear group',
53
- 'a2a.groupNew': 'New group…',
54
- 'a2a.groupNewGo': 'Create & assign',
29
+ 'a2a.stageTitle': 'A2A workbench (directory · ledger · activity · plan)',
30
+ 'a2a.stageWorkbench': 'Open workbench',
55
31
  };