@nelsonlongxiang/dsh-open-a2a-net 0.6.32 → 0.6.34
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 +12 -3
- package/README_zh.md +12 -3
- package/assets/nexusDist/assets/index-D2BGWOeU.js +9 -0
- package/assets/nexusDist/assets/index-z33lHtLw.css +1 -0
- package/assets/nexusDist/index.html +2 -2
- package/cli/a2a.mjs +5 -2
- package/lib/a2a-client.js +9 -2
- package/lib/auth-observer.js +1 -1
- package/lib/canvas-store.js +50 -6
- package/lib/card.js +2 -1
- package/lib/cli-home.js +44 -0
- package/lib/client/A2aControl.js +1 -1
- package/lib/client.js +65 -65
- package/lib/client.js.map +1 -1
- package/lib/direct-forward.js +61 -0
- package/lib/faces/admin.js +84 -9
- package/lib/faces/wire.js +57 -16
- package/lib/index.js +118 -67
- package/lib/layout-store.js +3 -2
- package/lib/outbound-scope.js +25 -0
- package/lib/queue-guard.js +37 -0
- package/lib/roster-op.js +104 -0
- package/lib/skill/references/cli.md +6 -5
- package/lib/skill/references/pitfalls.md +1 -4
- package/lib/skill/references/router.md +1 -1
- package/lib/task-auth.js +52 -3
- package/lib/task-ledger.js +4 -1
- package/lib/team-store.js +49 -1
- package/lib/tools/info.js +19 -3
- package/lib/types/a2a-client.d.ts +1 -1
- package/lib/types/auth-observer.d.ts +2 -2
- package/lib/types/canvas-store.d.ts +14 -1
- package/lib/types/cli-home.d.ts +24 -0
- package/lib/types/client/A2aControl.d.ts +1 -1
- package/lib/types/direct-forward.d.ts +32 -0
- package/lib/types/faces/admin.d.ts +9 -1
- package/lib/types/faces/wire.d.ts +13 -4
- package/lib/types/index.d.ts +4 -0
- package/lib/types/outbound-scope.d.ts +21 -0
- package/lib/types/queue-guard.d.ts +32 -0
- package/lib/types/roster-op.d.ts +21 -0
- package/lib/types/runtime.d.ts +1 -1
- package/lib/types/task-auth.d.ts +17 -2
- package/lib/types/task-ledger.d.ts +6 -1
- package/lib/types/team-store.d.ts +7 -0
- package/lib/types/types.d.ts +8 -0
- package/nexus-stage/src/canvas-ops.ts +2 -1
- package/nexus-stage/src/cli-home-picker.ts +42 -0
- package/nexus-stage/src/layout-doc.ts +6 -4
- package/nexus-stage/src/main.ts +36 -4
- package/nexus-stage/src/planning-card.ts +61 -0
- package/nexus-stage/src/planning-frame-hue.ts +13 -0
- package/nexus-stage/src/planning-legend.ts +1 -0
- package/nexus-stage/src/planning-menus.ts +63 -12
- package/nexus-stage/src/planning-roster.ts +15 -0
- package/nexus-stage/src/planning-types.ts +1 -1
- package/nexus-stage/src/planning-view.ts +43 -59
- package/nexus-stage/src/planning.css +19 -0
- package/nexus-stage/src/workbench.css +4 -0
- package/nexus-stage/src/workbench.ts +24 -5
- package/nexus-stage/src/world.ts +5 -1
- package/package.json +4 -2
- package/assets/nexusDist/assets/index-D5H8R8de.js +0 -9
- package/assets/nexusDist/assets/index-D9GYZyGZ.css +0 -1
|
@@ -41,7 +41,8 @@ import { clampViewport, fitView, panBy, screenToWorld, worldTransformCss, zoomAt
|
|
|
41
41
|
import { boardBounds, fitBelowHud, hudBottomOf } from './planning-fit'
|
|
42
42
|
import { WorldModel, nodeRect, deriveInitialFrame, homeTeams, planSeatClear, planTeamClusters, NODE_W, NODE_H } from './world'
|
|
43
43
|
import { applyAction, settleAction, actionTeam, innermostFrameAt, reorderOps, yOrderedMembers, type CanvasAction } from './canvas-ops'
|
|
44
|
-
import {
|
|
44
|
+
import { frameHue } from './planning-frame-hue'
|
|
45
|
+
import { rosterFrameTitle } from './planning-roster'
|
|
45
46
|
import { createPlanningMenus } from './planning-menus'
|
|
46
47
|
import { createNodeFlyer, mountBoardSearch } from './planning-search'
|
|
47
48
|
import { applyBoardRelayout } from './planning-relayout'
|
|
@@ -50,6 +51,7 @@ import { settleBoard, tidyMembers } from './planning-overlap'
|
|
|
50
51
|
import { createLayoutUndo, type LayoutSnap } from './planning-undo'
|
|
51
52
|
import { createNodeList } from './planning-nodelist'
|
|
52
53
|
import { cardText } from './planning-card-text'
|
|
54
|
+
import { createCardElement, paintPrio, workLabel } from './planning-card'
|
|
53
55
|
import { createLegend } from './planning-legend'
|
|
54
56
|
import { UNREACHABLE_HINT, showFrameReachability, unreachableRefs } from './planning-reachability'
|
|
55
57
|
import { viewportBucket, type PlanningDeps, type PlanningInput, type PlanningSession, type PlanningTeam, type PlanningView, type SeamKey, type SeamPointer, type ViewportBucket } from './planning-types'
|
|
@@ -58,28 +60,18 @@ import './planning.css'
|
|
|
58
60
|
export { viewportBucket } from './planning-types'
|
|
59
61
|
export type { PlanningDeps, PlanningInput, PlanningSession, PlanningTeam, PlanningView, SeamKey, SeamPointer, ViewportBucket } from './planning-types'
|
|
60
62
|
|
|
61
|
-
/** Team-name hue, same 31-polynomial family as the 3D scene's getHue. */
|
|
62
|
-
function frameHue(name: string): { line: string; bg: string } {
|
|
63
|
-
let h = 0
|
|
64
|
-
for (let i = 0; i < name.length; i++) h = ((h * 31 + name.charCodeAt(i)) & 0xfffffff) >>> 0
|
|
65
|
-
const rgb = FRAME_HUES[h % FRAME_HUES.length]!
|
|
66
|
-
const line = '#' + rgb.toString(16).padStart(6, '0')
|
|
67
|
-
// 5% alpha fill over the dark base, per the design system.
|
|
68
|
-
const r = (rgb >> 16) & 0xff
|
|
69
|
-
const g = (rgb >> 8) & 0xff
|
|
70
|
-
const b = rgb & 0xff
|
|
71
|
-
return { line, bg: `rgba(${r},${g},${b},0.05)` }
|
|
72
|
-
}
|
|
73
|
-
|
|
74
63
|
|
|
75
64
|
type Gesture =
|
|
76
65
|
| { readonly kind: 'none' }
|
|
77
|
-
| { readonly kind: 'node'; ids: readonly string[]; clicked: string; last: { x: number; y: number }; moved: boolean; el: Element | null }
|
|
66
|
+
| { readonly kind: 'node'; ids: readonly string[]; clicked: string; rosterOrigin?: string; last: { x: number; y: number }; moved: boolean; el: Element | null }
|
|
78
67
|
| { readonly kind: 'frame'; name: string; snap: NonNullable<ReturnType<WorldModel['beginFrameDrag']>>; origin: { x: number; y: number }; moved: boolean }
|
|
79
68
|
| { readonly kind: 'frame-resize'; name: string; dir: string; startRect: { x: number; y: number; w: number; h: number }; origin: { x: number; y: number }; moved: boolean }
|
|
80
69
|
| { readonly kind: 'pan'; last: { x: number; y: number }; moved: boolean }
|
|
81
70
|
| { readonly kind: 'marquee'; origin: { x: number; y: number }; last: { x: number; y: number }; additive: boolean }
|
|
82
71
|
|
|
72
|
+
// The card skeleton, its 工作中/空闲 chip and the right-hand chip live in planning-card (module-size cap).
|
|
73
|
+
export { workLabel } from './planning-card'
|
|
74
|
+
|
|
83
75
|
export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
84
76
|
const model = new WorldModel()
|
|
85
77
|
let layoutDoc: LayoutDoc | undefined
|
|
@@ -372,7 +364,10 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
372
364
|
const title = el.querySelector<HTMLElement>('.ttl')!
|
|
373
365
|
const cnt = el.querySelector<HTMLElement>('.cnt')!
|
|
374
366
|
const route = el.querySelector<HTMLElement>('.route')!
|
|
375
|
-
|
|
367
|
+
const isRoster = rosterFramesRef.has(name)
|
|
368
|
+
const shownName = isRoster ? rosterFrameTitle(name) : name
|
|
369
|
+
if (title.textContent !== shownName) title.textContent = shownName
|
|
370
|
+
el.classList.toggle('is-roster', isRoster)
|
|
376
371
|
let members = 0
|
|
377
372
|
let cross = 0
|
|
378
373
|
for (const m of model.allNodes()) {
|
|
@@ -385,7 +380,7 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
385
380
|
if (cnt.textContent !== cntText) cnt.textContent = cntText
|
|
386
381
|
const mirror = mirrorRoute(remoteFramesRef.get(name), pendingTeams.has(name)) // PR#96 single owner + mirror
|
|
387
382
|
const unavailable = mirror !== undefined && reachability.hosts.has(peerHostOf(remoteFramesRef.get(name)!))
|
|
388
|
-
const routeText0 = mirror?.text ??
|
|
383
|
+
const routeText0 = mirror?.text ?? (lastTeams.find(x => x.name === name)?.team ?? name)
|
|
389
384
|
if (route.textContent !== routeText0) route.textContent = routeText0
|
|
390
385
|
route.title = `${mirror?.title ?? ''}${unavailable ? ` · ${UNREACHABLE_HINT}` : ''}`
|
|
391
386
|
showFrameReachability(el, unavailable)
|
|
@@ -400,12 +395,6 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
400
395
|
}
|
|
401
396
|
}
|
|
402
397
|
|
|
403
|
-
/** The mono route line `<host>/canvas/<name>` when the payload carries it. */
|
|
404
|
-
function routeText(name: string): string {
|
|
405
|
-
const t = lastTeams.find(x => x.name === name)
|
|
406
|
-
return t?.team ?? name
|
|
407
|
-
}
|
|
408
|
-
|
|
409
398
|
// ── keyed diff: nodes ──
|
|
410
399
|
function renderNodes(): void {
|
|
411
400
|
const seen = new Set<string>()
|
|
@@ -413,22 +402,7 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
413
402
|
seen.add(n.id)
|
|
414
403
|
let el = nodeEls.get(n.id)
|
|
415
404
|
if (el === undefined) {
|
|
416
|
-
el =
|
|
417
|
-
el.className = 'p-node'
|
|
418
|
-
el.dataset.id = n.id
|
|
419
|
-
el.tabIndex = 0
|
|
420
|
-
const nm = document.createElement('div')
|
|
421
|
-
nm.className = 'nm'
|
|
422
|
-
const dot = document.createElement('span')
|
|
423
|
-
dot.className = 'dot'
|
|
424
|
-
const name = document.createElement('span')
|
|
425
|
-
name.className = 'nm-text'
|
|
426
|
-
nm.append(dot, name)
|
|
427
|
-
const sub = document.createElement('div')
|
|
428
|
-
sub.className = 'sub mono'
|
|
429
|
-
const prio = document.createElement('span')
|
|
430
|
-
prio.className = 'prio mono'
|
|
431
|
-
el.append(nm, sub, prio)
|
|
405
|
+
el = createCardElement(n.id)
|
|
432
406
|
nodeLayer.appendChild(el)
|
|
433
407
|
nodeEls.set(n.id, el)
|
|
434
408
|
}
|
|
@@ -437,6 +411,12 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
437
411
|
el.style.top = `${r.y}px`
|
|
438
412
|
el.classList.toggle('cold', !n.live)
|
|
439
413
|
el.classList.toggle('remote', n.remote === true)
|
|
414
|
+
// 工作中/空闲: live local sessions only; the host reports nothing for cold or remote nodes.
|
|
415
|
+
const work = n.live && n.remote !== true ? lastWork.get(n.id) : undefined
|
|
416
|
+
el.classList.toggle('is-working', work?.status === 'running')
|
|
417
|
+
const workText = workLabel(work)
|
|
418
|
+
const workEl = el.querySelector<HTMLElement>('.work')!
|
|
419
|
+
if (workEl.textContent !== workText) workEl.textContent = workText
|
|
440
420
|
el.classList.toggle('selected', model.isSelected(n.id))
|
|
441
421
|
el.setAttribute('aria-selected', String(model.isSelected(n.id)))
|
|
442
422
|
const host = n.id.slice('peer-'.length)
|
|
@@ -451,22 +431,7 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
451
431
|
if (subEl.textContent !== subText) subEl.textContent = subText
|
|
452
432
|
const tip = unavailable ? `${txt.tip} · ${UNREACHABLE_HINT}` : txt.tip
|
|
453
433
|
if (el.title !== tip) el.title = tip
|
|
454
|
-
|
|
455
|
-
if (n.remote === true) {
|
|
456
|
-
// gh#20 W-A: the peer badge — link class colors the chip (linked
|
|
457
|
-
// green / stranger gray), the verified tick/cross rides after it.
|
|
458
|
-
// An absent link field (older host) renders the neutral 'peer'.
|
|
459
|
-
const info = peerInfoByHost.get(host)
|
|
460
|
-
const cls = info?.link === 'linked' ? 'lnk-linked' : info?.link === 'stranger' ? 'lnk-stranger' : ''
|
|
461
|
-
if (prioEl.className !== `prio mono ${cls}`.trim()) prioEl.className = `prio mono ${cls}`.trim()
|
|
462
|
-
const mark = info === undefined ? '' : info.verified === true ? '✓' : '✗'
|
|
463
|
-
const badge = (info?.link ?? 'peer') + mark
|
|
464
|
-
if (prioEl.textContent !== badge) prioEl.textContent = badge
|
|
465
|
-
} else {
|
|
466
|
-
if (prioEl.className !== 'prio mono') prioEl.className = 'prio mono'
|
|
467
|
-
const prioText = n.memberships[0] !== undefined ? `P${n.memberships[0]!.index}` : ''
|
|
468
|
-
if (prioEl.textContent !== prioText) prioEl.textContent = prioText
|
|
469
|
-
}
|
|
434
|
+
paintPrio(el.querySelector<HTMLElement>('.prio')!, n, n.remote === true ? peerInfoByHost.get(host) : undefined)
|
|
470
435
|
}
|
|
471
436
|
for (const [id, el] of [...nodeEls]) {
|
|
472
437
|
if (!seen.has(id)) { el.remove(); nodeEls.delete(id) }
|
|
@@ -485,6 +450,7 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
485
450
|
const federation = createFederationOverlay(svg, model)
|
|
486
451
|
let lastPeers: ReadonlyArray<PeerRow> = []
|
|
487
452
|
let lastRemoteTeams: ReadonlyMap<string, RemoteTeamRow[]> = new Map()
|
|
453
|
+
let lastWork: ReadonlyMap<string, Pick<PlanningSession, 'status' | 'queued'>> = new Map()
|
|
488
454
|
const peerScoreByText = new Map<string, number | undefined>()
|
|
489
455
|
/** gh#20 W-A: per-host link/verified info for the peer badges. */
|
|
490
456
|
const peerInfoByHost = new Map<string, { link?: 'linked' | 'stranger'; verified?: boolean }>()
|
|
@@ -534,10 +500,17 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
534
500
|
// gh#41: remote-frame registry (frame name → peer URL), refreshed by
|
|
535
501
|
// reconcile and read by emitAction's serverAction to fork remote drops.
|
|
536
502
|
let remoteFramesRef = new Map<string, string>()
|
|
503
|
+
let rosterFramesRef = new Set<string>()
|
|
537
504
|
let reachability = unreachableRefs({})
|
|
538
505
|
const dissolving = createDissolveGuard(deps.now)
|
|
539
506
|
|
|
540
507
|
function emitAction(a: CanvasAction): void {
|
|
508
|
+
const roster = (a.type === 'create-team' && a.roster === true) || rosterFramesRef.has(actionTeam(a))
|
|
509
|
+
if (roster && a.type === 'remove-team' && !window.confirm(`解散网络队「${rosterFrameTitle(a.name)}」并撤回成员与白名单?`)) return
|
|
510
|
+
if (roster && a.type === 'reorder') { notice('error', 'roster 队不支持置顶路由'); return }
|
|
511
|
+
if (roster && (a.type === 'create-team' || a.type === 'add-member') && a.ids.some(id => model.getNode(id)?.remote === true || !lastSessions.some(s => s.id === id && s.joined === true))) {
|
|
512
|
+
notice('error', 'roster 只接受本机已入网会话,远端或未入网成员未写入'); return
|
|
513
|
+
}
|
|
541
514
|
// gh#20 W-B: the SERVER sees zone/id8 remote handles where the model
|
|
542
515
|
// keeps its peer-<host> node id — the canvas member contract is
|
|
543
516
|
// session-id-or-remote-ref. Translation happens here, one boundary;
|
|
@@ -590,14 +563,16 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
590
563
|
})
|
|
591
564
|
}
|
|
592
565
|
}
|
|
566
|
+
if (roster && a.type === 'create-team') rosterFramesRef.add(a.name)
|
|
593
567
|
const undo = applyAction(model, a)
|
|
594
568
|
const team = actionTeam(a)
|
|
595
569
|
pendingAdd(team)
|
|
596
570
|
render()
|
|
597
|
-
const sent = serverAction(a)
|
|
571
|
+
const sent = serverAction(roster ? { ...a, roster: true } : a)
|
|
598
572
|
void deps.onCanvasAction(sent).then(
|
|
599
573
|
ok => {
|
|
600
574
|
pendingRelease(team)
|
|
575
|
+
if (!ok && a.type === 'create-team' && roster) rosterFramesRef.delete(team)
|
|
601
576
|
layoutDoc = settleAction(model, a, ok, undo, layoutDoc); markDirty(); dissolving.settled(sent, ok)
|
|
602
577
|
render()
|
|
603
578
|
},
|
|
@@ -606,6 +581,7 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
606
581
|
// team's guard (and its optimistic state) forever — and per §3.4
|
|
607
582
|
// 不吞错, the rollback surfaces to the user.
|
|
608
583
|
pendingRelease(team)
|
|
584
|
+
if (a.type === 'create-team' && roster) rosterFramesRef.delete(team)
|
|
609
585
|
layoutDoc = settleAction(model, a, false, undo, layoutDoc); markDirty(); dissolving.settled(sent, false)
|
|
610
586
|
notice('error', '操作通道异常,已回滚')
|
|
611
587
|
render()
|
|
@@ -628,6 +604,8 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
628
604
|
const menus = createPlanningMenus({
|
|
629
605
|
root, netBtn, signal, model,
|
|
630
606
|
teams: () => lastTeams,
|
|
607
|
+
joined: id => lastSessions.some(s => s.id === id && s.joined === true),
|
|
608
|
+
isRoster: name => rosterFramesRef.has(name),
|
|
631
609
|
anchorFor: (t) => (t.kind === 'node' ? nodeEls.get(t.id) : frameEls.get(t.name)) ?? null,
|
|
632
610
|
emit: emitAction,
|
|
633
611
|
notice,
|
|
@@ -698,8 +676,11 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
698
676
|
}
|
|
699
677
|
}
|
|
700
678
|
}
|
|
701
|
-
//
|
|
702
|
-
|
|
679
|
+
// Canvas blank drops move only; leaving a roster frame explicitly retracts those declarations.
|
|
680
|
+
if (gesture.rosterOrigin !== undefined && f !== gesture.rosterOrigin) {
|
|
681
|
+
const leaving = gesture.ids.filter(id => model.teamMemberIds(gesture.rosterOrigin!).includes(id))
|
|
682
|
+
if (leaving.length > 0) emitAction({ type: 'remove-member', team: gesture.rosterOrigin, ids: leaving })
|
|
683
|
+
}
|
|
703
684
|
}
|
|
704
685
|
|
|
705
686
|
function pointerDown(ev: SeamPointer): void {
|
|
@@ -768,7 +749,8 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
768
749
|
const already = model.isSelected(id)
|
|
769
750
|
if (!already && !ev.shiftKey) model.setSelection([id])
|
|
770
751
|
const ids = ev.shiftKey || already ? (model.isSelected(id) ? model.selectedIds() : [...model.selectedIds(), id]) : [id]
|
|
771
|
-
|
|
752
|
+
const origin = innermostFrameAt(new Map(model.allFrames()), screenToWorld(vp, p.x, p.y))
|
|
753
|
+
gesture = { kind: 'node', clicked: id, ids, ...(origin && rosterFramesRef.has(origin) && model.teamMemberIds(origin).includes(id) ? { rosterOrigin: origin } : {}), last: screenToWorld(vp, p.x, p.y), moved: false, el: nodeEl }
|
|
772
754
|
nodeEl.classList.add('dragging')
|
|
773
755
|
} else if (headEl !== null) {
|
|
774
756
|
if (ev.button !== 0) return
|
|
@@ -1126,6 +1108,7 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
1126
1108
|
function reconcile(input: PlanningInput): void {
|
|
1127
1109
|
// gh#41: remote-frame registry shared with serverAction (drop fork).
|
|
1128
1110
|
lastTeams = input.teams
|
|
1111
|
+
rosterFramesRef = new Set([...input.teams.filter(t => t.kind === 'roster').map(t => t.name), ...[...rosterFramesRef].filter(n => pendingTeams.has(n))])
|
|
1129
1112
|
lastPeers = input.peers ?? []
|
|
1130
1113
|
federation.setInFlight(input.inFlight ?? [])
|
|
1131
1114
|
// Unjoined sessions have no node on the canvas — the panel-slim
|
|
@@ -1219,6 +1202,7 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
1219
1202
|
// excluded from bounds, so the column cannot feed back into itself).
|
|
1220
1203
|
const peerSeen = new Set<string>()
|
|
1221
1204
|
lastRemoteTeams = groupRemoteTeams(input.remoteTeams ?? [])
|
|
1205
|
+
lastWork = new Map(input.sessions.map(s => [s.id, { status: s.status, queued: s.queued }]))
|
|
1222
1206
|
reachability = unreachableRefs(input)
|
|
1223
1207
|
peerScoreByText.clear()
|
|
1224
1208
|
for (const p of input.peers ?? []) {
|
|
@@ -83,6 +83,8 @@
|
|
|
83
83
|
#dsh-plan .p-frame-head .cnt{color:var(--ink-muted); font-weight:400}
|
|
84
84
|
#dsh-plan .p-frame-head .route{font-size:10.5px; color:var(--ink-muted)}
|
|
85
85
|
#dsh-plan .p-frame.is-mirror{border-style:dashed}
|
|
86
|
+
#dsh-plan .p-frame.is-roster{border-style:double;border-width:3px}
|
|
87
|
+
#dsh-plan .p-frame.is-roster .p-frame-head{border-style:dashed}
|
|
86
88
|
#dsh-plan .p-frame.is-syncing .p-frame-head .route{color:var(--accent)}
|
|
87
89
|
#dsh-plan .p-frame.is-unreachable{opacity:.76;border-style:dashed}
|
|
88
90
|
#dsh-plan .p-frame-unreachable{position:absolute;left:8px;top:20px;max-width:calc(100% - 16px);color:var(--uupm-error);font-size:11px;pointer-events:none}
|
|
@@ -114,6 +116,14 @@
|
|
|
114
116
|
#dsh-plan .p-node.remote .nm-text{overflow-wrap:anywhere}
|
|
115
117
|
#dsh-plan .p-node .dot{width:8px; height:8px; border-radius:50%; background:var(--edge-live); flex:none}
|
|
116
118
|
#dsh-plan .p-node.cold .dot{background:transparent; border:1.5px solid var(--edge-cold)}
|
|
119
|
+
#dsh-plan .p-node .sub-row{display:flex; align-items:baseline; gap:6px; margin-top:2px; min-width:0}
|
|
120
|
+
#dsh-plan .p-node .sub-row .sub{flex:1 1 auto; min-width:0; margin-top:0}
|
|
121
|
+
#dsh-plan .p-node .work{flex:none; font-size:10.5px; font-weight:500; color:var(--ink-muted); white-space:nowrap}
|
|
122
|
+
#dsh-plan .p-node .work:empty{display:none}
|
|
123
|
+
#dsh-plan .p-node.is-working .work{color:var(--edge-live)}
|
|
124
|
+
#dsh-plan .p-node.is-working .dot{animation:p-working 1.4s ease-in-out infinite}
|
|
125
|
+
@keyframes p-working{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--edge-live) 55%,transparent)}50%{box-shadow:0 0 0 4px transparent}}
|
|
126
|
+
@media (prefers-reduced-motion: reduce){#dsh-plan .p-node.is-working .dot{animation:none}}
|
|
117
127
|
#dsh-plan .p-node .sub{font-size:11px; color:var(--ink-muted); margin-top:2px;
|
|
118
128
|
overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
|
|
119
129
|
/* W1: a picked search hit pulses its card for 1.2s (class removed on timer). */
|
|
@@ -250,6 +260,15 @@
|
|
|
250
260
|
#dsh-plan .p-dialog-input:hover{border-color:color-mix(in srgb,var(--uupm-ink-1) 55%,transparent)}
|
|
251
261
|
#dsh-plan .p-dialog-input:focus-visible{outline:2px solid var(--accent); outline-offset:1px; border-color:var(--accent)}
|
|
252
262
|
#dsh-plan .p-dialog-err{min-height:1.2em; font-size:11px; color:var(--uupm-error)}
|
|
263
|
+
#dsh-plan .p-dialog-kind{display:grid; grid-template-columns:1fr 1fr; gap:4px; padding:3px;
|
|
264
|
+
background:var(--uupm-bg-0); border:1px solid var(--card-line); border-radius:8px}
|
|
265
|
+
#dsh-plan .p-dialog-kind button{font:inherit; color:var(--ink-muted); background:transparent; border:1px solid transparent;
|
|
266
|
+
border-radius:6px; padding:6px 10px; cursor:pointer}
|
|
267
|
+
#dsh-plan .p-dialog-kind button:hover{color:var(--ink); background:var(--uupm-wash-8)}
|
|
268
|
+
#dsh-plan .p-dialog-kind button[aria-checked=true]{color:var(--ink); font-weight:600; background:var(--uupm-wash-15);
|
|
269
|
+
border-color:var(--accent)}
|
|
270
|
+
#dsh-plan .p-dialog-kind button:focus-visible{outline:2px solid var(--accent); outline-offset:1px}
|
|
271
|
+
#dsh-plan .p-dialog-hint{font-size:11px; color:var(--ink-muted); max-width:340px}
|
|
253
272
|
#dsh-plan .p-dialog-row{display:flex; gap:8px; justify-content:flex-end}
|
|
254
273
|
#dsh-plan .p-dialog-row button{background:transparent; color:var(--ink); border:1px solid var(--card-line);
|
|
255
274
|
border-radius:6px; padding:5px 12px; font:inherit; cursor:pointer}
|
|
@@ -93,6 +93,10 @@ body{background:var(--n-bg);color:var(--n-ink);font:13px/1.45 system-ui,"Segoe U
|
|
|
93
93
|
.wb-member-remove:hover:not(:disabled),.wb-member-submit:hover:not(:disabled){border-color:var(--n-danger)}
|
|
94
94
|
.wb-member-submit:hover:not(:disabled){border-color:var(--n-accent)}
|
|
95
95
|
.wb-member-add{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
|
|
96
|
+
.wb-cli-home{display:flex;flex-wrap:wrap;gap:6px;padding:8px;margin:4px 0 10px;border:1px solid var(--n-border);border-radius:6px}
|
|
97
|
+
.wb-cli-home [hidden]{display:none}
|
|
98
|
+
.wb-cli-home .wb-hint{flex-basis:100%;margin-top:0}
|
|
99
|
+
.wb-cli-home .wb-member-pick{min-width:0;max-width:100%}
|
|
96
100
|
.wb-member-pick,.wb-member-manual{background:var(--n-panel);border:1px solid var(--n-border);color:var(--n-ink);
|
|
97
101
|
border-radius:6px;padding:3px 8px;font:inherit;font-size:12px;min-width:0}
|
|
98
102
|
.wb-member-pick{flex:1 1 150px}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* @module nexus-stage/workbench
|
|
20
20
|
*/
|
|
21
21
|
import { createChatDrawer, type ChatTarget } from './wb-chat'
|
|
22
|
+
import { createCliHomePicker, type CliHomeOptions } from './cli-home-picker'
|
|
22
23
|
import { createRailTabs } from './wb-rail'
|
|
23
24
|
import { DIRECTORY_FILTERS, learnLogTitle, loadLogTitles, renderDirectory, type DirFilter } from './wb-directory'
|
|
24
25
|
import { ledgerFacts, renderLedger as paintLedger } from './wb-ledger'
|
|
@@ -130,6 +131,9 @@ export interface CanvasMemberRow {
|
|
|
130
131
|
readonly joined?: boolean
|
|
131
132
|
readonly live?: boolean
|
|
132
133
|
readonly remote?: boolean
|
|
134
|
+
/** Present only for CLI refs: null means the linked home is ambiguous or unknown. */
|
|
135
|
+
readonly cliHome?: string | null
|
|
136
|
+
readonly cliHeld?: boolean
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
/** One canvas team from /state `canvas.teams`. */
|
|
@@ -142,7 +146,7 @@ export interface CanvasTeamRow {
|
|
|
142
146
|
/** One activity-ring entry. */
|
|
143
147
|
export interface ActivityRow {
|
|
144
148
|
readonly ts: number
|
|
145
|
-
readonly dir: 'in' | 'out'
|
|
149
|
+
readonly dir: 'in' | 'out' | 'fwd'
|
|
146
150
|
readonly team: string
|
|
147
151
|
readonly peer: string
|
|
148
152
|
readonly ok: boolean
|
|
@@ -150,6 +154,7 @@ export interface ActivityRow {
|
|
|
150
154
|
|
|
151
155
|
/** The /state subset the workbench consumes (additive fields ignored). */
|
|
152
156
|
export interface StateBody {
|
|
157
|
+
readonly zone?: string
|
|
153
158
|
readonly version?: string
|
|
154
159
|
readonly sessions?: readonly SessionRow[]
|
|
155
160
|
readonly canvas?: { teams?: readonly CanvasTeamRow[] }
|
|
@@ -176,6 +181,7 @@ export interface StateBody {
|
|
|
176
181
|
readonly archived?: readonly ArchivedRow[]
|
|
177
182
|
/** gh#41 fleet planning: each tracked peer's published canvas org. */
|
|
178
183
|
readonly peerOrg?: readonly PeerOrgRow[]
|
|
184
|
+
readonly registry?: { teams?: ReadonlyArray<{ team: string; members: readonly string[]; live?: number }> }
|
|
179
185
|
readonly archivedCount?: number
|
|
180
186
|
/** Settled-archive split by verdict (the receipt outcome vocabulary — 'completed' | 'rejected' | 'failed' | … | 'abandoned' — or 'unclassified'). */
|
|
181
187
|
readonly archivedSplit?: Readonly<Record<string, number>>
|
|
@@ -210,7 +216,8 @@ export interface WorkbenchDeps {
|
|
|
210
216
|
abandon(taskId: string, reason: string): Promise<boolean>
|
|
211
217
|
/** Canvas team member change via the control route (0.6.6): `id` is a
|
|
212
218
|
* local session id or a zone-qualified remote handle. True on success. */
|
|
213
|
-
canvasOp(action: 'add-member' | 'remove-member', name: string, id: string): Promise<boolean>
|
|
219
|
+
canvasOp(action: 'add-member' | 'remove-member', name: string, id: string, home?: string): Promise<boolean>
|
|
220
|
+
cliHomeOptions?(name: string, id: string): Promise<CliHomeOptions>
|
|
214
221
|
/** Quick dispatch (0.6.9): POST /__dsh_a2a/dispatch as a human gesture —
|
|
215
222
|
* one message to a routing team, async delivery, receipt lands in the
|
|
216
223
|
* ledger. Optional — without it the 快派 form hides. */
|
|
@@ -331,6 +338,7 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
331
338
|
// Canvas add-member form cache (static DOM re-mounted across polls; keyed
|
|
332
339
|
// by team name — same trick as draftForm so inputs survive the 3s poll).
|
|
333
340
|
let canvasForm: { name: string; form: HTMLFormElement; pick: HTMLSelectElement; manual: HTMLInputElement } | null = null
|
|
341
|
+
const cliHomeForms = new Map<string, HTMLFormElement>()
|
|
334
342
|
// Quick-dispatch form (0.6.9), cached per target routing team the same
|
|
335
343
|
// draftForm way: a textarea's draft must survive the 3s poll remount.
|
|
336
344
|
let dispatchForm: { team: string; form: HTMLFormElement; text: HTMLTextAreaElement; note: HTMLElement } | null = null
|
|
@@ -688,7 +696,7 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
688
696
|
for (const row of ring) {
|
|
689
697
|
const line = el('div', 'wb-row')
|
|
690
698
|
const dir = el('span', 'wb-row-dir')
|
|
691
|
-
dir.textContent = row.dir === 'out' ? '→' : '←'
|
|
699
|
+
dir.textContent = row.dir === 'fwd' ? '⇢' : row.dir === 'out' ? '→' : '←'
|
|
692
700
|
if (row.ok !== true) dir.classList.add('is-fail')
|
|
693
701
|
line.appendChild(dir)
|
|
694
702
|
teamCell(line, last, row.team)
|
|
@@ -960,12 +968,12 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
960
968
|
}
|
|
961
969
|
for (const member of fresh.members) {
|
|
962
970
|
const row = el('div', 'wb-member-row')
|
|
963
|
-
const dot = member.remote === true ? '◇ 远端' : member.live === true ? '● 现场' : member.joined === true ? '○ 冷启' : '· 未入网'
|
|
971
|
+
const dot = member.cliHeld === true ? '本机持有,无需指定' : member.remote === true ? (member.cliHome === null ? '◇ 远端 · home 未确定' : '◇ 远端') : member.live === true ? '● 现场' : member.joined === true ? '○ 冷启' : '· 未入网'
|
|
964
972
|
row.appendChild(el('span', 'wb-member-dot')).textContent = dot
|
|
965
973
|
const label = el('span', 'wb-member-label')
|
|
966
974
|
const session = (last?.sessions ?? []).find(s => s.id === member.id)
|
|
967
975
|
label.textContent = member.remote === true ? member.id : (session !== undefined ? displayName(session) : member.id)
|
|
968
|
-
label.title = member.team ?? member.id
|
|
976
|
+
label.title = member.cliHeld === true ? '本机持有,无需指定' : member.cliHome !== undefined ? (member.cliHome ?? 'home 未确定') : (member.team ?? member.id)
|
|
969
977
|
row.appendChild(label)
|
|
970
978
|
const remove = el('button', 'wb-member-remove')
|
|
971
979
|
remove.type = 'button'
|
|
@@ -976,6 +984,15 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
976
984
|
})
|
|
977
985
|
row.appendChild(remove)
|
|
978
986
|
card.appendChild(row)
|
|
987
|
+
if (member.cliHome === null && member.cliHeld !== true && deps.cliHomeOptions !== undefined) {
|
|
988
|
+
const key = JSON.stringify([fresh.name, member.id])
|
|
989
|
+
let form = cliHomeForms.get(key)
|
|
990
|
+
if (form === undefined) {
|
|
991
|
+
form = createCliHomePicker(() => deps.cliHomeOptions!(fresh.name, member.id), home => deps.canvasOp('add-member', fresh.name, member.id, home))
|
|
992
|
+
cliHomeForms.set(key, form)
|
|
993
|
+
}
|
|
994
|
+
card.appendChild(form)
|
|
995
|
+
}
|
|
979
996
|
}
|
|
980
997
|
// Add-member form: static DOM re-mounted per poll (draftForm trick) —
|
|
981
998
|
// rebuilt only when the inspected team changes.
|
|
@@ -1194,6 +1211,8 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
1194
1211
|
}
|
|
1195
1212
|
|
|
1196
1213
|
function update(state: StateBody | null): void {
|
|
1214
|
+
const unknown = new Set((state?.canvas?.teams ?? []).flatMap(team => team.members.filter(member => member.cliHome === null && member.cliHeld !== true).map(member => JSON.stringify([team.name, member.id]))))
|
|
1215
|
+
for (const key of cliHomeForms.keys()) if (!unknown.has(key)) cliHomeForms.delete(key)
|
|
1197
1216
|
last = state
|
|
1198
1217
|
// 0.6.12: refresh the readable-log list opportunistically (single flight;
|
|
1199
1218
|
// the sidebar re-renders when rows land, mid-poll remount is idempotent).
|
package/nexus-stage/src/world.ts
CHANGED
|
@@ -153,7 +153,11 @@ export function planSeatClear(sid: string, occupied: ReadonlyArray<{ readonly x:
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
/** Minimal session row shape the model needs (topology.SessionRow satisfies it). */
|
|
156
|
-
export interface SessionLite {
|
|
156
|
+
export interface SessionLite {
|
|
157
|
+
id: string; label: string; team: string; name?: string; description?: string; joined: boolean; live?: boolean; teams?: readonly string[]
|
|
158
|
+
/** Live local sessions only: mid-turn ('running') or idle, and how many receipt-owing tasks wait on it. */
|
|
159
|
+
status?: 'running' | 'idle'; queued?: number
|
|
160
|
+
}
|
|
157
161
|
|
|
158
162
|
/** Minimal team shape (topology.CanvasTeam satisfies it). `team` is the
|
|
159
163
|
* mono route alias `<host>/canvas/<name>` when the payload carries it. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nelsonlongxiang/dsh-open-a2a-net",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.34",
|
|
4
4
|
"description": "Open A2A network plugin for DeepSeek Harness: signed agent cards, decentralized peer/zone discovery, direct routing model tools, and joinable session nodes with a sidebar control",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -84,8 +84,10 @@
|
|
|
84
84
|
"test:mesh": "vitest run tests/mesh",
|
|
85
85
|
"test:live": "vitest run tests/live",
|
|
86
86
|
"test:build-layout": "vitest run tests/gates/build-layout.spec.ts",
|
|
87
|
+
"verify:docs": "node scripts/doc-drift.mjs",
|
|
88
|
+
"reanchor:arch": "node scripts/reanchor-architecture.mjs",
|
|
87
89
|
"verify:nexus": "node scripts/verify-nexus-ci.mjs && npm --prefix nexus-stage run build && npm --prefix nexus-stage run test && git diff --exit-code -- assets/nexusDist/assets && git diff --ignore-cr-at-eol --exit-code -- assets/nexusDist/index.html",
|
|
88
|
-
"verify": "pnpm typecheck && pnpm build && pnpm test && pnpm test:build-layout && pnpm verify:nexus"
|
|
90
|
+
"verify": "pnpm typecheck && pnpm verify:docs && pnpm build && pnpm test && pnpm test:build-layout && pnpm verify:nexus"
|
|
89
91
|
},
|
|
90
92
|
"peerDependencies": {
|
|
91
93
|
"@deepseek-ai/cordis": ">=4.0.1",
|