@nelsonlongxiang/dsh-open-a2a-net 0.6.41 → 0.6.43
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/README.md +2 -1
- package/README_zh.md +2 -1
- package/assets/nexusDist/assets/index-CaA76j0F.js +9 -0
- package/assets/nexusDist/index.html +1 -1
- package/assets/stageDist/assets/index-Bj7S2pA1.css +1 -1
- package/assets/stageDist/assets/index-C-28k_o5.js +974 -974
- package/assets/stageDist/index.html +12 -12
- package/cli/a2a.mjs +24 -4
- package/lib/canvas-store.js +249 -17
- package/lib/client.js +62 -72
- package/lib/client.js.map +1 -1
- package/lib/config.js +1 -0
- package/lib/faces/admin.js +103 -63
- package/lib/faces/wire.js +3 -1
- package/lib/index.js +116 -22
- package/lib/origin-claim.js +4 -0
- package/lib/skill/SKILL.md +117 -117
- package/lib/skill/references/collaboration.md +428 -428
- package/lib/skill/references/discovery.md +78 -78
- package/lib/skill/references/intake.md +107 -107
- package/lib/skill/references/membership.md +73 -73
- package/lib/skill/references/pitfalls.md +51 -51
- package/lib/skill/references/router.md +96 -96
- package/lib/skill/references/routing.md +65 -65
- package/lib/types/canvas-store.d.ts +48 -1
- package/lib/types/config.d.ts +2 -0
- package/lib/types/faces/admin.d.ts +2 -0
- package/lib/types/origin-claim.d.ts +2 -0
- package/nexus-stage/package.json +21 -21
- package/nexus-stage/scripts/mockhost.py +261 -261
- package/nexus-stage/src/canvas-ops.ts +191 -191
- package/nexus-stage/src/canvas-wire.ts +164 -164
- package/nexus-stage/src/cli-home-picker.ts +42 -42
- package/nexus-stage/src/fault.ts +56 -56
- package/nexus-stage/src/federation-overlay.ts +103 -103
- package/nexus-stage/src/federation.ts +241 -241
- package/nexus-stage/src/layout-doc.ts +145 -145
- package/nexus-stage/src/main.ts +520 -513
- package/nexus-stage/src/planning-card-text.ts +73 -73
- package/nexus-stage/src/planning-card.ts +63 -63
- package/nexus-stage/src/planning-edges.ts +26 -26
- package/nexus-stage/src/planning-fit.ts +45 -45
- package/nexus-stage/src/planning-frame-hue.ts +13 -13
- package/nexus-stage/src/planning-legend.ts +39 -39
- package/nexus-stage/src/planning-member-handles.ts +61 -61
- package/nexus-stage/src/planning-menus.ts +422 -422
- package/nexus-stage/src/planning-mirror.ts +75 -75
- package/nexus-stage/src/planning-nodelist.ts +294 -294
- package/nexus-stage/src/planning-overlap.ts +159 -159
- package/nexus-stage/src/planning-reachability.ts +28 -28
- package/nexus-stage/src/planning-relayout.ts +121 -121
- package/nexus-stage/src/planning-roster.ts +15 -15
- package/nexus-stage/src/planning-search.ts +168 -168
- package/nexus-stage/src/planning-types.ts +115 -115
- package/nexus-stage/src/planning-undo.ts +68 -68
- package/nexus-stage/src/planning-view.ts +1499 -1499
- package/nexus-stage/src/planning.css +340 -340
- package/nexus-stage/src/seat-name-editor.ts +31 -31
- package/nexus-stage/src/session-drag.ts +64 -64
- package/nexus-stage/src/viewport.ts +106 -106
- package/nexus-stage/src/wb-chat.ts +684 -684
- package/nexus-stage/src/wb-directory.ts +237 -237
- package/nexus-stage/src/wb-ledger.ts +279 -279
- package/nexus-stage/src/wb-names.ts +43 -43
- package/nexus-stage/src/wb-rail.ts +147 -147
- package/nexus-stage/src/workbench.css +388 -388
- package/nexus-stage/src/workbench.ts +1294 -1272
- package/nexus-stage/src/world.ts +509 -509
- package/nexus-stage/tsconfig.json +24 -24
- package/nexus-stage/vite.config.ts +15 -15
- package/package.json +1 -1
- package/assets/nexusDist/assets/index-NkNSeIx6.js +0 -9
|
@@ -1,241 +1,241 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Federation overlay geometry for the 2D planning canvas (PR D 活动/联邦
|
|
3
|
-
* 层): where peer badges sit, which accent edges carry the pending a2a
|
|
4
|
-
* routes, and where the indigo federal referral lines attach. Everything
|
|
5
|
-
* is derived from the polling state (`/__dsh_a2a/state` -> inFlight,
|
|
6
|
-
* peers) plus the team-frame anchors, so the DOM layer only draws what
|
|
7
|
-
* these functions return - same derivation discipline as edges.ts.
|
|
8
|
-
*
|
|
9
|
-
* Pure: no DOM, no three, no clock. `now` is always a parameter so the
|
|
10
|
-
* suite pins time deterministically. All coordinates are world-space;
|
|
11
|
-
* the transformed #world container carries them to the screen for free.
|
|
12
|
-
* @module nexus-stage/federation
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { WorldBounds } from './viewport'
|
|
16
|
-
|
|
17
|
-
/** One pending outbound route from /state `inFlight`. */
|
|
18
|
-
export interface InFlightRow {
|
|
19
|
-
team: string
|
|
20
|
-
/** peer as `host:port` (no scheme) - the badge url carries the scheme. */
|
|
21
|
-
peer: string
|
|
22
|
-
startedAt: number
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** One federation peer from /state `peers`. */
|
|
26
|
-
export interface PeerRow {
|
|
27
|
-
url: string
|
|
28
|
-
score?: number
|
|
29
|
-
/** gh#20 W-A: this host's link view — 'linked' (operator-established
|
|
30
|
-
* basis holds) | 'stranger' (no link); absent = the host predates the
|
|
31
|
-
* field (render neutral). */
|
|
32
|
-
link?: 'linked' | 'stranger'
|
|
33
|
-
/** Signature-valid cached card held (the badge tick/cross). */
|
|
34
|
-
verified?: boolean
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** A team frame's titlebar anchor {fx + fw/2, fy + 11} (edges.ts lockstep). */
|
|
38
|
-
export interface FrameAnchor {
|
|
39
|
-
name: string
|
|
40
|
-
x: number
|
|
41
|
-
y: number
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** A peer badge's resolved slot in the placement column. */
|
|
45
|
-
export interface PeerPlacement {
|
|
46
|
-
url: string
|
|
47
|
-
score?: number
|
|
48
|
-
x: number
|
|
49
|
-
y: number
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** One derived overlay edge, ready for the SVG / label layers. */
|
|
53
|
-
/** Node id for a peer endpoint: `peer-<host>:<port>` (≤128, no '/').
|
|
54
|
-
* Unparseable urls keep their raw text, reduced to id-safe characters —
|
|
55
|
-
* attacker-shaped urls collapse to one id instead of poisoning selectors. */
|
|
56
|
-
export function peerNodeId(url: string): string {
|
|
57
|
-
const host = peerHostOf(url).replace(/[^A-Za-z0-9.:\[\]-]/g, '-')
|
|
58
|
-
return `peer-${host}`
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Bare host:port of a peer url (scheme stripped; unparseable urls pass through). */
|
|
62
|
-
export function peerHostOf(url: string): string {
|
|
63
|
-
try {
|
|
64
|
-
return new URL(url).host
|
|
65
|
-
} catch {
|
|
66
|
-
return url
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** One remote team row as the directory reports it (state.remote). */
|
|
71
|
-
export interface RemoteTeamRow {
|
|
72
|
-
team: string
|
|
73
|
-
unreachable?: boolean
|
|
74
|
-
name?: string
|
|
75
|
-
via?: string
|
|
76
|
-
workspace?: string
|
|
77
|
-
/** Publishing node's label + LAN IP — the host grouping dimension. */
|
|
78
|
-
origin?: string
|
|
79
|
-
/** Roster teams the publishing node declared for this handle (card teamMemberships). */
|
|
80
|
-
teams?: readonly string[]
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Group remote team rows by the peer endpoint they arrived through. Rows
|
|
85
|
-
* without a usable `via` land under '' (skipped by peer card rendering).
|
|
86
|
-
* Key = bare host:port.
|
|
87
|
-
*/
|
|
88
|
-
export function groupRemoteTeams(rows: ReadonlyArray<RemoteTeamRow>): Map<string, RemoteTeamRow[]> {
|
|
89
|
-
const out = new Map<string, RemoteTeamRow[]>()
|
|
90
|
-
for (const row of rows) {
|
|
91
|
-
const via = row.via !== undefined && row.via !== '' ? peerHostOf(row.via) : ''
|
|
92
|
-
const list = out.get(via) ?? []
|
|
93
|
-
list.push(row)
|
|
94
|
-
out.set(via, list)
|
|
95
|
-
}
|
|
96
|
-
return out
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export interface ActivityEdge {
|
|
100
|
-
x1: number
|
|
101
|
-
y1: number
|
|
102
|
-
x2: number
|
|
103
|
-
y2: number
|
|
104
|
-
label: string
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Badge column x-offset right of the frames' content bounds (maxX + gap). */
|
|
108
|
-
export const PEER_COLUMN_GAP = 140
|
|
109
|
-
/** Fallback column x while the canvas has no frames to anchor against. */
|
|
110
|
-
export const PEER_COLUMN_FALLBACK_X = 200
|
|
111
|
-
/** Vertical pitch between badges in the column. */
|
|
112
|
-
export const PEER_COLUMN_STEP = 90
|
|
113
|
-
/** Federal-line origin gap left of the content bounds (minX - gap). */
|
|
114
|
-
/** Fallback federal anchor offset (null bounds): x = -GAP. */
|
|
115
|
-
export const FEDERAL_ANCHOR_GAP = 80
|
|
116
|
-
/** Gutter gap between the card field's right edge and the federal anchor
|
|
117
|
-
* (bounds.maxX + GUTTER) — keeps the short stubs off the card field. */
|
|
118
|
-
export const FEDERAL_GUTTER_GAP = 20
|
|
119
|
-
/** Padding around every badge point when computing the fly region. */
|
|
120
|
-
export const FLY_PAD = 60
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Lay the peer badges out in a deterministic column: one x for all,
|
|
124
|
-
* y stacked from the content bounds' top at a fixed 90px pitch, in the
|
|
125
|
-
* peers array's order. Recomputed every reconcile - the column tracks
|
|
126
|
-
* the content, never the mouse. Null bounds (empty canvas) fall back to
|
|
127
|
-
* a fixed origin so first paint is still deterministic.
|
|
128
|
-
*/
|
|
129
|
-
export function placePeers(
|
|
130
|
-
peers: ReadonlyArray<PeerRow>,
|
|
131
|
-
bounds: WorldBounds | null,
|
|
132
|
-
): PeerPlacement[] {
|
|
133
|
-
const x = bounds !== null ? bounds.maxX + PEER_COLUMN_GAP : PEER_COLUMN_FALLBACK_X
|
|
134
|
-
const y0 = bounds !== null ? bounds.minY : 0
|
|
135
|
-
return peers.map((peer, i) => ({
|
|
136
|
-
url: peer.url,
|
|
137
|
-
score: peer.score,
|
|
138
|
-
x,
|
|
139
|
-
y: y0 + i * PEER_COLUMN_STEP,
|
|
140
|
-
}))
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* One accent edge per pending outbound route: from the route's team
|
|
145
|
-
* frame titlebar anchor to the matching peer badge, labelled with the
|
|
146
|
-
* route and its whole-second age. Rows whose team has no frame or whose
|
|
147
|
-
* peer matches no badge are skipped - dangling rows are a polling
|
|
148
|
-
* transient, not an error. Peer match is by suffix (`host:port` against
|
|
149
|
-
* `scheme://host:port`), first badge in array order wins.
|
|
150
|
-
*/
|
|
151
|
-
export function activityEdges(
|
|
152
|
-
inFlight: ReadonlyArray<InFlightRow>,
|
|
153
|
-
anchors: ReadonlyMap<string, FrameAnchor>,
|
|
154
|
-
peers: ReadonlyArray<PeerPlacement>,
|
|
155
|
-
now: number,
|
|
156
|
-
): ActivityEdge[] {
|
|
157
|
-
// Merge identical (team, peer) routes into ONE edge: N concurrent routes
|
|
158
|
-
// over the same span would stack perfectly opaque duplicates. The label
|
|
159
|
-
// counts them (×N) and reports the OLDEST route's age — the one the user
|
|
160
|
-
// has been waiting on longest.
|
|
161
|
-
const merged = new Map<string, { team: string; peer: string; startedAt: number; count: number }>()
|
|
162
|
-
for (const row of inFlight) {
|
|
163
|
-
if (!anchors.has(row.team)) continue
|
|
164
|
-
const key = `${row.team}\u0000${row.peer}`
|
|
165
|
-
const cur = merged.get(key)
|
|
166
|
-
if (cur === undefined) merged.set(key, { team: row.team, peer: row.peer, startedAt: row.startedAt, count: 1 })
|
|
167
|
-
else {
|
|
168
|
-
cur.startedAt = Math.min(cur.startedAt, row.startedAt)
|
|
169
|
-
cur.count += 1
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const out: ActivityEdge[] = []
|
|
174
|
-
for (const g of merged.values()) {
|
|
175
|
-
const anchor = anchors.get(g.team)
|
|
176
|
-
if (anchor === undefined) continue
|
|
177
|
-
// Match by parsed host:port — a bare endsWith would let 'badhost:port'
|
|
178
|
-
// satisfy a 'host:port' route.
|
|
179
|
-
const badge = peers.find(p => { try { return new URL(p.url).host === g.peer } catch { return false } })
|
|
180
|
-
if (badge === undefined) continue
|
|
181
|
-
const elapsed = Math.max(0, Math.round((now - g.startedAt) / 1000))
|
|
182
|
-
out.push({
|
|
183
|
-
x1: anchor.x,
|
|
184
|
-
y1: anchor.y,
|
|
185
|
-
x2: badge.x,
|
|
186
|
-
y2: badge.y,
|
|
187
|
-
label: `a2a_route · ${g.peer} · ${elapsed}s` + (g.count > 1 ? ` ×${g.count}` : ''),
|
|
188
|
-
})
|
|
189
|
-
}
|
|
190
|
-
return out
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Federal referral lines: from a content-bounds anchor (minX - 80,
|
|
195
|
-
* vertical center) out to every peer badge. Only the first line is
|
|
196
|
-
* labelled "gns referral" - one legend row, not one label per peer.
|
|
197
|
-
* Null bounds anchors the fan at the fixed fallback origin.
|
|
198
|
-
*/
|
|
199
|
-
export function federalEdges(
|
|
200
|
-
peers: ReadonlyArray<PeerPlacement>,
|
|
201
|
-
bounds: WorldBounds | null,
|
|
202
|
-
): ActivityEdge[] {
|
|
203
|
-
// Gutter anchor: between the card field's right edge and the badge column,
|
|
204
|
-
// at the badges' vertical mid — short stubs that cannot cross any card
|
|
205
|
-
// (an anchor at the content's left middle raked long diagonals through
|
|
206
|
-
// the whole field: the reported wiring anomaly).
|
|
207
|
-
const anchor = bounds !== null
|
|
208
|
-
? { x: bounds.maxX + FEDERAL_GUTTER_GAP, y: peers.length > 0 ? (peers[0]!.y + peers[peers.length - 1]!.y) / 2 : (bounds.minY + bounds.maxY) / 2 }
|
|
209
|
-
: { x: -FEDERAL_ANCHOR_GAP, y: 0 }
|
|
210
|
-
return peers.map((peer, i) => ({
|
|
211
|
-
x1: anchor.x,
|
|
212
|
-
y1: anchor.y,
|
|
213
|
-
x2: peer.x,
|
|
214
|
-
y2: peer.y,
|
|
215
|
-
label: i === 0 ? 'gns referral' : '',
|
|
216
|
-
}))
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* The fly region: frames' content bounds unioned with every badge point
|
|
221
|
-
* padded by FLY_PAD, so a fit over it frames the whole federation
|
|
222
|
-
* (canvas + column) with breathing room. Null only when there is
|
|
223
|
-
* nothing at all to fly to.
|
|
224
|
-
*/
|
|
225
|
-
export function flyBounds(
|
|
226
|
-
bounds: WorldBounds | null,
|
|
227
|
-
peers: ReadonlyArray<PeerPlacement>,
|
|
228
|
-
): WorldBounds | null {
|
|
229
|
-
if (bounds === null && peers.length === 0) return null
|
|
230
|
-
let minX = bounds !== null ? bounds.minX : Number.POSITIVE_INFINITY
|
|
231
|
-
let minY = bounds !== null ? bounds.minY : Number.POSITIVE_INFINITY
|
|
232
|
-
let maxX = bounds !== null ? bounds.maxX : Number.NEGATIVE_INFINITY
|
|
233
|
-
let maxY = bounds !== null ? bounds.maxY : Number.NEGATIVE_INFINITY
|
|
234
|
-
for (const peer of peers) {
|
|
235
|
-
minX = Math.min(minX, peer.x - FLY_PAD)
|
|
236
|
-
minY = Math.min(minY, peer.y - FLY_PAD)
|
|
237
|
-
maxX = Math.max(maxX, peer.x + FLY_PAD)
|
|
238
|
-
maxY = Math.max(maxY, peer.y + FLY_PAD)
|
|
239
|
-
}
|
|
240
|
-
return { minX, minY, maxX, maxY }
|
|
241
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Federation overlay geometry for the 2D planning canvas (PR D 活动/联邦
|
|
3
|
+
* 层): where peer badges sit, which accent edges carry the pending a2a
|
|
4
|
+
* routes, and where the indigo federal referral lines attach. Everything
|
|
5
|
+
* is derived from the polling state (`/__dsh_a2a/state` -> inFlight,
|
|
6
|
+
* peers) plus the team-frame anchors, so the DOM layer only draws what
|
|
7
|
+
* these functions return - same derivation discipline as edges.ts.
|
|
8
|
+
*
|
|
9
|
+
* Pure: no DOM, no three, no clock. `now` is always a parameter so the
|
|
10
|
+
* suite pins time deterministically. All coordinates are world-space;
|
|
11
|
+
* the transformed #world container carries them to the screen for free.
|
|
12
|
+
* @module nexus-stage/federation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { WorldBounds } from './viewport'
|
|
16
|
+
|
|
17
|
+
/** One pending outbound route from /state `inFlight`. */
|
|
18
|
+
export interface InFlightRow {
|
|
19
|
+
team: string
|
|
20
|
+
/** peer as `host:port` (no scheme) - the badge url carries the scheme. */
|
|
21
|
+
peer: string
|
|
22
|
+
startedAt: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One federation peer from /state `peers`. */
|
|
26
|
+
export interface PeerRow {
|
|
27
|
+
url: string
|
|
28
|
+
score?: number
|
|
29
|
+
/** gh#20 W-A: this host's link view — 'linked' (operator-established
|
|
30
|
+
* basis holds) | 'stranger' (no link); absent = the host predates the
|
|
31
|
+
* field (render neutral). */
|
|
32
|
+
link?: 'linked' | 'stranger'
|
|
33
|
+
/** Signature-valid cached card held (the badge tick/cross). */
|
|
34
|
+
verified?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A team frame's titlebar anchor {fx + fw/2, fy + 11} (edges.ts lockstep). */
|
|
38
|
+
export interface FrameAnchor {
|
|
39
|
+
name: string
|
|
40
|
+
x: number
|
|
41
|
+
y: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A peer badge's resolved slot in the placement column. */
|
|
45
|
+
export interface PeerPlacement {
|
|
46
|
+
url: string
|
|
47
|
+
score?: number
|
|
48
|
+
x: number
|
|
49
|
+
y: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** One derived overlay edge, ready for the SVG / label layers. */
|
|
53
|
+
/** Node id for a peer endpoint: `peer-<host>:<port>` (≤128, no '/').
|
|
54
|
+
* Unparseable urls keep their raw text, reduced to id-safe characters —
|
|
55
|
+
* attacker-shaped urls collapse to one id instead of poisoning selectors. */
|
|
56
|
+
export function peerNodeId(url: string): string {
|
|
57
|
+
const host = peerHostOf(url).replace(/[^A-Za-z0-9.:\[\]-]/g, '-')
|
|
58
|
+
return `peer-${host}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Bare host:port of a peer url (scheme stripped; unparseable urls pass through). */
|
|
62
|
+
export function peerHostOf(url: string): string {
|
|
63
|
+
try {
|
|
64
|
+
return new URL(url).host
|
|
65
|
+
} catch {
|
|
66
|
+
return url
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** One remote team row as the directory reports it (state.remote). */
|
|
71
|
+
export interface RemoteTeamRow {
|
|
72
|
+
team: string
|
|
73
|
+
unreachable?: boolean
|
|
74
|
+
name?: string
|
|
75
|
+
via?: string
|
|
76
|
+
workspace?: string
|
|
77
|
+
/** Publishing node's label + LAN IP — the host grouping dimension. */
|
|
78
|
+
origin?: string
|
|
79
|
+
/** Roster teams the publishing node declared for this handle (card teamMemberships). */
|
|
80
|
+
teams?: readonly string[]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Group remote team rows by the peer endpoint they arrived through. Rows
|
|
85
|
+
* without a usable `via` land under '' (skipped by peer card rendering).
|
|
86
|
+
* Key = bare host:port.
|
|
87
|
+
*/
|
|
88
|
+
export function groupRemoteTeams(rows: ReadonlyArray<RemoteTeamRow>): Map<string, RemoteTeamRow[]> {
|
|
89
|
+
const out = new Map<string, RemoteTeamRow[]>()
|
|
90
|
+
for (const row of rows) {
|
|
91
|
+
const via = row.via !== undefined && row.via !== '' ? peerHostOf(row.via) : ''
|
|
92
|
+
const list = out.get(via) ?? []
|
|
93
|
+
list.push(row)
|
|
94
|
+
out.set(via, list)
|
|
95
|
+
}
|
|
96
|
+
return out
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ActivityEdge {
|
|
100
|
+
x1: number
|
|
101
|
+
y1: number
|
|
102
|
+
x2: number
|
|
103
|
+
y2: number
|
|
104
|
+
label: string
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Badge column x-offset right of the frames' content bounds (maxX + gap). */
|
|
108
|
+
export const PEER_COLUMN_GAP = 140
|
|
109
|
+
/** Fallback column x while the canvas has no frames to anchor against. */
|
|
110
|
+
export const PEER_COLUMN_FALLBACK_X = 200
|
|
111
|
+
/** Vertical pitch between badges in the column. */
|
|
112
|
+
export const PEER_COLUMN_STEP = 90
|
|
113
|
+
/** Federal-line origin gap left of the content bounds (minX - gap). */
|
|
114
|
+
/** Fallback federal anchor offset (null bounds): x = -GAP. */
|
|
115
|
+
export const FEDERAL_ANCHOR_GAP = 80
|
|
116
|
+
/** Gutter gap between the card field's right edge and the federal anchor
|
|
117
|
+
* (bounds.maxX + GUTTER) — keeps the short stubs off the card field. */
|
|
118
|
+
export const FEDERAL_GUTTER_GAP = 20
|
|
119
|
+
/** Padding around every badge point when computing the fly region. */
|
|
120
|
+
export const FLY_PAD = 60
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Lay the peer badges out in a deterministic column: one x for all,
|
|
124
|
+
* y stacked from the content bounds' top at a fixed 90px pitch, in the
|
|
125
|
+
* peers array's order. Recomputed every reconcile - the column tracks
|
|
126
|
+
* the content, never the mouse. Null bounds (empty canvas) fall back to
|
|
127
|
+
* a fixed origin so first paint is still deterministic.
|
|
128
|
+
*/
|
|
129
|
+
export function placePeers(
|
|
130
|
+
peers: ReadonlyArray<PeerRow>,
|
|
131
|
+
bounds: WorldBounds | null,
|
|
132
|
+
): PeerPlacement[] {
|
|
133
|
+
const x = bounds !== null ? bounds.maxX + PEER_COLUMN_GAP : PEER_COLUMN_FALLBACK_X
|
|
134
|
+
const y0 = bounds !== null ? bounds.minY : 0
|
|
135
|
+
return peers.map((peer, i) => ({
|
|
136
|
+
url: peer.url,
|
|
137
|
+
score: peer.score,
|
|
138
|
+
x,
|
|
139
|
+
y: y0 + i * PEER_COLUMN_STEP,
|
|
140
|
+
}))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* One accent edge per pending outbound route: from the route's team
|
|
145
|
+
* frame titlebar anchor to the matching peer badge, labelled with the
|
|
146
|
+
* route and its whole-second age. Rows whose team has no frame or whose
|
|
147
|
+
* peer matches no badge are skipped - dangling rows are a polling
|
|
148
|
+
* transient, not an error. Peer match is by suffix (`host:port` against
|
|
149
|
+
* `scheme://host:port`), first badge in array order wins.
|
|
150
|
+
*/
|
|
151
|
+
export function activityEdges(
|
|
152
|
+
inFlight: ReadonlyArray<InFlightRow>,
|
|
153
|
+
anchors: ReadonlyMap<string, FrameAnchor>,
|
|
154
|
+
peers: ReadonlyArray<PeerPlacement>,
|
|
155
|
+
now: number,
|
|
156
|
+
): ActivityEdge[] {
|
|
157
|
+
// Merge identical (team, peer) routes into ONE edge: N concurrent routes
|
|
158
|
+
// over the same span would stack perfectly opaque duplicates. The label
|
|
159
|
+
// counts them (×N) and reports the OLDEST route's age — the one the user
|
|
160
|
+
// has been waiting on longest.
|
|
161
|
+
const merged = new Map<string, { team: string; peer: string; startedAt: number; count: number }>()
|
|
162
|
+
for (const row of inFlight) {
|
|
163
|
+
if (!anchors.has(row.team)) continue
|
|
164
|
+
const key = `${row.team}\u0000${row.peer}`
|
|
165
|
+
const cur = merged.get(key)
|
|
166
|
+
if (cur === undefined) merged.set(key, { team: row.team, peer: row.peer, startedAt: row.startedAt, count: 1 })
|
|
167
|
+
else {
|
|
168
|
+
cur.startedAt = Math.min(cur.startedAt, row.startedAt)
|
|
169
|
+
cur.count += 1
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const out: ActivityEdge[] = []
|
|
174
|
+
for (const g of merged.values()) {
|
|
175
|
+
const anchor = anchors.get(g.team)
|
|
176
|
+
if (anchor === undefined) continue
|
|
177
|
+
// Match by parsed host:port — a bare endsWith would let 'badhost:port'
|
|
178
|
+
// satisfy a 'host:port' route.
|
|
179
|
+
const badge = peers.find(p => { try { return new URL(p.url).host === g.peer } catch { return false } })
|
|
180
|
+
if (badge === undefined) continue
|
|
181
|
+
const elapsed = Math.max(0, Math.round((now - g.startedAt) / 1000))
|
|
182
|
+
out.push({
|
|
183
|
+
x1: anchor.x,
|
|
184
|
+
y1: anchor.y,
|
|
185
|
+
x2: badge.x,
|
|
186
|
+
y2: badge.y,
|
|
187
|
+
label: `a2a_route · ${g.peer} · ${elapsed}s` + (g.count > 1 ? ` ×${g.count}` : ''),
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
return out
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Federal referral lines: from a content-bounds anchor (minX - 80,
|
|
195
|
+
* vertical center) out to every peer badge. Only the first line is
|
|
196
|
+
* labelled "gns referral" - one legend row, not one label per peer.
|
|
197
|
+
* Null bounds anchors the fan at the fixed fallback origin.
|
|
198
|
+
*/
|
|
199
|
+
export function federalEdges(
|
|
200
|
+
peers: ReadonlyArray<PeerPlacement>,
|
|
201
|
+
bounds: WorldBounds | null,
|
|
202
|
+
): ActivityEdge[] {
|
|
203
|
+
// Gutter anchor: between the card field's right edge and the badge column,
|
|
204
|
+
// at the badges' vertical mid — short stubs that cannot cross any card
|
|
205
|
+
// (an anchor at the content's left middle raked long diagonals through
|
|
206
|
+
// the whole field: the reported wiring anomaly).
|
|
207
|
+
const anchor = bounds !== null
|
|
208
|
+
? { x: bounds.maxX + FEDERAL_GUTTER_GAP, y: peers.length > 0 ? (peers[0]!.y + peers[peers.length - 1]!.y) / 2 : (bounds.minY + bounds.maxY) / 2 }
|
|
209
|
+
: { x: -FEDERAL_ANCHOR_GAP, y: 0 }
|
|
210
|
+
return peers.map((peer, i) => ({
|
|
211
|
+
x1: anchor.x,
|
|
212
|
+
y1: anchor.y,
|
|
213
|
+
x2: peer.x,
|
|
214
|
+
y2: peer.y,
|
|
215
|
+
label: i === 0 ? 'gns referral' : '',
|
|
216
|
+
}))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The fly region: frames' content bounds unioned with every badge point
|
|
221
|
+
* padded by FLY_PAD, so a fit over it frames the whole federation
|
|
222
|
+
* (canvas + column) with breathing room. Null only when there is
|
|
223
|
+
* nothing at all to fly to.
|
|
224
|
+
*/
|
|
225
|
+
export function flyBounds(
|
|
226
|
+
bounds: WorldBounds | null,
|
|
227
|
+
peers: ReadonlyArray<PeerPlacement>,
|
|
228
|
+
): WorldBounds | null {
|
|
229
|
+
if (bounds === null && peers.length === 0) return null
|
|
230
|
+
let minX = bounds !== null ? bounds.minX : Number.POSITIVE_INFINITY
|
|
231
|
+
let minY = bounds !== null ? bounds.minY : Number.POSITIVE_INFINITY
|
|
232
|
+
let maxX = bounds !== null ? bounds.maxX : Number.NEGATIVE_INFINITY
|
|
233
|
+
let maxY = bounds !== null ? bounds.maxY : Number.NEGATIVE_INFINITY
|
|
234
|
+
for (const peer of peers) {
|
|
235
|
+
minX = Math.min(minX, peer.x - FLY_PAD)
|
|
236
|
+
minY = Math.min(minY, peer.y - FLY_PAD)
|
|
237
|
+
maxX = Math.max(maxX, peer.x + FLY_PAD)
|
|
238
|
+
maxY = Math.max(maxY, peer.y + FLY_PAD)
|
|
239
|
+
}
|
|
240
|
+
return { minX, minY, maxX, maxY }
|
|
241
|
+
}
|