pilotswarm 0.5.28 → 0.5.30
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/mcp/dist/bin/pilotswarm-mcp.js +4 -0
- package/mcp/dist/bin/pilotswarm-mcp.js.map +1 -1
- package/mcp/dist/src/context.d.ts +43 -10
- package/mcp/dist/src/context.d.ts.map +1 -1
- package/mcp/dist/src/context.js +15 -7
- package/mcp/dist/src/context.js.map +1 -1
- package/mcp/dist/src/resources/artifacts.d.ts.map +1 -1
- package/mcp/dist/src/resources/artifacts.js +4 -3
- package/mcp/dist/src/resources/artifacts.js.map +1 -1
- package/mcp/dist/src/server.d.ts.map +1 -1
- package/mcp/dist/src/server.js +2 -0
- package/mcp/dist/src/server.js.map +1 -1
- package/mcp/dist/src/tools/agent-packages.d.ts +14 -0
- package/mcp/dist/src/tools/agent-packages.d.ts.map +1 -0
- package/mcp/dist/src/tools/agent-packages.js +126 -0
- package/mcp/dist/src/tools/agent-packages.js.map +1 -0
- package/mcp/dist/src/tools/agents.d.ts.map +1 -1
- package/mcp/dist/src/tools/agents.js +18 -0
- package/mcp/dist/src/tools/agents.js.map +1 -1
- package/mcp/dist/src/tools/artifacts.d.ts.map +1 -1
- package/mcp/dist/src/tools/artifacts.js +12 -11
- package/mcp/dist/src/tools/artifacts.js.map +1 -1
- package/mcp/dist/src/tools/capabilities.js +1 -1
- package/mcp/dist/src/tools/capabilities.js.map +1 -1
- package/mcp/dist/src/tools/observability.js +1 -1
- package/mcp/dist/src/tools/observability.js.map +1 -1
- package/mcp/dist/src/tools/sessions.js +2 -2
- package/mcp/dist/src/tools/sessions.js.map +1 -1
- package/mcp/dist/src/tools/system.d.ts.map +1 -1
- package/mcp/dist/src/tools/system.js +56 -5
- package/mcp/dist/src/tools/system.js.map +1 -1
- package/mcp/dist/src/tools/turn-control.js +1 -1
- package/mcp/dist/src/tools/turn-control.js.map +1 -1
- package/mcp/dist/src/util/command.d.ts +3 -3
- package/mcp/dist/src/util/command.d.ts.map +1 -1
- package/mcp/dist/src/util/command.js +1 -1
- package/mcp/dist/src/util/command.js.map +1 -1
- package/package.json +6 -6
- package/tui/bin/tui.js +6 -0
- package/tui/src/agents-cli.js +442 -0
- package/tui/src/app.js +44 -1
- package/tui/src/node-sdk-transport.js +386 -5
- package/tui/src/plugin-config.js +17 -0
- package/ui/core/src/commands.js +7 -0
- package/ui/core/src/controller.js +498 -33
- package/ui/core/src/formatting.js +60 -16
- package/ui/core/src/index.js +2 -0
- package/ui/core/src/reducer.js +421 -34
- package/ui/core/src/repo-import.js +298 -0
- package/ui/core/src/repo-links.js +54 -0
- package/ui/core/src/selectors.js +879 -91
- package/ui/core/src/session-tree.js +109 -8
- package/ui/core/src/state.js +108 -2
- package/ui/core/src/themes/helpers.js +4 -1
- package/ui/core/src/themes/index.js +4 -10
- package/ui/core/src/themes/workspace-dark-rich.js +65 -0
- package/ui/core/src/themes/workspace-dark.js +65 -0
- package/ui/react/src/components.js +181 -4
- package/ui/react/src/web-app.js +1410 -91
- package/web/api/router.js +20 -2
- package/web/authz.js +4 -10
- package/web/dist/assets/{abnfDiagram-VRR7QNED-DLV45wH4.js → abnfDiagram-VRR7QNED-Cimc0f-u.js} +1 -1
- package/web/dist/assets/{arc-CE4xAYdE.js → arc-D8-_Dx5J.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-DfzR8-9F.js → architectureDiagram-ZJ3FMSHR-lzMAuQHJ.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-C2pZdqvH.js → blockDiagram-677ZJIJ3-Dp2CWjVS.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-CKRspTan.js → c4Diagram-LMCZKHZV-DF93UkOy.js} +1 -1
- package/web/dist/assets/channel-OwWeiCox.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-CNjLrfHT.js → chunk-2Q5K7J3B-pbsQRM-F.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-Bv5EwyaJ.js → chunk-32BRIVSS-B_f-gG1O.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-NDBesEbJ.js → chunk-5VM5RSS4-gqlpJDmt.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-DHQht9ty.js → chunk-EX3LRPZG-Cms7hPmM.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-r0OQpJRF.js → chunk-JWPE2WC7-Boq5PtPI.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-fcwIDgtl.js → chunk-MOJQB5TN-DVn9i8zt.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-x7XQlDO1.js → chunk-RYQCIY6F-a1vKs3UX.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-U8ZNVy1m.js → chunk-V7JOEXUC-i60p5fHd.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-DLEdPLcN.js → chunk-VR4S4FIN-a8p0o-8B.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-TeyAtB-i.js → chunk-XXDRQBXY-BwP1yBKd.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-CDaP7nuC.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-CDaP7nuC.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-B3bAl1-Z.js → cose-bilkent-JH36ORCC-HW6kGgze.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-DexP8nG-.js → cynefin-VYW2F7L2-YUFMn-vE.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-Doa2XnSs.js → cynefinDiagram-TSTJHNR4-Brdy17i4.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-DZMBKQcJ.js → dagre-VKFMJZFB-CAiSJJj-.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-BkzibmSq.js → diagram-FQU43EPY-B63Cqe0J.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-CihyHR3a.js → diagram-G47NLZAW-ByktrDJ2.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-OMDUUxwW.js → diagram-NH7WQ7WH-BLSfRMoE.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-Lc0imJba.js → diagram-OA4YK3LP-DNVu-zpk.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-Dlcy8hPe.js → diagram-WEI45ONY-CNf4Sz-I.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-Dh3u17Dg.js → ebnfDiagram-CCIWWBDH-9CT3ergx.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-C0OhlUC2.js → erDiagram-Q63AITRT-BPJHAMgl.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-CADKxT-x.js → flowDiagram-23GEKE2U-BVtpr6Iu.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-iVVdxWB7.js → ganttDiagram-NO4QXBWP-BTHnv9sV.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-BwDUec_a.js → gitGraphDiagram-IHSO6WYX-BfmtqbL5.js} +1 -1
- package/web/dist/assets/index-B37T1c2s.css +1 -0
- package/web/dist/assets/index-Bmb8qK39.js +24 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-CGPUif0s.js → infoDiagram-FWYZ7A6U-kU4i4HRu.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-QYnrl-KA.js → ishikawaDiagram-FXEZZL3T-Ch7i-E8O.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-CeKixxL0.js → journeyDiagram-5HDEW3XC-CPQxp0ps.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-CeYokFBS.js → kanban-definition-HUTT4EX6-BBgMVqoJ.js} +1 -1
- package/web/dist/assets/{linear-BbA6_gnz.js → linear-rwksvzXJ.js} +1 -1
- package/web/dist/assets/{mermaid.core--xGRoQTa.js → mermaid.core-o7PszRUc.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-D53u8h0z.js → mindmap-definition-LN4V7U3C-wgOTdzlq.js} +1 -1
- package/web/dist/assets/{papaparse.min-BU0o7jXn.js → papaparse.min-BRXtjEI6.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-B7QSyG-3.js → pegDiagram-2B236MQR-BkhQqqTm.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-XtSGL7GG.js → pieDiagram-ENE6RG2P-CcMnvxUz.js} +1 -1
- package/web/dist/assets/pilotswarm-DYXtzuUN.js +99 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-LZoYSbTH.js → quadrantDiagram-ABIIQ3AL-_KD1QeEr.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-B5FREx2U.js → railroadDiagram-RFXS5EU6-CqI0olIe.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-1VRFkBQX.js → requirementDiagram-TGXJPOKE-1JxRwusF.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-J456gWvu.js → sankeyDiagram-HTMAVEWB-9IpFoKzi.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CaCcw6KO.js → sequenceDiagram-DBY2YBRQ-teyObSQ9.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-DClsP3_O.js → sizeCapture-X5ZJPWSS-DVhU2J_6.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-D6VyrKhd.js → stateDiagram-2N3HPSRC-DHPoWpIQ.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DJjZWYLS.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-q678iAuo.js → swimlanes-5IMT3BWC-BxTtDQoB.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-CV12vDcK.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-WnPK_zhe.js → timeline-definition-FHXFAJF6-D0wXZx-4.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-WpumuLld.js → vennDiagram-L72KCM5P-BSFyI3iV.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-CBrYLUm_.js → wardleyDiagram-EHGQE667-BN-S7hh0.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-DbX1rnDW.js → xychartDiagram-FW5EYKEG-nDNuPs7o.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/runtime.js +36 -4
- package/ui/core/src/themes/daylight.js +0 -62
- package/ui/core/src/themes/light-high-contrast.js +0 -65
- package/ui/core/src/themes/paper-ink.js +0 -62
- package/ui/core/src/themes/solarized-ops.js +0 -59
- package/ui/core/src/themes/tokyo-night.js +0 -56
- package/web/dist/assets/channel-D7p-E4El.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-yFZ-1yMb.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-yFZ-1yMb.js +0 -1
- package/web/dist/assets/index-BGWTu1KK.css +0 -1
- package/web/dist/assets/index-Djfdr7ts.js +0 -24
- package/web/dist/assets/pilotswarm-CtO819QH.js +0 -94
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-D7eKFhHb.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-DlVHQBQx.js +0 -8
package/ui/core/src/selectors.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { INSPECTOR_TABS, FOCUS_REGIONS } from "./commands.js";
|
|
2
|
+
import { isManuallyOrderableSession } from "./session-tree.js";
|
|
2
3
|
import {
|
|
3
4
|
createSplashCard,
|
|
4
5
|
isRehydrationNoticeText,
|
|
@@ -216,15 +217,131 @@ function initialsFromText(value) {
|
|
|
216
217
|
.split(/[^A-Za-z0-9]+/u)
|
|
217
218
|
.map((part) => part.trim())
|
|
218
219
|
.filter(Boolean);
|
|
219
|
-
|
|
220
|
+
// UPPERCASE: these render as a monogram avatar, where lowercase reads as
|
|
221
|
+
// a typo rather than a name. Two letters from "First Last", else the
|
|
222
|
+
// first two of a single token.
|
|
223
|
+
if (parts.length >= 2) return `${parts[0][0]}${parts[1][0]}`.toUpperCase();
|
|
220
224
|
const compact = (parts[0] || text).replace(/[^A-Za-z0-9]/gu, "");
|
|
221
|
-
return compact.slice(0, 2).
|
|
225
|
+
return compact.slice(0, 2).toUpperCase();
|
|
222
226
|
}
|
|
223
227
|
|
|
224
228
|
function ownerInitials(owner) {
|
|
225
229
|
return initialsFromText(owner?.displayName) || initialsFromText(owner?.email) || "?";
|
|
226
230
|
}
|
|
227
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Which sibling list a row is ordered within — the unit a drag may rearrange.
|
|
234
|
+
*
|
|
235
|
+
* Rows only ever sort against their own siblings, and the bands keep the
|
|
236
|
+
* kinds apart, so the container is what decides whether a drop means "put it
|
|
237
|
+
* here" (same container ⇒ reorder) or "file it in there" (different one ⇒
|
|
238
|
+
* the folder gesture). Pinned rows form their own band and therefore their
|
|
239
|
+
* own list, which is why the pin state is part of the key.
|
|
240
|
+
*
|
|
241
|
+
* Returns null for rows that cannot be ordered at all.
|
|
242
|
+
*/
|
|
243
|
+
export function manualOrderContainerKey(session, pinned = false) {
|
|
244
|
+
if (!isManuallyOrderableSession(session)) return null;
|
|
245
|
+
if (pinned) return "pinned";
|
|
246
|
+
if (session.isGroup) return "folders";
|
|
247
|
+
if (session.groupId) return `g:${session.groupId}`;
|
|
248
|
+
return "";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Identity directory built from the sessions already in state.
|
|
253
|
+
*
|
|
254
|
+
* Session owners arrive as FULL principals — provider, subject, email and
|
|
255
|
+
* displayName — while an agent package carries only `{provider, subject}`
|
|
256
|
+
* plus a createdBy email. Resolving the package's principal against this
|
|
257
|
+
* directory recovers the real name, which is what makes initials come out as
|
|
258
|
+
* a person's initials ("Affan Dar" → AD) rather than the first two letters of
|
|
259
|
+
* an alias ("daraffan@…" → DA). No fetch: the data is already loaded.
|
|
260
|
+
*/
|
|
261
|
+
/**
|
|
262
|
+
* The one owner-badge descriptor both lists render from. Same person ⇒ same
|
|
263
|
+
* initials and same colour, wherever they appear.
|
|
264
|
+
*/
|
|
265
|
+
export function ownerBadgeFor(owner, { isMine = false } = {}) {
|
|
266
|
+
if (!owner) return null;
|
|
267
|
+
const name = ownerDisplayName(owner, owner?.email || "");
|
|
268
|
+
return {
|
|
269
|
+
initials: ownerInitials(owner),
|
|
270
|
+
hue: ownerBadgeHue(ownerBadgeKey(owner, owner?.email)),
|
|
271
|
+
name: name || "unknown user",
|
|
272
|
+
isMine: Boolean(isMine),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function ownerDirectoryFromSessions(byId) {
|
|
277
|
+
const directory = new Map();
|
|
278
|
+
for (const session of Object.values(byId || {})) {
|
|
279
|
+
const owner = session?.owner;
|
|
280
|
+
if (!owner?.subject) continue;
|
|
281
|
+
if (!owner.displayName && !owner.email) continue;
|
|
282
|
+
const key = ownerKeyForOwner(owner);
|
|
283
|
+
if (key && !directory.has(key)) directory.set(key, owner);
|
|
284
|
+
}
|
|
285
|
+
return directory;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* A package's owner, resolved to the richest identity available.
|
|
290
|
+
*
|
|
291
|
+
* A DISPLAY NAME beats an email, because initials should be the person's
|
|
292
|
+
* ("Affan Dar" → AD), not the first two letters of their alias
|
|
293
|
+
* ("daraffan@…" → DA) — which is exactly the mismatch that made the same
|
|
294
|
+
* person read differently in the session list and the package list.
|
|
295
|
+
*/
|
|
296
|
+
function resolvePackageOwner(pkg, directory) {
|
|
297
|
+
// The server resolves this now (migration 0041 joins the users table, the
|
|
298
|
+
// same way the session view always has), so owner arrives with a real
|
|
299
|
+
// display name and the two lists agree by construction.
|
|
300
|
+
if (pkg?.owner?.displayName || pkg?.owner?.email) return pkg.owner;
|
|
301
|
+
// Deployments still on an older schema return a bare principal. Recover
|
|
302
|
+
// the name from sessions already in state where we can, and fall back to
|
|
303
|
+
// the createdBy alias only when nothing better exists.
|
|
304
|
+
const fromDirectory = directory?.get?.(ownerKeyForOwner(pkg?.owner)) || null;
|
|
305
|
+
if (fromDirectory) return fromDirectory;
|
|
306
|
+
return pkg?.createdBy ? { email: pkg.createdBy } : null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Owner badge palette. Colour is derived from a stable identity key so the
|
|
311
|
+
* same person is the same colour everywhere and across reloads — the point of
|
|
312
|
+
* the badge is telling owners APART at a glance, which two-letter initials
|
|
313
|
+
* alone do poorly (and did not do at all while they were all "?").
|
|
314
|
+
*
|
|
315
|
+
* Eight hues, all legible on both the light and dark portal grounds; the
|
|
316
|
+
* viewer's own rows are styled separately by the caller, so this palette
|
|
317
|
+
* never has to encode "mine".
|
|
318
|
+
*/
|
|
319
|
+
const OWNER_BADGE_HUES = 8;
|
|
320
|
+
|
|
321
|
+
export function ownerBadgeHue(key) {
|
|
322
|
+
const text = String(key || "").trim().toLowerCase();
|
|
323
|
+
if (!text) return 0;
|
|
324
|
+
// FNV-1a: tiny, dependency-free, and stable across processes — a hash that
|
|
325
|
+
// changed between the TUI and the portal would defeat the whole purpose.
|
|
326
|
+
let hash = 0x811c9dc5;
|
|
327
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
328
|
+
hash ^= text.charCodeAt(i);
|
|
329
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
330
|
+
}
|
|
331
|
+
return hash % OWNER_BADGE_HUES;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Stable identity key for badge colouring: the email/name that names a human. */
|
|
335
|
+
export function ownerBadgeKey(owner, createdBy) {
|
|
336
|
+
return String(
|
|
337
|
+
createdBy
|
|
338
|
+
|| owner?.email
|
|
339
|
+
|| owner?.displayName
|
|
340
|
+
|| (owner?.provider && owner?.subject ? `${owner.provider}:${owner.subject}` : "")
|
|
341
|
+
|| "",
|
|
342
|
+
).trim().toLowerCase();
|
|
343
|
+
}
|
|
344
|
+
|
|
228
345
|
function shouldDecorateSessionOwners(state) {
|
|
229
346
|
// Decorate owners only when the list actually surfaces more than one
|
|
230
347
|
// distinct human owner — otherwise the owner chip is pure noise on every
|
|
@@ -381,6 +498,11 @@ function buildSessionTitle(session, brandingTitle) {
|
|
|
381
498
|
|
|
382
499
|
function matchesOwnerFilterDirect(session, ownerFilter = {}, auth = {}, ownerOverride = undefined) {
|
|
383
500
|
if (!ownerFilter || ownerFilter.all === true) return true;
|
|
501
|
+
// A group is the VIEWER'S OWN private organization — the server lists only
|
|
502
|
+
// your groups, and the row itself carries no owner. Owner filters are about
|
|
503
|
+
// whose sessions to show, so they must never hide your own folders (the
|
|
504
|
+
// group's members are still filtered on their own merits).
|
|
505
|
+
if (session?.isGroup) return true;
|
|
384
506
|
if (session?.isSystem) return ownerFilter.includeSystem === true;
|
|
385
507
|
const ownerKey = ownerKeyForOwner(ownerOverride !== undefined ? ownerOverride : session?.owner);
|
|
386
508
|
if (!ownerKey) return ownerFilter.includeUnowned === true;
|
|
@@ -595,7 +717,11 @@ function buildSessionRowView(entry, session, state, totalDescendantCounts, visib
|
|
|
595
717
|
: "";
|
|
596
718
|
|
|
597
719
|
if (depthPrefix) {
|
|
598
|
-
|
|
720
|
+
// `role` lets a host drop this run without losing the rest: the portal
|
|
721
|
+
// draws nesting as a surface (the "well") and would otherwise show a
|
|
722
|
+
// box-drawing character inside it. The TUI renders text+colour and
|
|
723
|
+
// ignores the field, so its glyphs are untouched.
|
|
724
|
+
prefixRuns.push({ text: depthPrefix, color: "gray", role: "depth" });
|
|
599
725
|
}
|
|
600
726
|
|
|
601
727
|
const pinnedSet = new Set(Array.isArray(state.sessions?.pinnedIds) ? state.sessions.pinnedIds : []);
|
|
@@ -604,12 +730,11 @@ function buildSessionRowView(entry, session, state, totalDescendantCounts, visib
|
|
|
604
730
|
const isSelected = selectedSet.has(entry.sessionId);
|
|
605
731
|
const isPinned = pinnedSet.has(entry.sessionId);
|
|
606
732
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
});
|
|
733
|
+
// Multi-selection reads as a selection BAR on the chosen rows (plus the
|
|
734
|
+
// host's own row styling) — not a checkbox column that shifts every row
|
|
735
|
+
// the moment selection mode turns on.
|
|
736
|
+
if (isSelected) {
|
|
737
|
+
prefixRuns.push({ text: "▌", color: "cyan", bold: true });
|
|
613
738
|
}
|
|
614
739
|
|
|
615
740
|
// Pin column renders first on top-level non-system rows so pinned and
|
|
@@ -638,9 +763,13 @@ function buildSessionRowView(entry, session, state, totalDescendantCounts, visib
|
|
|
638
763
|
prefixRuns.push({ text: "⚗ ", color: "magenta", bold: true });
|
|
639
764
|
} else {
|
|
640
765
|
const icon = sessionStatusIcon(session, mode);
|
|
766
|
+
// role:"status" so a host can render this as a MARK rather than a
|
|
767
|
+
// character — the portal draws a dot, since "~" vs "*" is unreadable
|
|
768
|
+
// without a legend. The TUI renders text+colour and ignores the field.
|
|
641
769
|
prefixRuns.push({
|
|
642
770
|
text: icon ? `${icon} ` : " ",
|
|
643
771
|
color: sessionStatusColor(session, mode),
|
|
772
|
+
role: "status",
|
|
644
773
|
});
|
|
645
774
|
}
|
|
646
775
|
|
|
@@ -674,10 +803,13 @@ function buildSessionRowView(entry, session, state, totalDescendantCounts, visib
|
|
|
674
803
|
// Bracketed + bold so it reads as an owner badge, not part of the title.
|
|
675
804
|
// The current viewer's own sessions get a distinct color so "mine" pops.
|
|
676
805
|
if (shouldDecorateSessionOwners(state) && !session?.isSystem && !session?.isGroup) {
|
|
677
|
-
const initials = effectiveOwner ? ownerInitials(effectiveOwner) : "?";
|
|
678
806
|
const viewerKey = ownerKeyForOwner(state?.auth?.principal);
|
|
679
|
-
const isMine = viewerKey && ownerKeyForOwner(effectiveOwner) === viewerKey;
|
|
680
|
-
|
|
807
|
+
const isMine = Boolean(viewerKey && ownerKeyForOwner(effectiveOwner) === viewerKey);
|
|
808
|
+
const initials = effectiveOwner ? ownerInitials(effectiveOwner) : "?";
|
|
809
|
+
// Tagged so the PORTAL can drop this run and draw a real avatar in its
|
|
810
|
+
// place, while the TUI — which can only print text — keeps the chip.
|
|
811
|
+
// Both read from the same descriptor, so they cannot disagree.
|
|
812
|
+
titleRuns.push({ text: `[${initials}] `, color: isMine ? "green" : "cyan", bold: true, ownerChip: true });
|
|
681
813
|
}
|
|
682
814
|
if (hasRealTitle) {
|
|
683
815
|
titleRuns.push({ text: rawTitle, color: mainColor, bold: Boolean(session?.isSystem || session?.isGroup) });
|
|
@@ -881,9 +1013,16 @@ export function selectSessionRows(state) {
|
|
|
881
1013
|
sessionRowMemo.set(state.sessions.flat, memo);
|
|
882
1014
|
}
|
|
883
1015
|
|
|
884
|
-
return state.sessions.flat.
|
|
885
|
-
|
|
886
|
-
|
|
1016
|
+
return state.sessions.flat.filter((entry) => (
|
|
1017
|
+
// A row whose session is in neither place can render nothing but an
|
|
1018
|
+
// owner chip and a dash - the "[?]" ghost. Drop it rather than show it.
|
|
1019
|
+
Boolean(state.sessions.byId[entry.sessionId] || entry.standIn)
|
|
1020
|
+
)).map((entry) => {
|
|
1021
|
+
const session = state.sessions.byId[entry.sessionId] || entry.standIn;
|
|
1022
|
+
// The LIST highlight can be cleared (click empty space) without
|
|
1023
|
+
// detaching the session — the chat and inspector panes keep it.
|
|
1024
|
+
const active = entry.sessionId === state.sessions.activeSessionId
|
|
1025
|
+
&& !state.sessions.listDeselected;
|
|
887
1026
|
const pinned = pinnedSet.has(entry.sessionId);
|
|
888
1027
|
const selected = selectedSet.has(entry.sessionId);
|
|
889
1028
|
// Every input the row below is derived from. Identity comparison is
|
|
@@ -911,6 +1050,9 @@ export function selectSessionRows(state) {
|
|
|
911
1050
|
status: session?.status,
|
|
912
1051
|
statusColor: sessionStatusColor(session, state.connection?.mode || "local"),
|
|
913
1052
|
active,
|
|
1053
|
+
// The RAW group id (rows are keyed "group:<uuid>"); the placement
|
|
1054
|
+
// API and drag hit-testing both need the bare id.
|
|
1055
|
+
groupId: session?.groupId || entry?.groupId || null,
|
|
914
1056
|
isSystem: Boolean(session?.isSystem),
|
|
915
1057
|
isGroup: Boolean(session?.isGroup),
|
|
916
1058
|
hasChildren: entry.hasChildren,
|
|
@@ -918,6 +1060,18 @@ export function selectSessionRows(state) {
|
|
|
918
1060
|
pinned,
|
|
919
1061
|
selected,
|
|
920
1062
|
canPin: canPinSessionRow(session),
|
|
1063
|
+
// Manual-ordering affordances. Derived HERE so the drag layer and
|
|
1064
|
+
// the tree agree on what may move — a UI that permits a drag the
|
|
1065
|
+
// tree then ignores is worse than offering no drag at all.
|
|
1066
|
+
// The avatar the portal draws in place of the tagged text run.
|
|
1067
|
+
ownerBadge: (shouldDecorateSessionOwners(state) && !session?.isSystem && !session?.isGroup && effectiveSessionOwner(session, state.sessions.byId))
|
|
1068
|
+
? ownerBadgeFor(effectiveSessionOwner(session, state.sessions.byId), {
|
|
1069
|
+
isMine: Boolean(ownerKeyForOwner(state?.auth?.principal)
|
|
1070
|
+
&& ownerKeyForOwner(effectiveSessionOwner(session, state.sessions.byId)) === ownerKeyForOwner(state?.auth?.principal)),
|
|
1071
|
+
})
|
|
1072
|
+
: null,
|
|
1073
|
+
orderable: isManuallyOrderableSession(session),
|
|
1074
|
+
orderContainer: manualOrderContainerKey(session, pinned),
|
|
921
1075
|
};
|
|
922
1076
|
memo.set(entry.sessionId, { deps, row });
|
|
923
1077
|
return row;
|
|
@@ -2969,6 +3123,75 @@ export function selectActiveActivity(state) {
|
|
|
2969
3123
|
return history?.activity || [];
|
|
2970
3124
|
}
|
|
2971
3125
|
|
|
3126
|
+
/**
|
|
3127
|
+
* Worker details — the pane that REPLACES Activity while the Node Map is up.
|
|
3128
|
+
* Registry specs for the selected node, then the sessions executing on it.
|
|
3129
|
+
*/
|
|
3130
|
+
export function selectWorkerDetailsPane(state) {
|
|
3131
|
+
const view = selectNodeMapView(state);
|
|
3132
|
+
const node = view.selected ? view.nodes.find((candidate) => candidate.label === view.selected) : null;
|
|
3133
|
+
const title = buildPaneTitleRuns("Worker", "gray");
|
|
3134
|
+
if (!node) {
|
|
3135
|
+
title.push({ text: " none", color: "gray" });
|
|
3136
|
+
return {
|
|
3137
|
+
title,
|
|
3138
|
+
lines: [{
|
|
3139
|
+
text: view.degraded && view.registryError
|
|
3140
|
+
? `No worker selected — ${view.registryError}`
|
|
3141
|
+
: "No workers to show yet.",
|
|
3142
|
+
color: "gray",
|
|
3143
|
+
}],
|
|
3144
|
+
};
|
|
3145
|
+
}
|
|
3146
|
+
title.push({ text: ` ${node.label}`, color: "cyan" });
|
|
3147
|
+
const lines = [];
|
|
3148
|
+
const spec = (label, value, color = "white") => {
|
|
3149
|
+
if (value === null || value === undefined || value === "") return;
|
|
3150
|
+
lines.push([{ text: `${label.padEnd(10)} `, color: "gray" }, { text: String(value), color }]);
|
|
3151
|
+
};
|
|
3152
|
+
if (node.registered) {
|
|
3153
|
+
spec("Node", node.workerNodeId);
|
|
3154
|
+
spec("Phase", node.phase, node.phase === "draining" ? "red" : node.phase === "starting" ? "yellow" : "green");
|
|
3155
|
+
spec("Pool", node.pool);
|
|
3156
|
+
if (node.owner) spec("Owner", node.owner);
|
|
3157
|
+
spec("Heartbeat", node.live ? `${node.agoText ?? "now"} · live` : `${node.agoText ?? "unknown"} · stale`, node.live ? "green" : "red");
|
|
3158
|
+
spec("Uptime", node.uptimeText);
|
|
3159
|
+
spec("Memory", [node.rssText ? `rss ${node.rssText}` : null, node.heapText ? `heap ${node.heapText}` : null].filter(Boolean).join(" · ") || null);
|
|
3160
|
+
spec("Loop p99", node.eventLoopText);
|
|
3161
|
+
spec("SDK", node.sdkVersion);
|
|
3162
|
+
spec("Runtime", node.substrate);
|
|
3163
|
+
if (node.capabilities.length) spec("Caps", node.capabilities.join(", "));
|
|
3164
|
+
if (node.consumes.length) spec("Consumes", node.consumes.join(", "));
|
|
3165
|
+
if (node.pkgEpoch !== null) {
|
|
3166
|
+
const ok = node.pkgInstalled.filter((pkg) => pkg.status === "ok").length;
|
|
3167
|
+
const bad = node.pkgInstalled.length - ok;
|
|
3168
|
+
spec("Packages", `epoch ${node.pkgEpoch} · ${ok} ok${bad ? ` · ${bad} error` : ""}`, bad ? "red" : "white");
|
|
3169
|
+
for (const pkg of node.pkgInstalled) {
|
|
3170
|
+
lines.push([
|
|
3171
|
+
{ text: " ", color: "gray" },
|
|
3172
|
+
{ text: `${pkg.status === "error" ? "✗" : "✓"} `, color: pkg.status === "error" ? "red" : "green" },
|
|
3173
|
+
{ text: `${pkg.name}${pkg.semver ? `@${pkg.semver}` : ""}`, color: pkg.status === "error" ? "red" : "white" },
|
|
3174
|
+
]);
|
|
3175
|
+
if (pkg.error) lines.push([{ text: ` ${pkg.error}`, color: "red" }]);
|
|
3176
|
+
}
|
|
3177
|
+
if (node.pkgLastError) lines.push([{ text: ` last refresh error: ${node.pkgLastError}`, color: "red" }]);
|
|
3178
|
+
}
|
|
3179
|
+
} else {
|
|
3180
|
+
lines.push({ text: "Not in the worker registry — derived from recent activity only.", color: "gray" });
|
|
3181
|
+
}
|
|
3182
|
+
lines.push({ text: "", color: "gray" });
|
|
3183
|
+
lines.push([{ text: `EXECUTING (${node.executing.length})`, color: "cyan", bold: true }]);
|
|
3184
|
+
for (const entry of node.executing) {
|
|
3185
|
+
lines.push(entry.active
|
|
3186
|
+
? buildActiveHighlightLine(entry.text)
|
|
3187
|
+
: { text: entry.text, color: entry.color, bold: entry.bold });
|
|
3188
|
+
}
|
|
3189
|
+
if (node.executing.length === 0) {
|
|
3190
|
+
lines.push({ text: `Nothing executing in the ${view.windowLabel} window.`, color: "gray" });
|
|
3191
|
+
}
|
|
3192
|
+
return { title, lines };
|
|
3193
|
+
}
|
|
3194
|
+
|
|
2972
3195
|
export function selectActivityPane(state, maxLines = 12) {
|
|
2973
3196
|
const activity = selectActiveActivity(state);
|
|
2974
3197
|
const session = selectActiveSession(state);
|
|
@@ -3510,6 +3733,28 @@ export function selectFilesView(state, options = {}) {
|
|
|
3510
3733
|
* editing / saving / error), and a small set of "actions" describing
|
|
3511
3734
|
* what keybindings or buttons are currently meaningful.
|
|
3512
3735
|
*/
|
|
3736
|
+
function adminPkgDate(value) {
|
|
3737
|
+
if (!value) return "—";
|
|
3738
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
3739
|
+
return Number.isNaN(date.getTime()) ? "—" : date.toISOString().slice(0, 10);
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
function adminPkgSize(bytes) {
|
|
3743
|
+
const n = Number(bytes) || 0;
|
|
3744
|
+
if (n <= 0) return "0 B";
|
|
3745
|
+
if (n < 1024) return `${n} B`;
|
|
3746
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
3747
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
function adminPkgLanguage(filePath) {
|
|
3751
|
+
const lower = String(filePath || "").toLowerCase();
|
|
3752
|
+
if (lower.endsWith(".md")) return "markdown";
|
|
3753
|
+
if (lower.endsWith(".json")) return "json";
|
|
3754
|
+
if (/\.(mjs|cjs|js)$/.test(lower)) return "javascript";
|
|
3755
|
+
return "text";
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3513
3758
|
export function selectAdminConsole(state) {
|
|
3514
3759
|
const admin = state.admin || {};
|
|
3515
3760
|
const profile = admin.profile || null;
|
|
@@ -3546,7 +3791,380 @@ export function selectAdminConsole(state) {
|
|
|
3546
3791
|
? "Configured (overrides env GITHUB_TOKEN for this user)"
|
|
3547
3792
|
: "Not configured (using env GITHUB_TOKEN fallback)");
|
|
3548
3793
|
|
|
3794
|
+
// ── Agent packages view-model (Admin → Agents) ───────────────
|
|
3795
|
+
const pkgState = admin.packages || {};
|
|
3796
|
+
const pkgList = Array.isArray(pkgState.list) ? pkgState.list : [];
|
|
3797
|
+
const ownsPackage = (pkg) => Boolean(
|
|
3798
|
+
isAdmin
|
|
3799
|
+
|| (pkg?.owner && principal
|
|
3800
|
+
&& pkg.owner.provider === principal.provider
|
|
3801
|
+
&& pkg.owner.subject === principal.subject),
|
|
3802
|
+
);
|
|
3803
|
+
// Built once per render, not per row.
|
|
3804
|
+
const ownerDirectory = ownerDirectoryFromSessions(state?.sessions?.byId);
|
|
3805
|
+
const packageRow = (pkg) => ({
|
|
3806
|
+
name: pkg.name,
|
|
3807
|
+
scope: pkg.scope === "shared" ? "shared" : "user",
|
|
3808
|
+
enabled: Boolean(pkg.enabled),
|
|
3809
|
+
semver: pkg.active?.semver || null,
|
|
3810
|
+
sha7: pkg.active?.sha256 ? String(pkg.active.sha256).slice(0, 7) : null,
|
|
3811
|
+
agentCount: Array.isArray(pkg.active?.manifest?.agents) ? pkg.active.manifest.agents.length : 0,
|
|
3812
|
+
canManage: ownsPackage(pkg),
|
|
3813
|
+
// A user-scope package belongs to a PERSON, so it carries the same
|
|
3814
|
+
// owner-initials chip the session rows use. Shared packages belong to
|
|
3815
|
+
// the deployment and keep the scope badge instead.
|
|
3816
|
+
// Resolved against the session-owner directory so the initials are the
|
|
3817
|
+
// PERSON's, matching the session list exactly.
|
|
3818
|
+
ownerBadge: pkg.scope === "shared" ? null : ownerBadgeFor(resolvePackageOwner(pkg, ownerDirectory)),
|
|
3819
|
+
// Highlight only while the Agents section is active — otherwise the
|
|
3820
|
+
// GitHub Keys screen shows a double selection.
|
|
3821
|
+
selected: admin.section === "packages" && pkgState.selectedName === pkg.name,
|
|
3822
|
+
});
|
|
3823
|
+
// Admins are served every user-scope package, but another person's private
|
|
3824
|
+
// agents are not part of YOUR workspace — keep them out of the main tree
|
|
3825
|
+
// and behind an explicit "Other users" group so the list stays yours.
|
|
3826
|
+
const isMinePackage = (pkg) => Boolean(
|
|
3827
|
+
pkg?.owner && principal
|
|
3828
|
+
&& pkg.owner.provider === principal.provider
|
|
3829
|
+
&& pkg.owner.subject === principal.subject,
|
|
3830
|
+
);
|
|
3831
|
+
// The owner filter is a statement about WHOSE work you want to see, and it
|
|
3832
|
+
// means the same thing here: with Sean filtered out, Sean's private agents
|
|
3833
|
+
// are not part of your workspace either.
|
|
3834
|
+
//
|
|
3835
|
+
// NOT matchesOwnerFilterDirect: for sessions, `includeShared` means "things
|
|
3836
|
+
// other people shared WITH me", a real relation that earns them a place in
|
|
3837
|
+
// your list. Another person's private package has no such relation — you
|
|
3838
|
+
// only see it because you are an admin — so reusing that rule would let
|
|
3839
|
+
// every other user's package through and the filter would do nothing.
|
|
3840
|
+
// Someone else's agents appear only when you asked for that someone (or
|
|
3841
|
+
// for everyone). Your own and the deployment's shared packages are always
|
|
3842
|
+
// yours to see, exactly as session groups are exempt.
|
|
3843
|
+
const ownerFilter = state.sessions?.ownerFilter;
|
|
3844
|
+
const passesOwnerFilter = (pkg) => {
|
|
3845
|
+
if (!ownerFilter || ownerFilter.all === true) return true;
|
|
3846
|
+
const ownerKey = ownerKeyForOwner(pkg?.owner);
|
|
3847
|
+
if (!ownerKey) return ownerFilter.includeUnowned === true;
|
|
3848
|
+
return Array.isArray(ownerFilter.ownerKeys) && ownerFilter.ownerKeys.includes(ownerKey);
|
|
3849
|
+
};
|
|
3850
|
+
const sharedRows = pkgList.filter((pkg) => pkg.scope === "shared").map(packageRow);
|
|
3851
|
+
const userRows = pkgList
|
|
3852
|
+
.filter((pkg) => pkg.scope !== "shared" && isMinePackage(pkg))
|
|
3853
|
+
.map(packageRow);
|
|
3854
|
+
const otherUserRows = pkgList
|
|
3855
|
+
.filter((pkg) => pkg.scope !== "shared" && !isMinePackage(pkg) && passesOwnerFilter(pkg))
|
|
3856
|
+
.map((pkg) => ({ ...packageRow(pkg), ownerLabel: pkg?.createdBy || pkg?.owner?.subject || "another user" }));
|
|
3857
|
+
|
|
3858
|
+
// Settings tree — the session-list-slot navigation. Rendered by both
|
|
3859
|
+
// hosts; `kind` drives affordances (section rows switch panes, package
|
|
3860
|
+
// rows select a package).
|
|
3861
|
+
const section = ["packages", "workers"].includes(admin.section) ? admin.section : "ghcp";
|
|
3862
|
+
const settingsTree = [
|
|
3863
|
+
{ id: "ghcp", kind: "section", depth: 0, label: "GitHub Keys", selected: section === "ghcp" },
|
|
3864
|
+
{ id: "agents", kind: "section", depth: 0, label: "Agents", selected: section === "packages" && !pkgState.selectedName },
|
|
3865
|
+
{ id: "group:shared", kind: "group", depth: 1, label: "Shared", count: sharedRows.length },
|
|
3866
|
+
...sharedRows.map((row) => ({ id: `pkg:${row.name}`, kind: "package", depth: 2, label: row.name, ...row })),
|
|
3867
|
+
{ id: "group:user", kind: "group", depth: 1, label: "User", count: userRows.length },
|
|
3868
|
+
...userRows.map((row) => ({ id: `pkg:${row.name}`, kind: "package", depth: 2, label: row.name, ...row })),
|
|
3869
|
+
...(otherUserRows.length > 0
|
|
3870
|
+
? [
|
|
3871
|
+
{ id: "group:others", kind: "group", depth: 1, label: "Other users", count: otherUserRows.length },
|
|
3872
|
+
...otherUserRows.map((row) => ({ id: `pkg:${row.name}`, kind: "package", depth: 2, label: row.name, ...row })),
|
|
3873
|
+
]
|
|
3874
|
+
: []),
|
|
3875
|
+
// The worker registry is a hard-gated admin read; hide the section
|
|
3876
|
+
// entirely from non-admins rather than showing a doomed pane.
|
|
3877
|
+
...(isAdmin ? [{ id: "workers", kind: "section", depth: 0, label: "Workers", selected: section === "workers" }] : []),
|
|
3878
|
+
];
|
|
3879
|
+
|
|
3880
|
+
// Detail view-model for the selected package.
|
|
3881
|
+
const detail = pkgState.detail || null;
|
|
3882
|
+
let packageDetail = null;
|
|
3883
|
+
if (pkgState.selectedName) {
|
|
3884
|
+
const summary = pkgList.find((pkg) => pkg.name === pkgState.selectedName) || null;
|
|
3885
|
+
const activeVersion = detail?.versions?.find((v) => v.versionId === detail.activeVersionId) || null;
|
|
3886
|
+
const manifest = activeVersion?.manifest || summary?.active?.manifest || {};
|
|
3887
|
+
// Workers are ephemeral pods: rows for retired pod names linger until
|
|
3888
|
+
// the server-side prune, so fleet adoption counts only workers whose
|
|
3889
|
+
// heartbeat (updated_at, touched every ~20s poll) is fresh.
|
|
3890
|
+
const FLEET_LIVENESS_MS = 90_000;
|
|
3891
|
+
const liveCutoff = Date.now() - FLEET_LIVENESS_MS;
|
|
3892
|
+
const workerRows = (Array.isArray(pkgState.workerState) ? pkgState.workerState : [])
|
|
3893
|
+
.filter((worker) => {
|
|
3894
|
+
const at = worker?.updatedAt instanceof Date ? worker.updatedAt : new Date(worker?.updatedAt ?? 0);
|
|
3895
|
+
return !Number.isNaN(at.getTime()) && at.getTime() >= liveCutoff;
|
|
3896
|
+
});
|
|
3897
|
+
const fleetTotal = workerRows.length;
|
|
3898
|
+
const fleetCurrent = activeVersion
|
|
3899
|
+
? workerRows.filter((worker) => worker.installed?.[pkgState.selectedName]?.semver === activeVersion.semver
|
|
3900
|
+
&& worker.installed?.[pkgState.selectedName]?.status === "ok").length
|
|
3901
|
+
: 0;
|
|
3902
|
+
const canManage = detail ? ownsPackage(detail) : (summary ? ownsPackage(summary) : false);
|
|
3903
|
+
packageDetail = {
|
|
3904
|
+
name: pkgState.selectedName,
|
|
3905
|
+
loading: Boolean(pkgState.detailLoading),
|
|
3906
|
+
error: pkgState.detailError || null,
|
|
3907
|
+
scope: (detail?.scope || summary?.scope) === "shared" ? "shared" : "user",
|
|
3908
|
+
enabled: detail ? Boolean(detail.enabled) : Boolean(summary?.enabled),
|
|
3909
|
+
canManage,
|
|
3910
|
+
createdBy: detail?.createdBy || summary?.createdBy || null,
|
|
3911
|
+
createdAtText: adminPkgDate(detail?.createdAt || summary?.createdAt),
|
|
3912
|
+
description: typeof manifest.description === "string" ? manifest.description : "",
|
|
3913
|
+
activeSemver: activeVersion?.semver || summary?.active?.semver || null,
|
|
3914
|
+
activeSha12: activeVersion?.sha256 ? String(activeVersion.sha256).slice(0, 12) : null,
|
|
3915
|
+
sizeText: adminPkgSize(activeVersion?.sizeBytes ?? summary?.active?.sizeBytes),
|
|
3916
|
+
agents: Array.isArray(manifest.agents)
|
|
3917
|
+
? manifest.agents.map((agent) => ({
|
|
3918
|
+
name: agent.name,
|
|
3919
|
+
description: agent.description || "",
|
|
3920
|
+
toolCount: Array.isArray(agent.tools) ? agent.tools.length : 0,
|
|
3921
|
+
skillCount: Array.isArray(agent.skills) ? agent.skills.length : 0,
|
|
3922
|
+
}))
|
|
3923
|
+
: [],
|
|
3924
|
+
versions: (detail?.versions || []).map((version) => ({
|
|
3925
|
+
semver: version.semver,
|
|
3926
|
+
sha12: String(version.sha256 || "").slice(0, 12),
|
|
3927
|
+
dateText: adminPkgDate(version.createdAt),
|
|
3928
|
+
active: version.versionId === detail?.activeVersionId,
|
|
3929
|
+
})),
|
|
3930
|
+
fleet: fleetTotal > 0 && activeVersion
|
|
3931
|
+
? { current: fleetCurrent, total: fleetTotal, text: `${fleetCurrent}/${fleetTotal} workers current` }
|
|
3932
|
+
: null,
|
|
3933
|
+
actionPending: pkgState.action?.pending || null,
|
|
3934
|
+
actionError: pkgState.action?.error || null,
|
|
3935
|
+
};
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
// Workspace view-model: nested tree rows honoring expandedDirs + preview.
|
|
3939
|
+
const workspaceState = pkgState.workspace || {};
|
|
3940
|
+
let workspace = null;
|
|
3941
|
+
if (pkgState.selectedName) {
|
|
3942
|
+
const tree = workspaceState.tree;
|
|
3943
|
+
const expanded = new Set(workspaceState.expandedDirs || []);
|
|
3944
|
+
const treeRows = [];
|
|
3945
|
+
if (tree) {
|
|
3946
|
+
const visible = (relPath) => {
|
|
3947
|
+
const parts = relPath.split("/");
|
|
3948
|
+
for (let i = 1; i < parts.length; i++) {
|
|
3949
|
+
if (!expanded.has(parts.slice(0, i).join("/"))) return false;
|
|
3950
|
+
}
|
|
3951
|
+
return true;
|
|
3952
|
+
};
|
|
3953
|
+
const treeDirs = Array.isArray(tree.dirs) ? tree.dirs : [];
|
|
3954
|
+
const treeFiles = Array.isArray(tree.files) ? tree.files : [];
|
|
3955
|
+
// Segment-aware sort: "/" must sort before every sibling char or a
|
|
3956
|
+
// dir separates from its children (agents/ vs agents-guide.md).
|
|
3957
|
+
const sortKey = (value) => String(value).split("/").join("\u0000");
|
|
3958
|
+
const nodes = [
|
|
3959
|
+
...treeDirs.map((dir) => ({ type: "dir", path: dir })),
|
|
3960
|
+
...treeFiles.map((file) => ({ type: "file", path: file.path, size: file.size })),
|
|
3961
|
+
].sort((a, b) => (sortKey(a.path) < sortKey(b.path) ? -1 : 1));
|
|
3962
|
+
for (const node of nodes) {
|
|
3963
|
+
if (!visible(node.path)) continue;
|
|
3964
|
+
const depth = node.path.split("/").length - 1;
|
|
3965
|
+
treeRows.push({
|
|
3966
|
+
type: node.type,
|
|
3967
|
+
path: node.path,
|
|
3968
|
+
label: node.path.split("/").pop() + (node.type === "dir" ? "/" : ""),
|
|
3969
|
+
depth,
|
|
3970
|
+
expanded: node.type === "dir" ? expanded.has(node.path) : undefined,
|
|
3971
|
+
sizeText: node.type === "file" ? adminPkgSize(node.size) : null,
|
|
3972
|
+
selected: workspaceState.selectedPath === node.path,
|
|
3973
|
+
});
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
const file = workspaceState.file;
|
|
3977
|
+
workspace = {
|
|
3978
|
+
loading: Boolean(workspaceState.treeLoading),
|
|
3979
|
+
error: workspaceState.treeError || null,
|
|
3980
|
+
semver: workspaceState.tree?.semver || null,
|
|
3981
|
+
treeRows,
|
|
3982
|
+
file: file
|
|
3983
|
+
? {
|
|
3984
|
+
path: file.path,
|
|
3985
|
+
sizeText: adminPkgSize(file.size),
|
|
3986
|
+
truncated: Boolean(file.truncated),
|
|
3987
|
+
isBinary: file.encoding === "base64",
|
|
3988
|
+
text: file.encoding === "base64" ? null : String(file.content || ""),
|
|
3989
|
+
language: adminPkgLanguage(file.path),
|
|
3990
|
+
}
|
|
3991
|
+
: null,
|
|
3992
|
+
fileLoading: Boolean(workspaceState.fileLoading),
|
|
3993
|
+
fileError: workspaceState.fileError || null,
|
|
3994
|
+
selectedPath: workspaceState.selectedPath || null,
|
|
3995
|
+
};
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3998
|
+
// ── Worker registry (Admin → Workers) ────────────────────────
|
|
3999
|
+
// Rows self-prune server-side after 1h; the UI additionally marks a
|
|
4000
|
+
// worker live only when its heartbeat is younger than the same 90s
|
|
4001
|
+
// window the fleet-adoption count uses.
|
|
4002
|
+
const workersState = admin.workers || {};
|
|
4003
|
+
const workerList = Array.isArray(workersState.list) ? workersState.list : [];
|
|
4004
|
+
const WORKERS_LIVE_MS = 90_000;
|
|
4005
|
+
const workersNow = Date.now();
|
|
4006
|
+
const workerAgo = (ms) => {
|
|
4007
|
+
if (!Number.isFinite(ms) || ms < 0) return "—";
|
|
4008
|
+
if (ms < 60_000) return `${Math.round(ms / 1000)}s ago`;
|
|
4009
|
+
if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m ago`;
|
|
4010
|
+
return `${Math.round(ms / 3_600_000)}h ago`;
|
|
4011
|
+
};
|
|
4012
|
+
const workerUptime = (seconds) => {
|
|
4013
|
+
if (!Number.isFinite(seconds) || seconds < 0) return null;
|
|
4014
|
+
if (seconds < 60) return `${Math.round(seconds)}s`;
|
|
4015
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
|
4016
|
+
const h = Math.floor(seconds / 3600);
|
|
4017
|
+
return h < 48 ? `${h}h ${Math.floor((seconds % 3600) / 60)}m` : `${Math.floor(h / 24)}d ${h % 24}h`;
|
|
4018
|
+
};
|
|
4019
|
+
const workerRegistryRows = workerList
|
|
4020
|
+
.map((worker) => {
|
|
4021
|
+
const at = worker?.updatedAt instanceof Date ? worker.updatedAt : new Date(worker?.updatedAt ?? 0);
|
|
4022
|
+
const ageMs = Number.isNaN(at.getTime()) ? Number.NaN : workersNow - at.getTime();
|
|
4023
|
+
const health = worker?.health || {};
|
|
4024
|
+
const info = worker?.info || {};
|
|
4025
|
+
const pkg = worker?.state?.["agent-packages"] || null;
|
|
4026
|
+
const installed = pkg?.installed && typeof pkg.installed === "object" ? Object.values(pkg.installed) : [];
|
|
4027
|
+
const pkgErrors = installed.filter((entry) => entry?.status === "error").length;
|
|
4028
|
+
return {
|
|
4029
|
+
id: String(worker?.workerNodeId ?? ""),
|
|
4030
|
+
pool: String(worker?.pool ?? "default"),
|
|
4031
|
+
phase: ["starting", "ready", "draining"].includes(worker?.phase) ? worker.phase : "ready",
|
|
4032
|
+
live: Number.isFinite(ageMs) && ageMs <= WORKERS_LIVE_MS,
|
|
4033
|
+
agoText: workerAgo(ageMs),
|
|
4034
|
+
uptimeText: workerUptime(health.uptimeS),
|
|
4035
|
+
rssText: adminPkgSize(health.rssBytes),
|
|
4036
|
+
sessions: Number.isFinite(health.activeSessions) ? health.activeSessions : null,
|
|
4037
|
+
eventLoopText: Number.isFinite(health.eventLoopDelayP99Ms) ? `${health.eventLoopDelayP99Ms}ms` : null,
|
|
4038
|
+
sdkVersion: typeof info.sdkVersion === "string" ? info.sdkVersion : null,
|
|
4039
|
+
substrate: typeof info.runtime?.substrate === "string" ? info.runtime.substrate : null,
|
|
4040
|
+
consumes: Array.isArray(info.consumes) ? info.consumes : [],
|
|
4041
|
+
owner: worker?.owner?.subject ? String(worker.owner.subject) : null,
|
|
4042
|
+
pkgEpoch: Number.isFinite(pkg?.epoch) ? pkg.epoch : null,
|
|
4043
|
+
pkgText: pkg
|
|
4044
|
+
? `${installed.length - pkgErrors} ok${pkgErrors ? ` · ${pkgErrors} error` : ""}`
|
|
4045
|
+
: null,
|
|
4046
|
+
};
|
|
4047
|
+
})
|
|
4048
|
+
.sort((a, b) => (a.pool === b.pool ? (a.id < b.id ? -1 : 1) : (a.pool < b.pool ? -1 : 1)));
|
|
4049
|
+
const liveWorkerRows = workerRegistryRows.filter((row) => row.live);
|
|
4050
|
+
const workersView = {
|
|
4051
|
+
loading: Boolean(workersState.loading),
|
|
4052
|
+
error: workersState.error || null,
|
|
4053
|
+
empty: workerRegistryRows.length === 0 && !workersState.loading,
|
|
4054
|
+
rows: workerRegistryRows,
|
|
4055
|
+
counts: {
|
|
4056
|
+
registered: workerRegistryRows.length,
|
|
4057
|
+
live: liveWorkerRows.length,
|
|
4058
|
+
ready: liveWorkerRows.filter((row) => row.phase === "ready").length,
|
|
4059
|
+
starting: liveWorkerRows.filter((row) => row.phase === "starting").length,
|
|
4060
|
+
draining: liveWorkerRows.filter((row) => row.phase === "draining").length,
|
|
4061
|
+
pools: [...new Set(liveWorkerRows.map((row) => row.pool))].length,
|
|
4062
|
+
},
|
|
4063
|
+
summaryText: workerRegistryRows.length
|
|
4064
|
+
? `${liveWorkerRows.length} live / ${workerRegistryRows.length} registered`
|
|
4065
|
+
: null,
|
|
4066
|
+
};
|
|
4067
|
+
|
|
4068
|
+
const packagesView = {
|
|
4069
|
+
loading: Boolean(pkgState.loading),
|
|
4070
|
+
error: pkgState.error || null,
|
|
4071
|
+
empty: pkgList.length === 0 && !pkgState.loading,
|
|
4072
|
+
sharedCount: sharedRows.length,
|
|
4073
|
+
userCount: userRows.length,
|
|
4074
|
+
selectedName: pkgState.selectedName || null,
|
|
4075
|
+
detail: packageDetail,
|
|
4076
|
+
workspace,
|
|
4077
|
+
addDialog: {
|
|
4078
|
+
open: Boolean(pkgState.addDialog?.open),
|
|
4079
|
+
kind: pkgState.addDialog?.kind || "repo",
|
|
4080
|
+
scope: pkgState.addDialog?.scope || "user",
|
|
4081
|
+
repoUrl: pkgState.addDialog?.repoUrl || "",
|
|
4082
|
+
ref: pkgState.addDialog?.ref || "",
|
|
4083
|
+
path: pkgState.addDialog?.path || "",
|
|
4084
|
+
url: pkgState.addDialog?.url || "",
|
|
4085
|
+
authToken: pkgState.addDialog?.authToken || "",
|
|
4086
|
+
submitting: Boolean(pkgState.addDialog?.submitting),
|
|
4087
|
+
progress: pkgState.addDialog?.progress || null,
|
|
4088
|
+
error: pkgState.addDialog?.error || null,
|
|
4089
|
+
},
|
|
4090
|
+
};
|
|
4091
|
+
|
|
3549
4092
|
const actions = [];
|
|
4093
|
+
if (section === "workers") {
|
|
4094
|
+
actions.push({ id: "workersRefresh", label: "Refresh workers", key: "r" });
|
|
4095
|
+
actions.push({ id: "showPackages", label: "Agents", key: "a" });
|
|
4096
|
+
actions.push({ id: "showGhcp", label: "GitHub Keys", key: "g" });
|
|
4097
|
+
actions.push({ id: "close", label: "Close console", key: "Esc" });
|
|
4098
|
+
return {
|
|
4099
|
+
visible: Boolean(admin.visible),
|
|
4100
|
+
loading: Boolean(admin.loading),
|
|
4101
|
+
loadError: admin.loadError || null,
|
|
4102
|
+
principal,
|
|
4103
|
+
isAdmin,
|
|
4104
|
+
section,
|
|
4105
|
+
settingsTree,
|
|
4106
|
+
packages: packagesView,
|
|
4107
|
+
workers: workersView,
|
|
4108
|
+
ghcpKey: {
|
|
4109
|
+
configured: Boolean(profile?.githubCopilotKeySet),
|
|
4110
|
+
targetConfigured,
|
|
4111
|
+
storeAsSystem,
|
|
4112
|
+
editing: false,
|
|
4113
|
+
draft: "",
|
|
4114
|
+
cursorIndex: 0,
|
|
4115
|
+
saving: false,
|
|
4116
|
+
error: null,
|
|
4117
|
+
statusText: ghcpStatusText,
|
|
4118
|
+
},
|
|
4119
|
+
systemGhcpKey: {
|
|
4120
|
+
supported: Boolean(systemGhcpKey.supported),
|
|
4121
|
+
loading: Boolean(systemGhcpKey.loading),
|
|
4122
|
+
configured: Boolean(systemGhcpKey.configured),
|
|
4123
|
+
changedBy: systemGhcpKey.changedBy || null,
|
|
4124
|
+
changedAt: systemGhcpKey.changedAt || null,
|
|
4125
|
+
error: systemGhcpKey.error || null,
|
|
4126
|
+
},
|
|
4127
|
+
actions,
|
|
4128
|
+
};
|
|
4129
|
+
}
|
|
4130
|
+
if (section === "packages") {
|
|
4131
|
+
actions.push({ id: "packagesRefresh", label: "Refresh packages", key: "r" });
|
|
4132
|
+
actions.push({ id: "packagesNext", label: "Next package", key: "j" });
|
|
4133
|
+
actions.push({ id: "packagesPrev", label: "Previous package", key: "k" });
|
|
4134
|
+
actions.push({ id: "showGhcp", label: "GitHub Keys", key: "g" });
|
|
4135
|
+
actions.push({ id: "close", label: "Close console", key: "Esc" });
|
|
4136
|
+
return {
|
|
4137
|
+
visible: Boolean(admin.visible),
|
|
4138
|
+
loading: Boolean(admin.loading),
|
|
4139
|
+
loadError: admin.loadError || null,
|
|
4140
|
+
principal,
|
|
4141
|
+
isAdmin,
|
|
4142
|
+
section,
|
|
4143
|
+
settingsTree,
|
|
4144
|
+
packages: packagesView,
|
|
4145
|
+
workers: workersView,
|
|
4146
|
+
ghcpKey: {
|
|
4147
|
+
configured: Boolean(profile?.githubCopilotKeySet),
|
|
4148
|
+
targetConfigured,
|
|
4149
|
+
storeAsSystem,
|
|
4150
|
+
editing: false,
|
|
4151
|
+
draft: "",
|
|
4152
|
+
cursorIndex: 0,
|
|
4153
|
+
saving: false,
|
|
4154
|
+
error: null,
|
|
4155
|
+
statusText: ghcpStatusText,
|
|
4156
|
+
},
|
|
4157
|
+
systemGhcpKey: {
|
|
4158
|
+
supported: Boolean(systemGhcpKey.supported),
|
|
4159
|
+
loading: Boolean(systemGhcpKey.loading),
|
|
4160
|
+
configured: Boolean(systemGhcpKey.configured),
|
|
4161
|
+
changedBy: systemGhcpKey.changedBy || null,
|
|
4162
|
+
changedAt: systemGhcpKey.changedAt || null,
|
|
4163
|
+
error: systemGhcpKey.error || null,
|
|
4164
|
+
},
|
|
4165
|
+
actions,
|
|
4166
|
+
};
|
|
4167
|
+
}
|
|
3550
4168
|
if (!ghcpKey.editing) {
|
|
3551
4169
|
actions.push({ id: "edit", label: targetConfigured ? `Replace ${keyNoun}` : `Set ${keyNoun}`, key: "e" });
|
|
3552
4170
|
if (targetConfigured) {
|
|
@@ -3559,12 +4177,19 @@ export function selectAdminConsole(state) {
|
|
|
3559
4177
|
}
|
|
3560
4178
|
actions.push({ id: "close", label: "Close console", key: ghcpKey.editing ? "Ctrl+Esc" : "Esc" });
|
|
3561
4179
|
|
|
4180
|
+
if (section === "ghcp") {
|
|
4181
|
+
actions.splice(actions.length - 1, 0, { id: "showPackages", label: "Agents", key: "a" });
|
|
4182
|
+
}
|
|
3562
4183
|
return {
|
|
3563
4184
|
visible: Boolean(admin.visible),
|
|
3564
4185
|
loading: Boolean(admin.loading),
|
|
3565
4186
|
loadError: admin.loadError || null,
|
|
3566
4187
|
principal,
|
|
3567
4188
|
isAdmin,
|
|
4189
|
+
section,
|
|
4190
|
+
settingsTree,
|
|
4191
|
+
packages: packagesView,
|
|
4192
|
+
workers: workersView,
|
|
3568
4193
|
ghcpKey: {
|
|
3569
4194
|
configured: Boolean(profile?.githubCopilotKeySet),
|
|
3570
4195
|
targetConfigured,
|
|
@@ -4611,92 +5236,228 @@ function buildNodeMapCell(session, brandingTitle, width, active) {
|
|
|
4611
5236
|
};
|
|
4612
5237
|
}
|
|
4613
5238
|
|
|
4614
|
-
function
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
if (
|
|
4618
|
-
|
|
4619
|
-
|
|
5239
|
+
function nodeMapAgoText(ms) {
|
|
5240
|
+
if (!Number.isFinite(ms) || ms < 0) return null;
|
|
5241
|
+
if (ms < 60_000) return `${Math.round(ms / 1000)}s ago`;
|
|
5242
|
+
if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m ago`;
|
|
5243
|
+
return `${Math.round(ms / 3_600_000)}h ago`;
|
|
5244
|
+
}
|
|
4620
5245
|
|
|
5246
|
+
function nodeMapUptimeText(seconds) {
|
|
5247
|
+
if (!Number.isFinite(seconds) || seconds < 0) return null;
|
|
5248
|
+
if (seconds < 60) return `${Math.round(seconds)}s`;
|
|
5249
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
|
5250
|
+
const h = Math.floor(seconds / 3600);
|
|
5251
|
+
return h < 48 ? `${h}h ${Math.floor((seconds % 3600) / 60)}m` : `${Math.floor(h / 24)}d ${h % 24}h`;
|
|
5252
|
+
}
|
|
5253
|
+
|
|
5254
|
+
function nodeMapSessionEntry(session, brandingTitle, active) {
|
|
5255
|
+
const label = session?.isSystem
|
|
5256
|
+
? canonicalSystemTitle(session, brandingTitle)
|
|
5257
|
+
: (session?.title || shortSessionId(session?.sessionId));
|
|
5258
|
+
const prefix = session?.isSystem ? "⚙ " : session?.serviceKind ? "⚗ " : `${sessionStatusIcon(session) || "."} `;
|
|
5259
|
+
return {
|
|
5260
|
+
sessionId: session?.sessionId || null,
|
|
5261
|
+
text: `${prefix}${label}`,
|
|
5262
|
+
color: session?.isSystem ? "yellow" : sessionStatusColor(session),
|
|
5263
|
+
bold: Boolean(session?.isSystem),
|
|
5264
|
+
active: Boolean(active),
|
|
5265
|
+
};
|
|
5266
|
+
}
|
|
5267
|
+
|
|
5268
|
+
/**
|
|
5269
|
+
* Node Map view-model — registry-first (docs/proposals/worker-registry.md).
|
|
5270
|
+
*
|
|
5271
|
+
* Nodes lead with the WORKER REGISTRY: every registered worker with phase,
|
|
5272
|
+
* pool, and health specs, unioned with any node seen in recent activity
|
|
5273
|
+
* history (covers deployments/credentials without registry access — the view
|
|
5274
|
+
* degrades to activity-derived nodes rather than going blank). Sessions map
|
|
5275
|
+
* onto nodes exactly the way the legacy grid mapped them: last known
|
|
5276
|
+
* workerNodeId in the recent window. Both hosts render from this one VM.
|
|
5277
|
+
*/
|
|
5278
|
+
export function selectNodeMapView(state) {
|
|
4621
5279
|
const recentWindow = getRecentActivityWindow(state);
|
|
4622
|
-
const
|
|
4623
|
-
const
|
|
4624
|
-
|
|
4625
|
-
|
|
5280
|
+
const brandingTitle = state.branding?.title || "PilotSwarm";
|
|
5281
|
+
const activeSessionId = state.sessions.activeSessionId;
|
|
5282
|
+
const registry = Array.isArray(state.admin?.workers?.list) ? state.admin.workers.list : [];
|
|
5283
|
+
const now = Date.now();
|
|
5284
|
+
const NODE_LIVE_MS = 90_000;
|
|
5285
|
+
|
|
5286
|
+
const byLabel = new Map();
|
|
5287
|
+
for (const worker of registry) {
|
|
5288
|
+
const label = shortNodeLabel(worker?.workerNodeId);
|
|
5289
|
+
if (!label) continue;
|
|
5290
|
+
const at = worker?.updatedAt instanceof Date ? worker.updatedAt.getTime() : new Date(worker?.updatedAt ?? 0).getTime();
|
|
5291
|
+
const ageMs = Number.isFinite(at) ? now - at : Number.NaN;
|
|
5292
|
+
const health = worker?.health || {};
|
|
5293
|
+
byLabel.set(label, {
|
|
5294
|
+
label,
|
|
5295
|
+
workerNodeId: String(worker?.workerNodeId ?? ""),
|
|
5296
|
+
registered: true,
|
|
5297
|
+
live: Number.isFinite(ageMs) && ageMs <= NODE_LIVE_MS,
|
|
5298
|
+
phase: ["starting", "ready", "draining"].includes(worker?.phase) ? worker.phase : "ready",
|
|
5299
|
+
pool: String(worker?.pool ?? "default"),
|
|
5300
|
+
agoText: nodeMapAgoText(ageMs),
|
|
5301
|
+
uptimeText: nodeMapUptimeText(health.uptimeS),
|
|
5302
|
+
rssText: Number.isFinite(health.rssBytes) ? adminPkgSize(health.rssBytes) : null,
|
|
5303
|
+
heapText: Number.isFinite(health.heapUsedBytes) ? adminPkgSize(health.heapUsedBytes) : null,
|
|
5304
|
+
eventLoopText: Number.isFinite(health.eventLoopDelayP99Ms) ? `${health.eventLoopDelayP99Ms}ms` : null,
|
|
5305
|
+
sessions: Number.isFinite(health.activeSessions) ? health.activeSessions : null,
|
|
5306
|
+
sdkVersion: typeof worker?.info?.sdkVersion === "string" ? worker.info.sdkVersion : null,
|
|
5307
|
+
substrate: typeof worker?.info?.runtime?.substrate === "string" ? worker.info.runtime.substrate : null,
|
|
5308
|
+
capabilities: worker?.info?.capabilities && typeof worker.info.capabilities === "object"
|
|
5309
|
+
? Object.entries(worker.info.capabilities).filter(([, on]) => Boolean(on)).map(([cap]) => cap).sort()
|
|
5310
|
+
: [],
|
|
5311
|
+
consumes: Array.isArray(worker?.info?.consumes) ? worker.info.consumes : [],
|
|
5312
|
+
owner: worker?.owner?.subject ? String(worker.owner.subject) : null,
|
|
5313
|
+
pkgEpoch: Number.isFinite(worker?.state?.["agent-packages"]?.epoch) ? worker.state["agent-packages"].epoch : null,
|
|
5314
|
+
pkgInstalled: worker?.state?.["agent-packages"]?.installed && typeof worker.state["agent-packages"].installed === "object"
|
|
5315
|
+
? Object.entries(worker.state["agent-packages"].installed).map(([name, entry]) => ({
|
|
5316
|
+
name,
|
|
5317
|
+
semver: entry?.semver ?? null,
|
|
5318
|
+
status: entry?.status === "error" ? "error" : "ok",
|
|
5319
|
+
error: entry?.error ? String(entry.error) : null,
|
|
5320
|
+
}))
|
|
5321
|
+
: [],
|
|
5322
|
+
pkgLastError: worker?.state?.["agent-packages"]?.lastError ? String(worker.state["agent-packages"].lastError) : null,
|
|
5323
|
+
executing: [],
|
|
5324
|
+
});
|
|
5325
|
+
}
|
|
5326
|
+
for (const label of buildNodeMapNodeUnionForWindow(state, recentWindow)) {
|
|
5327
|
+
if (byLabel.has(label)) continue;
|
|
5328
|
+
byLabel.set(label, {
|
|
5329
|
+
label, workerNodeId: null, registered: false, live: true,
|
|
5330
|
+
phase: null, pool: null, agoText: null, uptimeText: null,
|
|
5331
|
+
rssText: null, heapText: null, eventLoopText: null, sessions: null,
|
|
5332
|
+
sdkVersion: null, substrate: null, capabilities: [], consumes: [],
|
|
5333
|
+
owner: null, pkgEpoch: null, pkgInstalled: [], pkgLastError: null,
|
|
5334
|
+
executing: [],
|
|
5335
|
+
});
|
|
5336
|
+
}
|
|
4626
5337
|
|
|
4627
|
-
|
|
5338
|
+
// Sessions → nodes: last known workerNodeId inside the recent window.
|
|
5339
|
+
let missingHistoryCount = 0;
|
|
5340
|
+
for (const sessionId of buildOrderedSessionIds(state)) {
|
|
4628
5341
|
const session = state.sessions.byId[sessionId];
|
|
4629
5342
|
if (!session) continue;
|
|
4630
5343
|
const history = state.history.bySessionId.get(sessionId);
|
|
4631
5344
|
if (!history?.events) missingHistoryCount += 1;
|
|
4632
5345
|
const nodeLabel = getLastKnownSessionNode(history, recentWindow);
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
nodeSessionMap.set(nodeLabel, []);
|
|
4637
|
-
}
|
|
4638
|
-
nodeSessionMap.get(nodeLabel).push(session);
|
|
4639
|
-
}
|
|
4640
|
-
|
|
4641
|
-
if (knownNodes.length === 0) {
|
|
4642
|
-
return [plainInspectorLine(`No worker activity in the ${recentWindow.label} window.`, "gray")];
|
|
5346
|
+
const node = nodeLabel ? byLabel.get(nodeLabel) : null;
|
|
5347
|
+
if (!node) continue;
|
|
5348
|
+
node.executing.push(nodeMapSessionEntry(session, brandingTitle, sessionId === activeSessionId));
|
|
4643
5349
|
}
|
|
4644
5350
|
|
|
4645
|
-
const
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
const
|
|
4660
|
-
const
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
0,
|
|
4664
|
-
);
|
|
5351
|
+
const nodes = Array.from(byLabel.values()).sort((a, b) => {
|
|
5352
|
+
if (a.registered !== b.registered) return a.registered ? -1 : 1;
|
|
5353
|
+
if (a.live !== b.live) return a.live ? -1 : 1;
|
|
5354
|
+
const poolCmp = String(a.pool ?? "~").localeCompare(String(b.pool ?? "~"));
|
|
5355
|
+
if (poolCmp !== 0) return poolCmp;
|
|
5356
|
+
return a.label.localeCompare(b.label);
|
|
5357
|
+
});
|
|
5358
|
+
nodes.forEach((node, index) => { node.ordinal = index + 1; });
|
|
5359
|
+
|
|
5360
|
+
const registeredNodes = nodes.filter((node) => node.registered);
|
|
5361
|
+
// A node is always selected while the Node Map is up: the remembered
|
|
5362
|
+
// choice when it is still in the fleet, otherwise the first row. The
|
|
5363
|
+
// remembered value is never cleared, so switching away and back restores
|
|
5364
|
+
// it — and a worker that vanishes falls back instead of blanking.
|
|
5365
|
+
const remembered = state.ui?.nodeMapSelectedNode;
|
|
5366
|
+
const selected = (remembered && nodes.some((node) => node.label === remembered))
|
|
5367
|
+
? remembered
|
|
5368
|
+
: (nodes[0]?.label ?? null);
|
|
4665
5369
|
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
5370
|
+
return {
|
|
5371
|
+
windowLabel: recentWindow.label,
|
|
5372
|
+
degraded: registeredNodes.length === 0,
|
|
5373
|
+
registryError: state.admin?.workers?.error || null,
|
|
5374
|
+
registryFetchedAt: state.admin?.workers?.fetchedAt || 0,
|
|
5375
|
+
registryLoading: Boolean(state.admin?.workers?.loading),
|
|
5376
|
+
registryAttempts: state.admin?.workers?.attempts || 0,
|
|
5377
|
+
registryLastAttemptAt: state.admin?.workers?.lastAttemptAt || 0,
|
|
5378
|
+
registryLastSkip: state.admin?.workers?.lastSkip || null,
|
|
5379
|
+
registered: registeredNodes.length,
|
|
5380
|
+
liveCount: registeredNodes.filter((node) => node.live).length,
|
|
5381
|
+
executingTotal: nodes.reduce((sum, node) => sum + node.executing.length, 0),
|
|
5382
|
+
missingHistoryCount,
|
|
5383
|
+
selected,
|
|
5384
|
+
nodes,
|
|
5385
|
+
};
|
|
5386
|
+
}
|
|
4671
5387
|
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
rowRuns.push(buildNodeMapCell(
|
|
4682
|
-
session,
|
|
4683
|
-
state.branding?.title || "PilotSwarm",
|
|
4684
|
-
colWidth,
|
|
4685
|
-
session.sessionId === state.sessions.activeSessionId,
|
|
4686
|
-
));
|
|
4687
|
-
});
|
|
4688
|
-
lines.push(rowRuns);
|
|
5388
|
+
function buildNodeMapLines(state, maxWidth, options = {}) {
|
|
5389
|
+
void options;
|
|
5390
|
+
const view = selectNodeMapView(state);
|
|
5391
|
+
if (view.nodes.length === 0) {
|
|
5392
|
+
return [plainInspectorLine(
|
|
5393
|
+
view.degraded
|
|
5394
|
+
? `No workers registered and no worker activity in the ${view.windowLabel} window.`
|
|
5395
|
+
: `No worker activity in the ${view.windowLabel} window.`,
|
|
5396
|
+
"gray")];
|
|
4689
5397
|
}
|
|
4690
5398
|
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
5399
|
+
const lines = [];
|
|
5400
|
+
lines.push([{
|
|
5401
|
+
text: view.degraded
|
|
5402
|
+
? `Nodes (activity-derived) · window ${view.windowLabel}`
|
|
5403
|
+
: `Nodes · ${view.liveCount} live / ${view.registered} registered · window ${view.windowLabel}`,
|
|
5404
|
+
color: "gray",
|
|
5405
|
+
}]);
|
|
5406
|
+
if (view.degraded) {
|
|
5407
|
+
// Say WHY the registry is absent — three different failures used to
|
|
5408
|
+
// collapse into one silent "(activity-derived)" header.
|
|
5409
|
+
if (view.registryError) {
|
|
5410
|
+
lines.push([{ text: `! worker registry unavailable: ${view.registryError}`, color: "red" }]);
|
|
5411
|
+
} else if (view.registryFetchedAt) {
|
|
5412
|
+
lines.push([{ text: "registry reachable but EMPTY — no worker has heartbeated in the last hour", color: "yellow" }]);
|
|
5413
|
+
} else if (view.registryLoading) {
|
|
5414
|
+
lines.push([{ text: "registry request in flight… (times out red after 10s)", color: "yellow" }]);
|
|
5415
|
+
} else if (view.registryAttempts === 0) {
|
|
5416
|
+
// No attempt has EVER been recorded: the refresh call is not
|
|
5417
|
+
// reaching the controller at all (host wiring), which used to be
|
|
5418
|
+
// indistinguishable from "the fetch failed".
|
|
5419
|
+
lines.push([{ text: "registry refresh has NEVER been attempted in this tab — reload; if it persists the refresh call is not reaching the controller", color: "red" }]);
|
|
5420
|
+
} else {
|
|
5421
|
+
const ago = view.registryLastAttemptAt ? Math.round((Date.now() - view.registryLastAttemptAt) / 1000) : null;
|
|
5422
|
+
lines.push([{
|
|
5423
|
+
text: `registry attempted ${view.registryAttempts}× (last ${ago === null ? "?" : `${ago}s`} ago${view.registryLastSkip ? `, skipped: ${view.registryLastSkip}` : ""}) but no rows and no error — reload if this persists`,
|
|
5424
|
+
color: "yellow",
|
|
5425
|
+
}]);
|
|
5426
|
+
}
|
|
4694
5427
|
}
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
5428
|
+
lines.push(plainInspectorLine("", "gray"));
|
|
5429
|
+
|
|
5430
|
+
for (const node of view.nodes) {
|
|
5431
|
+
const isSelected = node.label === view.selected;
|
|
5432
|
+
const dot = node.live ? "●" : "○";
|
|
5433
|
+
const dotColor = !node.live ? "gray" : node.phase === "draining" ? "red" : node.phase === "starting" ? "yellow" : "green";
|
|
5434
|
+
const runs = [
|
|
5435
|
+
{ text: isSelected ? "› " : " ", color: "green", bold: isSelected, nodeSelect: node.label, nodeSelected: isSelected },
|
|
5436
|
+
{ text: node.ordinal <= 9 ? `${node.ordinal} ` : " ", color: "gray", nodeSelect: node.label },
|
|
5437
|
+
{ text: `${dot} `, color: dotColor, nodeSelect: node.label },
|
|
5438
|
+
{ text: node.label.padEnd(7), color: isSelected ? "white" : node.live ? "white" : "gray", bold: isSelected, nodeSelect: node.label },
|
|
5439
|
+
];
|
|
5440
|
+
if (node.registered) {
|
|
5441
|
+
runs.push({ text: ` ${(node.phase || "").padEnd(8)}`, color: dotColor, nodeSelect: node.label });
|
|
5442
|
+
runs.push({ text: ` ${node.pool}`, color: "gray", nodeSelect: node.label });
|
|
5443
|
+
const specs = [
|
|
5444
|
+
node.executing.length ? `${node.executing.length} sess` : null,
|
|
5445
|
+
node.rssText, node.uptimeText ? `up ${node.uptimeText}` : null, node.agoText,
|
|
5446
|
+
].filter(Boolean).join(" · ");
|
|
5447
|
+
if (specs) runs.push({ text: ` ${specs}`, color: "gray", nodeSelect: node.label });
|
|
5448
|
+
} else {
|
|
5449
|
+
runs.push({ text: " activity only", color: "gray", nodeSelect: node.label });
|
|
5450
|
+
if (node.executing.length) runs.push({ text: ` · ${node.executing.length} sess`, color: "gray", nodeSelect: node.label });
|
|
5451
|
+
}
|
|
5452
|
+
lines.push(trimTrailingRunPad(runs));
|
|
4698
5453
|
}
|
|
4699
5454
|
|
|
5455
|
+
lines.push(plainInspectorLine("", "gray"));
|
|
5456
|
+
lines.push(plainInspectorLine("Click a node (or press 1-9) — its details fill the pane below.", "gray"));
|
|
5457
|
+
|
|
5458
|
+
if (view.missingHistoryCount > 0) {
|
|
5459
|
+
lines.push(plainInspectorLine(`Loading worker history for ${view.missingHistoryCount} session(s)…`, "gray"));
|
|
5460
|
+
}
|
|
4700
5461
|
return lines;
|
|
4701
5462
|
}
|
|
4702
5463
|
|
|
@@ -4804,18 +5565,40 @@ export function selectSessionAgentPickerModal(state, maxWidth = 76) {
|
|
|
4804
5565
|
const items = Array.isArray(modal.items) ? modal.items : [];
|
|
4805
5566
|
const selectedIndex = Math.max(0, Number(modal.selectedIndex) || 0);
|
|
4806
5567
|
const contentWidth = Math.max(24, maxWidth - 4);
|
|
4807
|
-
|
|
5568
|
+
// Items arrive pre-sorted Shared → My agents → Generic (controller);
|
|
5569
|
+
// emit a non-clickable heading row at each group boundary. rowItemIndexes
|
|
5570
|
+
// maps rendered rows back to item indexes (null = heading) — the same
|
|
5571
|
+
// mechanism the model picker uses.
|
|
5572
|
+
const GROUP_HEADINGS = { shared: "Shared", mine: "My agents", generic: "" };
|
|
5573
|
+
// Package-scope vocabulary only makes sense when packages exist: a pure
|
|
5574
|
+
// baked deployment keeps the classic flat list, no headings.
|
|
5575
|
+
const hasPackageItems = items.some((item) => item?.packageName);
|
|
5576
|
+
const rows = [];
|
|
5577
|
+
const rowItemIndexes = [];
|
|
5578
|
+
let lastGroup = null;
|
|
5579
|
+
items.forEach((item, index) => {
|
|
5580
|
+
const group = item?.group || "shared";
|
|
5581
|
+
if (hasPackageItems && group !== lastGroup) {
|
|
5582
|
+
const heading = GROUP_HEADINGS[group] ?? "";
|
|
5583
|
+
rows.push([{ text: heading ? `── ${heading} ──` : "──", color: "gray" }]);
|
|
5584
|
+
rowItemIndexes.push(null);
|
|
5585
|
+
lastGroup = group;
|
|
5586
|
+
}
|
|
4808
5587
|
const isSelected = index === selectedIndex;
|
|
5588
|
+
const suffix = item?.kind === "generic"
|
|
5589
|
+
? " [generic]"
|
|
5590
|
+
: item?.packageName
|
|
5591
|
+
? ` ${item.packageName} ${item.packageSemver || ""}`.trimEnd()
|
|
5592
|
+
: ` (${item?.agentName || item?.id || "agent"})`;
|
|
4809
5593
|
const labelRuns = fitRuns([
|
|
4810
5594
|
{ text: item?.kind === "generic" ? "○ " : "· ", color: "gray" },
|
|
4811
5595
|
{ text: item?.title || item?.agentName || item?.id || "Agent", color: "white", bold: true },
|
|
4812
|
-
|
|
4813
|
-
? [{ text: " [generic]", color: "gray" }]
|
|
4814
|
-
: [{ text: ` (${item?.agentName || item?.id || "agent"})`, color: "gray" }]),
|
|
5596
|
+
{ text: suffix, color: "gray" },
|
|
4815
5597
|
], contentWidth);
|
|
4816
|
-
|
|
5598
|
+
rows.push(isSelected
|
|
4817
5599
|
? buildActiveHighlightLine(labelRuns.map((run) => run.text).join("").padEnd(contentWidth, " "))
|
|
4818
|
-
: labelRuns;
|
|
5600
|
+
: labelRuns);
|
|
5601
|
+
rowItemIndexes.push(index);
|
|
4819
5602
|
});
|
|
4820
5603
|
|
|
4821
5604
|
const selectedItem = items[selectedIndex] || null;
|
|
@@ -4831,6 +5614,9 @@ export function selectSessionAgentPickerModal(state, maxWidth = 76) {
|
|
|
4831
5614
|
...(selectedItem.kind === "generic"
|
|
4832
5615
|
? [[{ text: "Open-ended session", color: "gray" }]]
|
|
4833
5616
|
: [[{ text: selectedItem.agentName || selectedItem.id || "agent", color: "gray" }]]),
|
|
5617
|
+
...(selectedItem.packageName
|
|
5618
|
+
? [[{ text: `Package: ${selectedItem.packageName}@${selectedItem.packageSemver || "?"} · ${selectedItem.packageScope || "shared"}`, color: "gray" }]]
|
|
5619
|
+
: (selectedItem.kind === "agent" ? [[{ text: "Built-in agent", color: "gray" }]] : [])),
|
|
4834
5620
|
...(selectedModel ? [[{ text: `Model: ${selectedModel}`, color: "gray" }]] : []),
|
|
4835
5621
|
...(selectedReasoningEffort ? [[{ text: `Reasoning: ${selectedReasoningEffort}`, color: "gray" }]] : []),
|
|
4836
5622
|
[{ text: "", color: "gray" }],
|
|
@@ -4852,10 +5638,12 @@ export function selectSessionAgentPickerModal(state, maxWidth = 76) {
|
|
|
4852
5638
|
]
|
|
4853
5639
|
: [[{ text: "No agent selected.", color: "gray" }]];
|
|
4854
5640
|
|
|
5641
|
+
const selectedRowIndex = rowItemIndexes.indexOf(selectedIndex);
|
|
4855
5642
|
return {
|
|
4856
5643
|
title: modal.title || "Select agent for new session",
|
|
4857
5644
|
rows,
|
|
4858
|
-
|
|
5645
|
+
rowItemIndexes,
|
|
5646
|
+
selectedRowIndex: selectedRowIndex >= 0 ? selectedRowIndex : 0,
|
|
4859
5647
|
detailsTitle: "Agent Details",
|
|
4860
5648
|
detailsLines,
|
|
4861
5649
|
idealWidth: Math.min(
|