@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
@@ -0,0 +1,280 @@
1
+ /* Planning mode (2D infinite canvas) — ported from the approved wireframe
2
+ (feedback/2026-08-27-nexus-canvas-ux/prototype-v1.html) with the design
3
+ system's three-layer token discipline. Tokens are scoped to #dsh-plan
4
+ (token sovereignty: the stage owns its theme, detached from any host).
5
+ Deviations from the prototype, all for computable geometry:
6
+ - .frame-head is horizontally centered with a fixed 24px border-box
7
+ height, so the star-edge origin is exactly (fx + fw/2, fy + 11);
8
+ - .node has a fixed 56px border-box height matching world.NODE_H, with
9
+ nowrap/ellipsis so the card can never outgrow the edge math.
10
+ Layers (z): grid 0 < world 1 < marquee 2 < HUD 3. */
11
+
12
+ #dsh-plan *,#dsh-plan *::before,#dsh-plan *::after{box-sizing:border-box}
13
+
14
+ #dsh-plan{
15
+ /* primitive — bound to the workbench tokens (--n-*, workbench.css) so the
16
+ board follows the same dark/light theme as the rail; the literal after
17
+ each comma is the stage's own dark value, kept so the board still
18
+ renders on a host that ships without workbench.css (token sovereignty). */
19
+ --uupm-ink-0:var(--n-ink,#dde3f0); --uupm-ink-1:var(--n-ink-muted,#7a86a0);
20
+ --uupm-bg-0:var(--n-bg,#060a12); --uupm-bg-1:var(--n-panel,#0d1322); --uupm-bg-2:var(--n-raised,#131b2e);
21
+ --uupm-accent:var(--n-accent,#9673f7); --uupm-live:var(--n-live,#4ade80); --uupm-cold:var(--n-cold,#7a86a0);
22
+ --uupm-edge-member:var(--n-canvas,#22d3ee);
23
+ --uupm-warn:var(--n-act,#f59e0b); --uupm-error:var(--n-danger,#f87171); --uupm-indigo:var(--n-federal,#6366f1);
24
+ /* accent washes (hover 8% / active 12-18% / tint 5-10%), one definition each */
25
+ --uupm-wash-5:color-mix(in srgb,var(--uupm-accent) 5%,transparent);
26
+ --uupm-wash-8:color-mix(in srgb,var(--uupm-accent) 8%,transparent);
27
+ --uupm-wash-12:color-mix(in srgb,var(--uupm-accent) 12%,transparent);
28
+ --uupm-wash-15:color-mix(in srgb,var(--uupm-accent) 15%,transparent);
29
+ --uupm-wash-18:color-mix(in srgb,var(--uupm-accent) 18%,transparent);
30
+ /* semantic */
31
+ --surface:var(--uupm-bg-1); --ink:var(--uupm-ink-0); --ink-muted:var(--uupm-ink-1);
32
+ --accent:var(--uupm-accent); --edge-live:var(--uupm-live); --edge-cold:var(--uupm-cold);
33
+ --federal:var(--uupm-indigo);
34
+ /* component */
35
+ --card-bg:var(--surface); --card-line:var(--n-card-line,#5c6680); --card-line-hover:var(--uupm-ink-1);
36
+ --toolbar-bg:color-mix(in srgb,var(--uupm-bg-1) 92%,transparent);
37
+ --grid-dot:color-mix(in srgb,var(--uupm-ink-1) 22%,transparent);
38
+ --scrim:color-mix(in srgb,var(--uupm-bg-0) 60%,transparent);
39
+ --error-ink:var(--n-error-ink,#fecaca); --error-panel:var(--n-error-panel,#2a0d10f2);
40
+ }
41
+
42
+ #dsh-plan{
43
+ position:absolute; inset:0; overflow:hidden; background:var(--uupm-bg-0); color:var(--ink);
44
+ font-size:13px; line-height:1.5; outline:none;
45
+ }
46
+ #dsh-plan .mono{font-family:ui-monospace,Cascadia,Consolas,monospace}
47
+ #dsh-plan:focus-visible{outline:2px solid var(--accent); outline-offset:-2px}
48
+
49
+ /* ── dot-grid world layer: screen-fixed, positioned by JS from the viewport ── */
50
+ #dsh-plan .p-grid{position:absolute; inset:0; z-index:0; pointer-events:none;
51
+ background-image:radial-gradient(circle, var(--grid-dot) 1px, transparent 1px);
52
+ background-size:24px 24px; background-position:0 0}
53
+
54
+ /* ── transformed world container ── */
55
+ #dsh-plan .p-world{position:absolute; inset:0; z-index:1; transform-origin:0 0}
56
+ #dsh-plan .p-layer{position:absolute; inset:0; pointer-events:none}
57
+ #dsh-plan .p-edges{position:absolute; overflow:visible; width:100%; height:100%; pointer-events:none}
58
+ /* The team frame is a grouping surface, not an interactive one: left
59
+ event-transparent so real drags reach the member cards inside it (the
60
+ frame head opts back in for group-drag/menu/Delete). */
61
+ #dsh-plan .p-frame{pointer-events:none}
62
+ #dsh-plan .p-frame-head{pointer-events:auto}
63
+ #dsh-plan .p-node{pointer-events:auto}
64
+ #dsh-plan .e-member{stroke:var(--uupm-edge-member); stroke-opacity:.38; stroke-width:1.5}
65
+ #dsh-plan .e-member.dashed{stroke-dasharray:3 4}
66
+
67
+ /* ── team frame ── */
68
+ /* --frame-line is the team hue set inline per frame (frameHue); --frame-ink is
69
+ what the frame actually paints with — the hue itself on dark, shaded 72%
70
+ toward black on light, where amber/lime/teal outlines fell to 1.8-2.2:1. */
71
+ #dsh-plan .p-frame{--frame-ink:var(--frame-line,#8B5CF6)}
72
+ @media (prefers-color-scheme: light){
73
+ #dsh-plan .p-frame{--frame-ink:color-mix(in srgb,var(--frame-line,#8B5CF6) 72%,black)}
74
+ }
75
+ #dsh-plan .p-frame{position:absolute; border:1px solid var(--frame-ink); border-radius:10px;
76
+ background:var(--frame-bg,var(--uupm-wash-5)); min-width:120px; min-height:60px}
77
+ #dsh-plan .p-frame-head{position:absolute; top:-13px; left:50%; transform:translateX(-50%);
78
+ height:24px; padding:0 10px; border-radius:6px; display:flex; gap:8px; align-items:center;
79
+ background:var(--uupm-bg-2); border:1px solid var(--frame-ink);
80
+ font-size:12px; font-weight:600; letter-spacing:.02em; white-space:nowrap; cursor:grab}
81
+ #dsh-plan .p-frame-head:active{cursor:grabbing}
82
+ #dsh-plan .p-frame-head:hover{border-color:var(--card-line-hover)}
83
+ #dsh-plan .p-frame-head .cnt{color:var(--ink-muted); font-weight:400}
84
+ #dsh-plan .p-frame-head .route{font-size:10.5px; color:var(--ink-muted)}
85
+
86
+ /* ── node card (fixed 56px border-box height = world.NODE_H) ── */
87
+ #dsh-plan .p-node{position:absolute; width:172px; height:56px; padding:8px 26px 8px 14px;
88
+ background:var(--card-bg); border:1px solid var(--card-line); border-left:3px solid var(--edge-live);
89
+ border-radius:8px; cursor:grab; user-select:none;
90
+ transition:border-color 120ms cubic-bezier(0.2,0,0,1), transform 120ms cubic-bezier(0.2,0,0,1)}
91
+ #dsh-plan .p-node:hover{border-color:var(--card-line-hover)}
92
+ #dsh-plan .p-node:active{cursor:grabbing}
93
+ #dsh-plan .p-node:focus-visible{outline:2px solid var(--accent); outline-offset:2px}
94
+ #dsh-plan .p-node.selected{outline:2px solid var(--accent); outline-offset:1px}
95
+ #dsh-plan .p-node.dragging{transform:scale(1.02); border-color:var(--accent)}
96
+ #dsh-plan .p-node.cold{border-left-color:var(--edge-cold)}
97
+ #dsh-plan .p-node .nm{font-weight:600; display:flex; align-items:center; gap:7px;
98
+ overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
99
+ /* The name span clips with an ellipsis (min-width:0 lets it shrink inside
100
+ the flex row) — without it a long peer address was hard-cut mid-digit. */
101
+ #dsh-plan .p-node .nm-text{min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
102
+ #dsh-plan .p-node .dot{width:8px; height:8px; border-radius:50%; background:var(--edge-live); flex:none}
103
+ #dsh-plan .p-node.cold .dot{background:transparent; border:1.5px solid var(--edge-cold)}
104
+ #dsh-plan .p-node .sub{font-size:11px; color:var(--ink-muted); margin-top:2px;
105
+ overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
106
+ #dsh-plan .p-node .prio{position:absolute; top:6px; right:8px; font-size:10px; color:var(--ink-muted);
107
+ border:1px solid var(--card-line); border-radius:4px; padding:0 5px}
108
+
109
+ /* ── marquee ── */
110
+ #dsh-plan .p-marquee{position:absolute; z-index:2; display:none; pointer-events:none;
111
+ border:1px solid var(--accent); background:var(--uupm-wash-8)}
112
+
113
+ /* ── HUD ── */
114
+ #dsh-plan .p-toolbar{position:absolute; top:16px; left:16px; z-index:3; display:flex; gap:6px; align-items:center;
115
+ background:var(--toolbar-bg); border:1px solid var(--card-line); border-radius:8px; padding:6px 8px}
116
+ #dsh-plan .p-tabs{position:absolute; top:16px; left:50%; transform:translateX(-50%); z-index:3; display:flex; gap:4px;
117
+ background:var(--toolbar-bg); border:1px solid var(--card-line); border-radius:8px; padding:4px}
118
+ #dsh-plan .p-tabs [role=tab]{background:transparent; color:var(--ink-muted); border:1px solid transparent; border-radius:6px;
119
+ padding:4px 12px; font:inherit; cursor:pointer}
120
+ #dsh-plan .p-tabs [role=tab]:hover{color:var(--ink); border-color:var(--card-line)}
121
+ #dsh-plan .p-tabs [role=tab][aria-selected=true]{color:var(--ink); border-color:var(--accent); background:var(--uupm-wash-12)}
122
+ #dsh-plan .p-tabs [role=tab]:focus-visible{outline:2px solid var(--accent); outline-offset:1px}
123
+ #dsh-plan .p-toolbar button{background:transparent; color:var(--ink); border:1px solid var(--card-line); border-radius:6px;
124
+ padding:4px 10px; font:inherit; cursor:pointer; white-space:nowrap}
125
+ #dsh-plan .p-toolbar button:hover{border-color:var(--accent)}
126
+ #dsh-plan .p-toolbar button:active{background:var(--uupm-wash-15)}
127
+ #dsh-plan .p-toolbar button:focus-visible{outline:2px solid var(--accent); outline-offset:1px}
128
+ #dsh-plan .p-zoom{min-width:44px; text-align:center; font-size:11px; color:var(--ink-muted)}
129
+ #dsh-plan .p-lamp{display:flex; align-items:center; gap:6px; font-size:11px; color:var(--ink-muted); padding:0 6px}
130
+ #dsh-plan .p-lamp i{width:7px; height:7px; border-radius:50%; background:var(--ink-muted); transition:background 140ms ease}
131
+ #dsh-plan .p-lamp.saved i{background:var(--edge-live)}
132
+ #dsh-plan .p-lamp.pending i{background:var(--uupm-warn)}
133
+ #dsh-plan .p-lamp.error{color:var(--uupm-error); cursor:pointer}
134
+ #dsh-plan .p-lamp.error i{background:var(--uupm-error)}
135
+ #dsh-plan .p-lamp.error:hover{border:1px solid var(--uupm-error); border-radius:4px}
136
+
137
+ #dsh-plan .p-status{position:absolute; top:16px; right:16px; z-index:3; display:flex; gap:14px;
138
+ background:var(--toolbar-bg); border:1px solid var(--card-line); border-radius:8px; padding:8px 14px; font-size:12px}
139
+ #dsh-plan .p-status b{font-weight:600}
140
+ #dsh-plan .ok{color:var(--edge-live)} #dsh-plan .warn{color:var(--uupm-warn)} #dsh-plan .mut{color:var(--ink-muted)}
141
+
142
+ #dsh-plan .p-legend{position:absolute; bottom:16px; left:16px; z-index:3; font-size:11px; color:var(--ink-muted);
143
+ background:var(--toolbar-bg); border:1px solid var(--card-line); border-radius:8px; padding:8px 12px;
144
+ display:grid; gap:4px}
145
+ #dsh-plan .p-legend i{display:inline-block; width:18px; border-top:2px solid var(--uupm-edge-member);
146
+ vertical-align:middle; margin-right:6px}
147
+ #dsh-plan .p-legend i.dashed{border-top-style:dashed}
148
+
149
+ /* ── narrow board (<680px): the HUD stacks instead of colliding ──
150
+ The toolbar, status chip and legend are absolutely placed for a wide
151
+ board; on a phone they overlapped each other and the toolbar's CJK
152
+ labels wrapped one glyph per line. Toolbar scrolls sideways on one row,
153
+ the status chip drops under it, the legend (reference, not state) hides. */
154
+ @media (max-width:680px){
155
+ #dsh-plan .p-toolbar{left:8px; right:8px; top:8px; overflow-x:auto; scrollbar-width:none}
156
+ #dsh-plan .p-toolbar > *{flex:none}
157
+ #dsh-plan .p-status{left:8px; right:auto; top:58px; padding:6px 10px; gap:10px; flex-wrap:wrap}
158
+ #dsh-plan .p-legend{display:none}
159
+ }
160
+ @media (pointer:coarse){
161
+ #dsh-plan .p-toolbar button{min-height:36px; min-width:36px}
162
+ }
163
+
164
+ /* ── reduced motion: zero animation, state carried by text (hard baseline) ── */
165
+ @media (prefers-reduced-motion: reduce){
166
+ #dsh-plan *{transition:none !important; animation:none !important}
167
+ }
168
+
169
+ /* ── PR C: drop-target highlight, context menu, name dialog, notices ── */
170
+ #dsh-plan .p-frame.drop-target{border-color:var(--accent); border-width:2px; background:var(--uupm-wash-12)}
171
+ #dsh-plan .p-frame-head:focus-visible{outline:2px solid var(--accent); outline-offset:1px}
172
+
173
+ /* ── PR D: federation overlay — peer badges, activity/federal edges, status fly ── */
174
+ /* Peer cards: the 'peer' tag is ~36px wide against the 26px gutter a P0–P9
175
+ tag fits in, so it sat on top of the address text — widen the gutter. */
176
+ #dsh-plan .p-node.remote{border-left-color:var(--federal); padding-right:46px}
177
+ #dsh-plan .p-node.remote .dot{background:var(--federal)}
178
+ #dsh-plan .p-node.remote .prio{color:var(--ink)}
179
+ #dsh-plan .e-activity{stroke:var(--accent); stroke-width:2; stroke-dasharray:8 6; stroke-opacity:.9}
180
+ #dsh-plan .e-federal{stroke:var(--federal); stroke-width:1.5; stroke-dasharray:6 5; stroke-opacity:.5}
181
+ #dsh-plan .edge-label{font-size:10.5px; fill:var(--ink-muted)}
182
+ /* in-flight routes flow only when motion is welcome; reduced-motion keeps
183
+ the zero-animation baseline (state stays in the label text). */
184
+ @media (prefers-reduced-motion: no-preference){
185
+ #dsh-plan .e-activity{animation:p-activity-flow 1.1s linear infinite}
186
+ }
187
+ @keyframes p-activity-flow{to{stroke-dashoffset:-28}}
188
+ #dsh-plan .p-status{cursor:pointer}
189
+ #dsh-plan .p-status:hover{border-color:var(--accent)}
190
+ #dsh-plan .p-status:active{background:var(--uupm-wash-12)}
191
+ #dsh-plan .p-status:focus-visible{outline:2px solid var(--accent); outline-offset:2px}
192
+
193
+ #dsh-plan .p-menu{position:fixed; z-index:5; min-width:170px; display:grid; padding:4px; gap:2px;
194
+ background:var(--toolbar-bg); border:1px solid var(--card-line); border-radius:8px}
195
+ #dsh-plan .p-menu [role=menuitem]{background:transparent; color:var(--ink); border:1px solid transparent;
196
+ border-radius:6px; padding:6px 10px; font:inherit; text-align:left; cursor:pointer; white-space:nowrap}
197
+ #dsh-plan .p-menu [role=menuitem]:hover{border-color:var(--card-line); background:var(--uupm-wash-8)}
198
+ #dsh-plan .p-menu [role=menuitem]:active{background:var(--uupm-wash-18)}
199
+ #dsh-plan .p-menu [role=menuitem]:focus-visible{outline:2px solid var(--accent); outline-offset:1px}
200
+ #dsh-plan .p-menu [role=menuitem][aria-disabled=true]{color:var(--ink-muted); cursor:not-allowed}
201
+
202
+ #dsh-plan .p-dialog{position:absolute; inset:0; z-index:6; display:grid; place-items:center;
203
+ background:var(--scrim)}
204
+ #dsh-plan .p-dialog-panel{min-width:300px; padding:14px 16px; display:grid; gap:10px;
205
+ background:var(--uupm-bg-2); border:1px solid var(--card-line); border-radius:10px}
206
+ #dsh-plan .p-dialog-title{font-weight:600; font-size:12.5px; color:var(--ink)}
207
+ #dsh-plan .p-dialog-input{font:inherit; color:var(--ink); background:var(--uupm-bg-0);
208
+ border:1px solid var(--card-line); border-radius:6px; padding:7px 10px}
209
+ #dsh-plan .p-dialog-input:hover{border-color:color-mix(in srgb,var(--uupm-ink-1) 55%,transparent)}
210
+ #dsh-plan .p-dialog-input:focus-visible{outline:2px solid var(--accent); outline-offset:1px; border-color:var(--accent)}
211
+ #dsh-plan .p-dialog-err{min-height:1.2em; font-size:11px; color:var(--uupm-error)}
212
+ #dsh-plan .p-dialog-row{display:flex; gap:8px; justify-content:flex-end}
213
+ #dsh-plan .p-dialog-row button{background:transparent; color:var(--ink); border:1px solid var(--card-line);
214
+ border-radius:6px; padding:5px 12px; font:inherit; cursor:pointer}
215
+ #dsh-plan .p-dialog-row button:hover{border-color:var(--accent)}
216
+ #dsh-plan .p-dialog-row button:active{background:var(--uupm-wash-15)}
217
+ #dsh-plan .p-dialog-row button:focus-visible{outline:2px solid var(--accent); outline-offset:1px}
218
+
219
+ #dsh-plan .p-notice-stack{position:absolute; bottom:52px; left:50%; transform:translateX(-50%); z-index:5;
220
+ display:grid; gap:6px; justify-items:center; pointer-events:none}
221
+ #dsh-plan .p-notice{max-width:60ch; padding:7px 12px; font-size:12px; color:var(--ink);
222
+ background:var(--toolbar-bg); border:1px solid var(--card-line); border-radius:8px; white-space:nowrap;
223
+ overflow:hidden; text-overflow:ellipsis}
224
+ #dsh-plan .p-notice.error{color:var(--error-ink); border-color:var(--uupm-error); background:var(--error-panel)}
225
+
226
+ /* Panel-slim migration: the unjoined-sessions join affordance on the toolbar. */
227
+ #dsh-plan .p-netbtn{color:var(--ink-muted)}
228
+ #dsh-plan .p-netbtn:not([disabled]){color:var(--ink)}
229
+ #dsh-plan .p-netmenu{max-height:240px; overflow:auto}
230
+
231
+ /* ── Node list drawer (network-membership-display): teamed nodes hold the
232
+ canvas; every other session is managed here, switched by host. ── */
233
+ #dsh-plan .p-nodebtn{color:var(--ink)}
234
+ #dsh-plan .p-nodelist{min-width:300px; max-width:340px; max-height:420px; overflow:auto; padding:6px}
235
+ #dsh-plan .p-nodelist-head{display:flex; align-items:center; justify-content:space-between; gap:8px;
236
+ padding:2px 4px 6px; border-bottom:1px solid var(--card-line)}
237
+ #dsh-plan .p-nodelist-title{font-size:12px; font-weight:600; color:var(--ink)}
238
+ #dsh-plan .p-nodesel{font:inherit; font-size:11.5px; color:var(--ink); background:var(--uupm-bg-0);
239
+ border:1px solid var(--card-line); border-radius:6px; padding:2px 6px; max-width:220px}
240
+ #dsh-plan .p-nodesel:focus-visible{outline:2px solid var(--accent); outline-offset:1px}
241
+ #dsh-plan .p-nodelist-group{font-size:11px; color:var(--ink-muted); padding:8px 4px 3px;
242
+ display:flex; align-items:baseline; gap:8px}
243
+ #dsh-plan .p-nodelist-note{font-size:10px; color:var(--ink-muted); opacity:.8}
244
+ #dsh-plan .p-noderow{display:flex; align-items:center; gap:6px; padding:3px 4px; border-radius:6px;
245
+ font-size:12px; color:var(--ink)}
246
+ #dsh-plan .p-noderow:hover{background:var(--uupm-wash-8)}
247
+ #dsh-plan .p-nodedot{flex:none; width:7px; height:7px; border-radius:50%; background:var(--edge-live)}
248
+ #dsh-plan .p-nodedot.cold{background:var(--ink-muted)}
249
+ #dsh-plan .p-nodedot.off{background:transparent; border:1px solid var(--ink-muted)}
250
+ #dsh-plan .p-nodename{flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
251
+ #dsh-plan .p-nonet{flex:none; font-size:10px; color:var(--uupm-warn); border:1px solid var(--uupm-warn);
252
+ border-radius:4px; padding:0 4px}
253
+ #dsh-plan .p-teamcount{flex:none; font-size:10px; color:var(--ink-muted); border:1px solid var(--card-line);
254
+ border-radius:4px; padding:0 4px}
255
+ #dsh-plan .p-listbtn{flex:none; background:transparent; color:var(--ink); border:1px solid var(--card-line);
256
+ border-radius:5px; padding:1px 7px; font:inherit; font-size:11px; cursor:pointer}
257
+ #dsh-plan .p-listbtn:hover{border-color:var(--card-line-hover); background:var(--uupm-wash-8)}
258
+ #dsh-plan .p-listbtn:focus-visible{outline:2px solid var(--accent); outline-offset:1px}
259
+ #dsh-plan .p-nodelist-empty{padding:10px 4px; font-size:11.5px; color:var(--ink-muted)}
260
+
261
+ /* ── Frame resize handles (owner ruling: frames stretch/adjust) — eight
262
+ invisible hit zones that show the grip on hover. ── */
263
+ #dsh-plan .p-frame{position:absolute}
264
+ #dsh-plan .p-frame-handle{position:absolute; pointer-events:auto; width:10px; height:10px; z-index:2; opacity:0;
265
+ transition:opacity 120ms ease}
266
+ #dsh-plan .p-frame:hover .p-frame-handle{opacity:1}
267
+ #dsh-plan .p-frame-handle::after{content:''; position:absolute; inset:2px; background:var(--frame-ink);
268
+ border-radius:2px}
269
+ #dsh-plan .p-h-n{top:-5px; left:calc(50% - 12px); width:24px; height:8px; cursor:ns-resize}
270
+ #dsh-plan .p-h-s{bottom:-5px; left:calc(50% - 12px); width:24px; height:8px; cursor:ns-resize}
271
+ #dsh-plan .p-h-e{right:-5px; top:calc(50% - 12px); width:8px; height:24px; cursor:ew-resize}
272
+ #dsh-plan .p-h-w{left:-5px; top:calc(50% - 12px); width:8px; height:24px; cursor:ew-resize}
273
+ #dsh-plan .p-h-ne{top:-5px; right:-5px; cursor:nesw-resize}
274
+ #dsh-plan .p-h-nw{top:-5px; left:-5px; cursor:nwse-resize}
275
+ #dsh-plan .p-h-se{bottom:-5px; right:-5px; cursor:nwse-resize}
276
+ #dsh-plan .p-h-sw{bottom:-5px; left:-5px; cursor:nesw-resize}
277
+ #dsh-plan .p-h-ne::after, #dsh-plan .p-h-sw::after{transform:rotate(45deg)}
278
+ #dsh-plan .p-h-nw::after, #dsh-plan .p-h-se::after{transform:rotate(-45deg)}
279
+ #dsh-plan .p-frame.selected .p-frame-handle{opacity:1}
280
+ #dsh-plan .p-frame.selected{outline:1px solid var(--frame-ink)}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Deterministic fallback seating: a session id hashes to a stable polar
3
+ * seat, so a reload reproduces the same star map even when no layout is
4
+ * persisted (saved layouts still win - main.ts checks them first). This
5
+ * replaces three Math.random() calls whose only product was a star map
6
+ * that reshuffled on every reload, making screenshots and sessions
7
+ * incomparable. Same hash family as getHue (FNV-1a 32-bit); zero
8
+ * dependencies so the unit test pins stability/bounds without three.
9
+ * @module nexus-stage/seat
10
+ */
11
+
12
+ /** Radial distance bounds, mirroring the historical random-seat envelope. */
13
+ export const SEAT_DIST_MIN = 12
14
+ export const SEAT_DIST_MAX = 30
15
+ /** Vertical jitter bound in world units above/below the grid plane. */
16
+ export const SEAT_Y_SPAN = 1
17
+
18
+ /** FNV-1a 32-bit - the same determinism family as getHue. */
19
+ export function hashId(sid: string): number {
20
+ let h = 0x811c9dc5
21
+ for (let i = 0; i < sid.length; i++) {
22
+ h ^= sid.charCodeAt(i)
23
+ h = Math.imul(h, 0x01000193) >>> 0
24
+ }
25
+ return h >>> 0
26
+ }
27
+
28
+ /** Deterministic world seat (x, vertical jitter, z) for one session id. */
29
+ export function seatFor(sid: string): { x: number; y: number; z: number } {
30
+ const h = hashId(sid)
31
+ const angle = ((h % 3600) / 3600) * Math.PI * 2
32
+ const dist = SEAT_DIST_MIN + (((h >>> 9) % 1800) / 1800) * (SEAT_DIST_MAX - SEAT_DIST_MIN)
33
+ const y = (((h >>> 18) % 2000) / 1999) * 2 * SEAT_Y_SPAN - SEAT_Y_SPAN
34
+ return { x: Math.cos(angle) * dist, y, z: Math.sin(angle) * dist }
35
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Display name for one stage node: the session title when present, then the
3
+ * routable team label (`team/<id8>`), then the host fallback label
4
+ * (`dsh-host-<node>-<id8>`). Untitled cold joined sessions carry no title and
5
+ * without the middle step the canvas drowned in dsh-host-* noise (2026-08-30
6
+ * user report: the planning canvas showed a dozen unreadable label cards).
7
+ * @module nexus-stage/stage-name
8
+ */
9
+
10
+ /** One node's display title. */
11
+ export function displayName(node: { readonly name?: string; readonly team?: string; readonly label?: string }): string {
12
+ return node.name ?? node.team ?? node.label ?? ''
13
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * A2A Nexus design tokens.
3
+ * Three-layer: primitive(C) -> semantic(S) -> component-level inline styles.
4
+ */
5
+
6
+ export const C = {
7
+ bg0: '#060a12',
8
+ bg1: '#0d1322',
9
+ ink: '#dde3f0',
10
+ inkMuted: '#7a86a0',
11
+ accent: '#8b5cf6',
12
+ liveGreen: '#4ade80',
13
+ coldGrey: '#4a5568',
14
+ lineFederal: '#6366f1',
15
+ membershipLine: '#22d3ee',
16
+ } as const;
17
+
18
+ /**
19
+ * Semantic layer: the scene-facing names the viewer actually consumes,
20
+ * each pinned to its primitive above. This is the `S` main.ts reached for
21
+ * as `T.S.*` — the layer was documented but never exported, so every seat
22
+ * attempt threw and cycle()'s empty catch buried it: an eternally empty
23
+ * stage behind a healthy-looking render loop.
24
+ */
25
+ export const S = {
26
+ /** Mesh color + emissive for live session nodes (primitive liveGreen). */
27
+ nodeLive: 0x4ade80,
28
+ /** Mesh color + emissive for cold/joined-offline nodes (primitive coldGrey). */
29
+ nodeCold: 0x4a5568,
30
+ } as const;
31
+
32
+ export const FRAME_HUES = [0x8b5cf6, 0x3b82f6, 0x14b8a6, 0xf59e0b, 0xec4899, 0x84cc16];
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Viewport math for the 2D planning canvas: pan/zoom state with the same
3
+ * clamp contract as the persisted layout (scale 0.25-3, coordinates
4
+ * bounded), pointer-anchored zoom, and the CSS transform that carries the
5
+ * world container. Pure functions throughout - the DOM layer applies the
6
+ * returned transform, it never computes one.
7
+ *
8
+ * Convention (shared with the host LayoutStore): (x, y) is the world
9
+ * point displayed at the viewport's top-left corner; scale multiplies
10
+ * world units into screen pixels.
11
+ * @module nexus-stage/viewport
12
+ */
13
+
14
+ import { SCALE_MAX, SCALE_MIN, type LayoutViewport } from './layout-doc'
15
+
16
+ /** Re-export for callers that only want the viewport vocabulary. */
17
+ export type { LayoutViewport, LayoutViewport as ViewportState } from './layout-doc'
18
+
19
+ /** The zero viewport: origin, unzoomed - what a fresh canvas shows. */
20
+ export const IDENTITY_VIEWPORT: LayoutViewport = { x: 0, y: 0, scale: 1 }
21
+
22
+ function clampScale(scale: number): number {
23
+ return Math.max(SCALE_MIN, Math.min(SCALE_MAX, scale))
24
+ }
25
+
26
+ /** Clamp an untrusted viewport into the persisted-layout envelope. */
27
+ export function clampViewport(vp: LayoutViewport): LayoutViewport {
28
+ const safe = Number.isFinite(vp.scale) ? vp.scale : 1
29
+ const x = Number.isFinite(vp.x) ? vp.x : 0
30
+ const y = Number.isFinite(vp.y) ? vp.y : 0
31
+ return { x, y, scale: clampScale(safe) }
32
+ }
33
+
34
+ /** Screen (client-relative px) -> world point. */
35
+ export function screenToWorld(vp: LayoutViewport, sx: number, sy: number): { x: number; y: number } {
36
+ return { x: vp.x + sx / vp.scale, y: vp.y + sy / vp.scale }
37
+ }
38
+
39
+ /** World point -> screen (client-relative px). */
40
+ export function worldToScreen(vp: LayoutViewport, wx: number, wy: number): { x: number; y: number } {
41
+ return { x: (wx - vp.x) * vp.scale, y: (wy - vp.y) * vp.scale }
42
+ }
43
+
44
+ /** Pan by screen-space pixels; dividing by scale keeps 1px = 1px on screen. */
45
+ export function panBy(vp: LayoutViewport, dx: number, dy: number): LayoutViewport {
46
+ return { x: vp.x + dx / vp.scale, y: vp.y + dy / vp.scale, scale: vp.scale }
47
+ }
48
+
49
+ /**
50
+ * Zoom by a multiplicative factor keeping the world point under the
51
+ * anchor screen position (ax, ay) fixed: the card under the cursor stays
52
+ * under the cursor. Invariant: vp' = vp + a/s - a/s'.
53
+ */
54
+ export function zoomAt(vp: LayoutViewport, factor: number, ax: number, ay: number): LayoutViewport {
55
+ const scale = clampScale(vp.scale * factor)
56
+ if (scale === vp.scale) return vp
57
+ return { x: vp.x + ax / vp.scale - ax / scale, y: vp.y + ay / vp.scale - ay / scale, scale }
58
+ }
59
+
60
+ /** Content bounds in world space (null = nothing to fit). */
61
+ export interface WorldBounds { readonly minX: number; readonly minY: number; readonly maxX: number; readonly maxY: number }
62
+
63
+ /**
64
+ * Fit the content bounds into a viewW x viewH viewport at 90% fill,
65
+ * clamped to the persisted scale envelope; empty bounds reset to identity.
66
+ */
67
+ export function fitView(bounds: WorldBounds | null, viewW: number, viewH: number, maxScale = Number.POSITIVE_INFINITY): LayoutViewport {
68
+ if (bounds === null || viewW <= 0 || viewH <= 0) return IDENTITY_VIEWPORT
69
+ const bw = bounds.maxX - bounds.minX
70
+ const bh = bounds.maxY - bounds.minY
71
+ if (!(bw > 0) || !(bh > 0)) return IDENTITY_VIEWPORT
72
+ const scale = clampScale(Math.min(maxScale, 0.9 * Math.min(viewW / bw, viewH / bh)))
73
+ return {
74
+ x: (bounds.minX + bounds.maxX) / 2 - viewW / (2 * scale),
75
+ y: (bounds.minY + bounds.maxY) / 2 - viewH / (2 * scale),
76
+ scale,
77
+ }
78
+ }
79
+
80
+ /** The CSS transform for the world container: translate then scale. */
81
+ export function worldTransformCss(vp: LayoutViewport): string {
82
+ return `translate(${-vp.x * vp.scale}px, ${-vp.y * vp.scale}px) scale(${vp.scale})`
83
+ }