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
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { UI_COMMANDS, FOCUS_REGIONS, INSPECTOR_TABS, cycleValue } from "./commands.js";
|
|
2
|
+
import { parseAgentSourceLink } from "./repo-links.js";
|
|
3
|
+
import { importPackageFilesFromLink, readImportedPackageName } from "./repo-import.js";
|
|
2
4
|
import {
|
|
3
5
|
appendEventToHistory,
|
|
4
6
|
buildHistoryModel,
|
|
@@ -104,6 +106,16 @@ function sessionGroupIdFromRowId(sessionId) {
|
|
|
104
106
|
return String(sessionId || "").startsWith("group:") ? String(sessionId).slice("group:".length) : null;
|
|
105
107
|
}
|
|
106
108
|
|
|
109
|
+
// A folder's row id ("group:<uuid>") is a CLIENT-SIDE row, not a session. Feed
|
|
110
|
+
// one to a per-session endpoint and the server correctly answers 404 — which
|
|
111
|
+
// the data loops read as "this session was deleted" and evict the row. The
|
|
112
|
+
// folder then vanished a few ms after every refresh re-added it, and its
|
|
113
|
+
// members reflowed under the folder above. Every per-session path must skip
|
|
114
|
+
// these ids.
|
|
115
|
+
function isSessionGroupRowId(sessionId) {
|
|
116
|
+
return sessionGroupIdFromRowId(sessionId) !== null;
|
|
117
|
+
}
|
|
118
|
+
|
|
107
119
|
// Per-row placement skip reasons ('system', 'not_found') folded into a short
|
|
108
120
|
// status-bar suffix, e.g. "2 system, 1 not found".
|
|
109
121
|
function summarizePlacementSkips(rows) {
|
|
@@ -2031,6 +2043,9 @@ export class PilotSwarmUiController {
|
|
|
2031
2043
|
*/
|
|
2032
2044
|
handleSessionGone(sessionId) {
|
|
2033
2045
|
if (!sessionId) return;
|
|
2046
|
+
// A folder row can never be "gone" from the session catalog: it is not
|
|
2047
|
+
// a session. Only sessions/groupsLoaded may remove one.
|
|
2048
|
+
if (isSessionGroupRowId(sessionId)) return;
|
|
2034
2049
|
if (this.activeSessionSubscriptionId === sessionId) {
|
|
2035
2050
|
this.detachActiveSession();
|
|
2036
2051
|
}
|
|
@@ -2050,16 +2065,44 @@ export class PilotSwarmUiController {
|
|
|
2050
2065
|
}
|
|
2051
2066
|
|
|
2052
2067
|
async refreshSessions() {
|
|
2068
|
+
// FIRST, above every early return below (a selected group or a
|
|
2069
|
+
// navigation-intent branch returns before the tail): the worker
|
|
2070
|
+
// registry must refresh on every tick of the loop that provably runs.
|
|
2071
|
+
this.refreshWorkerRegistryIfStale().catch(() => {});
|
|
2072
|
+
// Refreshes OVERLAP. The catalog loop ticks every 4s and a dozen
|
|
2073
|
+
// actions call this directly, while one run awaits several sequential
|
|
2074
|
+
// round-trips (N catalog pages, the folder list, up to two getSession
|
|
2075
|
+
// fetches) — easily longer than the tick. Without an ordering guard a
|
|
2076
|
+
// slow run applies a snapshot taken BEFORE a folder existed (or before
|
|
2077
|
+
// a member moved) on top of a newer one, and since groupsLoaded is
|
|
2078
|
+
// authoritative — "folders it omits are gone" — the folder is deleted,
|
|
2079
|
+
// its members reflow to the top level, and the next tick puts it back.
|
|
2080
|
+
// That is the flicker. Stamp each run and let only the newest apply.
|
|
2081
|
+
const refreshSeq = (this.sessionRefreshSeq = (this.sessionRefreshSeq || 0) + 1);
|
|
2053
2082
|
const preRefreshState = this.getState();
|
|
2054
2083
|
const recoveringConnection = !preRefreshState.connection.connected || Boolean(preRefreshState.connection.error);
|
|
2055
2084
|
const shouldClearRefreshFailureBanner = preRefreshState.ui.statusText === SESSION_REFRESH_FAILED_STATUS;
|
|
2056
2085
|
const previousActive = this.getState().sessions.activeSessionId;
|
|
2057
2086
|
let sessions = (await loadSessionCatalogPageWindow(this.transport)).map(normalizeSessionListRow);
|
|
2087
|
+
// Folders are NOT merged into the session payload any more: they live
|
|
2088
|
+
// in their own state slice, so a session refresh cannot drop them. A
|
|
2089
|
+
// failed fetch is "no news" and simply leaves the slice alone.
|
|
2090
|
+
//
|
|
2091
|
+
// This still dispatches BEFORE sessions/loaded, and must: sessions/loaded
|
|
2092
|
+
// seeds default collapse state and the default selection from the rows
|
|
2093
|
+
// it can see, so with no folder rows in state it collapses nothing and
|
|
2094
|
+
// auto-selects a folder MEMBER — which then holds the folder open
|
|
2095
|
+
// forever to keep the selection visible. What it could not do from
|
|
2096
|
+
// state alone is judge which folders are still claimed, since the store
|
|
2097
|
+
// still holds the PREVIOUS membership at this point; the incoming rows
|
|
2098
|
+
// ride along on the action for exactly that.
|
|
2099
|
+
let pendingGroupRows = null;
|
|
2058
2100
|
if (typeof this.transport.listSessionGroups === "function") {
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2101
|
+
try {
|
|
2102
|
+
const groups = await this.transport.listSessionGroups();
|
|
2103
|
+
pendingGroupRows = (Array.isArray(groups) ? groups : []).map(sessionGroupToRow).filter(Boolean);
|
|
2104
|
+
} catch (error) {
|
|
2105
|
+
console.warn(`[PilotSwarmUi] session-group fetch failed, keeping the known folders: ${error?.message || error}`);
|
|
2063
2106
|
}
|
|
2064
2107
|
}
|
|
2065
2108
|
// A pending deep-link target may be readable but absent from the
|
|
@@ -2095,6 +2138,7 @@ export class PilotSwarmUiController {
|
|
|
2095
2138
|
const active = previousActive;
|
|
2096
2139
|
if (
|
|
2097
2140
|
active
|
|
2141
|
+
&& !isSessionGroupRowId(active)
|
|
2098
2142
|
&& !sessions.some((session) => session?.sessionId === active)
|
|
2099
2143
|
&& typeof this.transport.getSession === "function"
|
|
2100
2144
|
) {
|
|
@@ -2103,6 +2147,11 @@ export class PilotSwarmUiController {
|
|
|
2103
2147
|
sessions = [...sessions, normalizeSessionListRow(activeSession)];
|
|
2104
2148
|
}
|
|
2105
2149
|
}
|
|
2150
|
+
// Everything above is READ-ONLY on the store (bar the navigation-intent
|
|
2151
|
+
// failures, which are keyed to a specific session and stay true). From
|
|
2152
|
+
// here down the run WRITES, so a run that a newer one has overtaken
|
|
2153
|
+
// must stop: its snapshot is older than what the store already holds.
|
|
2154
|
+
if (refreshSeq !== this.sessionRefreshSeq) return;
|
|
2106
2155
|
if (recoveringConnection) {
|
|
2107
2156
|
this.dispatch({
|
|
2108
2157
|
type: "connection/ready",
|
|
@@ -2110,6 +2159,9 @@ export class PilotSwarmUiController {
|
|
|
2110
2159
|
...(shouldClearRefreshFailureBanner ? { statusText: "Connected" } : {}),
|
|
2111
2160
|
});
|
|
2112
2161
|
}
|
|
2162
|
+
if (pendingGroupRows) {
|
|
2163
|
+
this.dispatch({ type: "sessions/groupsLoaded", groups: pendingGroupRows, sessions });
|
|
2164
|
+
}
|
|
2113
2165
|
this.dispatch({ type: "sessions/loaded", sessions });
|
|
2114
2166
|
this.refreshOpenSessionOwnerFilterModal();
|
|
2115
2167
|
const selected = this.getState().sessions.activeSessionId;
|
|
@@ -2164,6 +2216,26 @@ export class PilotSwarmUiController {
|
|
|
2164
2216
|
this.evictStaleSessionState();
|
|
2165
2217
|
}
|
|
2166
2218
|
|
|
2219
|
+
/**
|
|
2220
|
+
* Refresh the worker registry at most every 20s. Never throws.
|
|
2221
|
+
* Records EVERY attempt (including skips and why) so the Node Map can
|
|
2222
|
+
* distinguish "the refresh never ran" from "the fetch failed" — the two
|
|
2223
|
+
* used to look identical in the UI.
|
|
2224
|
+
*/
|
|
2225
|
+
async refreshWorkerRegistryIfStale() {
|
|
2226
|
+
const workers = this.getState().admin?.workers;
|
|
2227
|
+
if (workers?.loading) {
|
|
2228
|
+
this.dispatch({ type: "admin/workers/attempt", skip: "already in flight" });
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
if (workers?.fetchedAt && (Date.now() - workers.fetchedAt) < 20_000) {
|
|
2232
|
+
this.dispatch({ type: "admin/workers/attempt", skip: "fresh" });
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
this.dispatch({ type: "admin/workers/attempt", skip: null });
|
|
2236
|
+
await this.refreshAdminWorkers();
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2167
2239
|
/**
|
|
2168
2240
|
* Evict cached orchestration, executionHistory, and file-preview state for sessions
|
|
2169
2241
|
* that are not the active session and haven't been fetched recently.
|
|
@@ -2246,7 +2318,8 @@ export class PilotSwarmUiController {
|
|
|
2246
2318
|
const sessionIds = [...new Set(
|
|
2247
2319
|
visibleRows
|
|
2248
2320
|
.map((row) => row.sessionId)
|
|
2249
|
-
|
|
2321
|
+
// Folder rows are visible rows but not sessions.
|
|
2322
|
+
.filter((sessionId) => sessionId && !isSessionGroupRowId(sessionId) && !excludedIds.has(sessionId)),
|
|
2250
2323
|
)];
|
|
2251
2324
|
if (sessionIds.length === 0) return;
|
|
2252
2325
|
|
|
@@ -2395,6 +2468,15 @@ export class PilotSwarmUiController {
|
|
|
2395
2468
|
return;
|
|
2396
2469
|
}
|
|
2397
2470
|
if (targetTab === "nodes") {
|
|
2471
|
+
// Registry-first: the node list leads with the worker registry
|
|
2472
|
+
// (specs, phases, health). Refresh it on the same cadence the
|
|
2473
|
+
// history loads ride, throttled to the ~20s heartbeat interval;
|
|
2474
|
+
// non-admin transports fail quietly and the view degrades to
|
|
2475
|
+
// history-derived nodes.
|
|
2476
|
+
// No silent skip here: refreshAdminWorkers itself reports every
|
|
2477
|
+
// outcome (missing method, error, empty) so the pane can never sit
|
|
2478
|
+
// on a pristine "not fetched yet" state with nothing explaining it.
|
|
2479
|
+
void this.refreshWorkerRegistryIfStale().catch(() => {});
|
|
2398
2480
|
// Only fetch history for visible session rows — not the entire catalog.
|
|
2399
2481
|
// With hundreds of sessions, fetching all of them every 4s causes unbounded memory growth.
|
|
2400
2482
|
const state = this.getState();
|
|
@@ -2573,6 +2655,7 @@ export class PilotSwarmUiController {
|
|
|
2573
2655
|
async openAdminConsole() {
|
|
2574
2656
|
this.dispatch({ type: "admin/visibility", visible: true });
|
|
2575
2657
|
await this.refreshAdminProfile().catch(() => {});
|
|
2658
|
+
void this.refreshAdminAgentPackages().catch(() => {});
|
|
2576
2659
|
}
|
|
2577
2660
|
|
|
2578
2661
|
/** Close the Admin Console and return to the standard workspace. */
|
|
@@ -2580,6 +2663,18 @@ export class PilotSwarmUiController {
|
|
|
2580
2663
|
this.dispatch({ type: "admin/visibility", visible: false });
|
|
2581
2664
|
}
|
|
2582
2665
|
|
|
2666
|
+
/**
|
|
2667
|
+
* Land on a freshly created session. Selecting it is not enough: the
|
|
2668
|
+
* Admin Console REPLACES the workspace, so creating from there left the
|
|
2669
|
+
* new session selected behind a pane that does not show it — the create
|
|
2670
|
+
* appeared to do nothing but print a status line. Any workspace-replacing
|
|
2671
|
+
* surface has to be dismissed here, not just the focus moved.
|
|
2672
|
+
*/
|
|
2673
|
+
revealCreatedSession() {
|
|
2674
|
+
if (this.getState().admin?.visible) this.closeAdminConsole();
|
|
2675
|
+
this.setFocus(FOCUS_REGIONS.PROMPT);
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2583
2678
|
/**
|
|
2584
2679
|
* Re-fetch the current user's profile (settings + ghcp key-set
|
|
2585
2680
|
* flag). Safe to call repeatedly; updates state in place.
|
|
@@ -2630,6 +2725,292 @@ export class PilotSwarmUiController {
|
|
|
2630
2725
|
this.dispatch({ type: "admin/ghcpKey/setSystemTarget", value: Boolean(value) });
|
|
2631
2726
|
}
|
|
2632
2727
|
|
|
2728
|
+
// ── Agent packages (Admin → Agents) ──────────────────────────
|
|
2729
|
+
|
|
2730
|
+
setAdminSection(section) {
|
|
2731
|
+
this.dispatch({ type: "admin/section", section });
|
|
2732
|
+
if (section === "packages") {
|
|
2733
|
+
const pkgs = this.getState().admin?.packages;
|
|
2734
|
+
if (!pkgs?.fetchedAt) void this.refreshAdminAgentPackages().catch(() => {});
|
|
2735
|
+
}
|
|
2736
|
+
if (section === "workers") {
|
|
2737
|
+
// Always refetch on entry: liveness is heartbeat recency, so rows
|
|
2738
|
+
// fetched minutes ago render as a dead fleet ("0 live").
|
|
2739
|
+
void this.refreshAdminWorkers().catch(() => {});
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
/** Node Map: select a node (toggles off when re-selected). Scopes Activity. */
|
|
2744
|
+
selectNodeMapNode(label) {
|
|
2745
|
+
this.dispatch({ type: "ui/nodeMapSelect", label: label ? String(label) : null });
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
/** Reload the worker registry (Admin → Workers). Admin-gated server-side. */
|
|
2749
|
+
async refreshAdminWorkers() {
|
|
2750
|
+
if (typeof this.transport.listWorkers !== "function") {
|
|
2751
|
+
this.dispatch({ type: "admin/workers/loadFailed", error: "The worker registry is not available on this deployment." });
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
this.dispatch({ type: "admin/workers/loading" });
|
|
2755
|
+
try {
|
|
2756
|
+
// Bounded on purpose: a request that neither resolves nor rejects
|
|
2757
|
+
// (wedged token refresh, dead socket) left the Node Map in a
|
|
2758
|
+
// permanent "not fetched yet" limbo. Ten seconds is an answer.
|
|
2759
|
+
const list = await Promise.race([
|
|
2760
|
+
Promise.resolve().then(() => this.transport.listWorkers()),
|
|
2761
|
+
new Promise((_, reject) => {
|
|
2762
|
+
const t = setTimeout(() => reject(new Error("request timed out after 10s (connection or auth renewal wedged — try a hard refresh)")), 10_000);
|
|
2763
|
+
if (typeof t?.unref === "function") t.unref();
|
|
2764
|
+
}),
|
|
2765
|
+
]);
|
|
2766
|
+
console.info(`[PilotSwarmUi] worker registry: ${Array.isArray(list) ? list.length : 0} row(s)`);
|
|
2767
|
+
this.dispatch({ type: "admin/workers/loaded", list });
|
|
2768
|
+
} catch (error) {
|
|
2769
|
+
// Loud on purpose: the Node Map silently degrading to
|
|
2770
|
+
// activity-derived nodes hid a real fetch failure in prod.
|
|
2771
|
+
console.warn(`[PilotSwarmUi] worker-registry fetch failed: ${error?.message || error}`);
|
|
2772
|
+
this.dispatch({ type: "admin/workers/loadFailed", error: error?.message || String(error) });
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
/** Reload the package list + sources (+ fleet state when permitted). */
|
|
2777
|
+
async refreshAdminAgentPackages() {
|
|
2778
|
+
if (typeof this.transport.listAgentPackages !== "function") {
|
|
2779
|
+
this.dispatch({ type: "admin/packages/loadFailed", error: "Agent packages are not available on this deployment." });
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
this.dispatch({ type: "admin/packages/loading" });
|
|
2783
|
+
try {
|
|
2784
|
+
const [list, workerState] = await Promise.all([
|
|
2785
|
+
this.transport.listAgentPackages(),
|
|
2786
|
+
typeof this.transport.listAgentWorkerState === "function"
|
|
2787
|
+
? this.transport.listAgentWorkerState().catch(() => [])
|
|
2788
|
+
: [],
|
|
2789
|
+
]);
|
|
2790
|
+
this.dispatch({ type: "admin/packages/loaded", list, workerState });
|
|
2791
|
+
} catch (error) {
|
|
2792
|
+
this.dispatch({ type: "admin/packages/loadFailed", error: error?.message || String(error) });
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
/** Select a package: loads its detail and workspace tree. */
|
|
2797
|
+
async selectAdminPackage(name) {
|
|
2798
|
+
this.dispatch({ type: "admin/packages/select", name });
|
|
2799
|
+
if (!name) return;
|
|
2800
|
+
await Promise.all([
|
|
2801
|
+
(async () => {
|
|
2802
|
+
try {
|
|
2803
|
+
const detail = await this.transport.getAgentPackage(name);
|
|
2804
|
+
if (!detail) throw new Error(`package "${name}" not found`);
|
|
2805
|
+
this.dispatch({ type: "admin/packages/detail/loaded", name, detail });
|
|
2806
|
+
} catch (error) {
|
|
2807
|
+
this.dispatch({ type: "admin/packages/detail/loadFailed", name, error: error?.message || String(error) });
|
|
2808
|
+
}
|
|
2809
|
+
})(),
|
|
2810
|
+
(async () => {
|
|
2811
|
+
try {
|
|
2812
|
+
const tree = await this.transport.getAgentPackageTree(name);
|
|
2813
|
+
// A slower tree for a PREVIOUS selection must not clobber
|
|
2814
|
+
// the current package's workspace (stale-response race).
|
|
2815
|
+
if (this.getState().admin?.packages?.selectedName !== name) return;
|
|
2816
|
+
this.dispatch({ type: "admin/packages/tree/loaded", name, tree });
|
|
2817
|
+
// Default preview: plugin.json (always present in a valid package).
|
|
2818
|
+
const first = tree?.files?.find((f) => f.path === "plugin.json") ?? tree?.files?.[0];
|
|
2819
|
+
if (first) void this.selectAdminPackageFile(first.path);
|
|
2820
|
+
} catch (error) {
|
|
2821
|
+
this.dispatch({ type: "admin/packages/tree/loadFailed", name, error: error?.message || String(error) });
|
|
2822
|
+
}
|
|
2823
|
+
})(),
|
|
2824
|
+
]);
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
/** TUI keyboard selection: move through the settings-tree package rows. */
|
|
2828
|
+
async stepAdminPackageSelection(delta) {
|
|
2829
|
+
const pkgs = this.getState().admin?.packages;
|
|
2830
|
+
if (!pkgs?.list?.length) return;
|
|
2831
|
+
// Same shared→user projection the settings tree renders, so j/k walks
|
|
2832
|
+
// the list in VISUAL order regardless of transport ordering.
|
|
2833
|
+
const names = [
|
|
2834
|
+
...pkgs.list.filter((p) => p.scope === "shared"),
|
|
2835
|
+
...pkgs.list.filter((p) => p.scope !== "shared"),
|
|
2836
|
+
].map((p) => p.name);
|
|
2837
|
+
const index = names.indexOf(pkgs.selectedName);
|
|
2838
|
+
const next = names[Math.min(names.length - 1, Math.max(0, (index < 0 ? (delta > 0 ? -1 : 0) : index) + delta))];
|
|
2839
|
+
if (next && next !== pkgs.selectedName) await this.selectAdminPackage(next);
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
toggleAdminPackageDir(dir) {
|
|
2843
|
+
this.dispatch({ type: "admin/packages/toggleDir", dir });
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
async selectAdminPackageFile(filePath) {
|
|
2847
|
+
const pkgs = this.getState().admin?.packages;
|
|
2848
|
+
const name = pkgs?.selectedName;
|
|
2849
|
+
if (!name || !filePath) return;
|
|
2850
|
+
this.dispatch({ type: "admin/packages/file/loading", path: filePath });
|
|
2851
|
+
try {
|
|
2852
|
+
const file = await this.transport.getAgentPackageFile(name, null, filePath);
|
|
2853
|
+
this.dispatch({ type: "admin/packages/file/loaded", file });
|
|
2854
|
+
} catch (error) {
|
|
2855
|
+
this.dispatch({ type: "admin/packages/file/loadFailed", path: filePath, error: error?.message || String(error) });
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
/**
|
|
2860
|
+
* One entry point for package mutations so pending/error state stays
|
|
2861
|
+
* uniform: kind = promote | demote | pin | enable | disable | delete.
|
|
2862
|
+
* (No "sync": packages are imported client-side and published as
|
|
2863
|
+
* artifacts — re-import from the link to update.)
|
|
2864
|
+
*/
|
|
2865
|
+
async runAdminPackageAction(kind, arg = null) {
|
|
2866
|
+
const pkgs = this.getState().admin?.packages;
|
|
2867
|
+
const name = pkgs?.selectedName;
|
|
2868
|
+
if (!name) return;
|
|
2869
|
+
this.dispatch({ type: "admin/packages/action/pending", action: kind });
|
|
2870
|
+
try {
|
|
2871
|
+
switch (kind) {
|
|
2872
|
+
case "promote":
|
|
2873
|
+
await this.transport.setAgentPackageScope(name, "shared");
|
|
2874
|
+
break;
|
|
2875
|
+
case "demote":
|
|
2876
|
+
await this.transport.setAgentPackageScope(name, "user");
|
|
2877
|
+
break;
|
|
2878
|
+
case "pin":
|
|
2879
|
+
await this.transport.pinAgentPackageVersion(name, arg);
|
|
2880
|
+
break;
|
|
2881
|
+
case "enable":
|
|
2882
|
+
case "disable":
|
|
2883
|
+
await this.transport.setAgentPackageEnabled(name, kind === "enable");
|
|
2884
|
+
break;
|
|
2885
|
+
case "delete":
|
|
2886
|
+
await this.transport.deleteAgentPackage(name);
|
|
2887
|
+
break;
|
|
2888
|
+
default:
|
|
2889
|
+
throw new Error(`unknown package action: ${kind}`);
|
|
2890
|
+
}
|
|
2891
|
+
this.dispatch({ type: "admin/packages/action/done" });
|
|
2892
|
+
await this.refreshAdminAgentPackages();
|
|
2893
|
+
if (kind !== "delete") await this.selectAdminPackage(name);
|
|
2894
|
+
} catch (error) {
|
|
2895
|
+
this.dispatch({ type: "admin/packages/action/failed", error: error?.message || String(error) });
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
/** Surface an add-dialog problem (used by the web layer's read phase). */
|
|
2900
|
+
failAdminAddPackage(message) {
|
|
2901
|
+
this.dispatch({ type: "admin/packages/addDialog/failed", error: message });
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
/** Publish an uploaded folder ([{path, contentBase64}]) as a package. */
|
|
2905
|
+
async submitAdminUploadPackage(files, scope) {
|
|
2906
|
+
const dialog = this.getState().admin?.packages?.addDialog;
|
|
2907
|
+
if (!dialog?.open || dialog.submitting) return;
|
|
2908
|
+
if (typeof this.transport.uploadAgentPackage !== "function") {
|
|
2909
|
+
this.dispatch({ type: "admin/packages/addDialog/failed", error: "Folder upload is not available on this deployment." });
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
this.dispatch({ type: "admin/packages/addDialog/submitting" });
|
|
2913
|
+
try {
|
|
2914
|
+
const outcome = await this.transport.uploadAgentPackage(files, scope === "shared" ? "shared" : "user");
|
|
2915
|
+
this.dispatch({ type: "admin/packages/addDialog/close" });
|
|
2916
|
+
await this.refreshAdminAgentPackages();
|
|
2917
|
+
if (outcome?.name) await this.selectAdminPackage(outcome.name);
|
|
2918
|
+
} catch (error) {
|
|
2919
|
+
const validation = Array.isArray(error?.validation?.errors)
|
|
2920
|
+
? `\n${error.validation.errors.map((e) => `[${e.code}] ${e.message}`).join("\n")}`
|
|
2921
|
+
: "";
|
|
2922
|
+
this.dispatch({ type: "admin/packages/addDialog/failed", error: `${error?.message || error}${validation}` });
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
openAdminAddPackage() {
|
|
2927
|
+
this.dispatch({ type: "admin/packages/addDialog/open" });
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
/**
|
|
2931
|
+
* Publish a new version of an existing package. Same dialog, same import
|
|
2932
|
+
* path — the only difference is that the destination is already chosen, so
|
|
2933
|
+
* the scope is inherited and the manifest name is checked on submit.
|
|
2934
|
+
*/
|
|
2935
|
+
openAdminUpdatePackage(name, scope = "user") {
|
|
2936
|
+
const packageName = String(name || "").trim();
|
|
2937
|
+
if (!packageName) return;
|
|
2938
|
+
this.dispatch({ type: "admin/packages/addDialog/open", updateName: packageName, scope });
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
closeAdminAddPackage() {
|
|
2942
|
+
this.dispatch({ type: "admin/packages/addDialog/close" });
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
setAdminAddPackageField(field, value) {
|
|
2946
|
+
this.dispatch({ type: "admin/packages/addDialog/setField", field, value });
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
/**
|
|
2950
|
+
* Import a package from a pasted repo link — IN THIS BROWSER, as the
|
|
2951
|
+
* signed-in user — and publish it through the standard upload path.
|
|
2952
|
+
* Nothing about the repo is stored server-side: no source row, no token.
|
|
2953
|
+
* A pasted PAT is used for this import only and never leaves the tab.
|
|
2954
|
+
*/
|
|
2955
|
+
async submitAdminAddPackage() {
|
|
2956
|
+
const dialog = this.getState().admin?.packages?.addDialog;
|
|
2957
|
+
if (!dialog?.open || dialog.submitting) return;
|
|
2958
|
+
if (typeof this.transport.uploadAgentPackage !== "function") {
|
|
2959
|
+
this.dispatch({ type: "admin/packages/addDialog/failed", error: "Package upload is not available on this deployment." });
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
const link = String(dialog.repoUrl || "").trim();
|
|
2963
|
+
const parsed = parseAgentSourceLink(link);
|
|
2964
|
+
if (parsed.error) {
|
|
2965
|
+
this.dispatch({ type: "admin/packages/addDialog/failed", error: parsed.error });
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
this.dispatch({ type: "admin/packages/addDialog/submitting" });
|
|
2969
|
+
try {
|
|
2970
|
+
const pat = String(dialog.authToken || "").trim();
|
|
2971
|
+
let token = pat || null;
|
|
2972
|
+
let tokenKind = "pat";
|
|
2973
|
+
if (!token && parsed.kind === "ado") {
|
|
2974
|
+
// No PAT: read the repo with the viewer's own Entra token.
|
|
2975
|
+
this.dispatch({ type: "admin/packages/addDialog/progress", message: "getting an Azure DevOps token for your account…" });
|
|
2976
|
+
token = typeof this.transport.getRepoAccessToken === "function"
|
|
2977
|
+
? await this.transport.getRepoAccessToken("ado")
|
|
2978
|
+
: null;
|
|
2979
|
+
tokenKind = "bearer";
|
|
2980
|
+
}
|
|
2981
|
+
const { files } = await importPackageFilesFromLink(link, {
|
|
2982
|
+
token,
|
|
2983
|
+
tokenKind,
|
|
2984
|
+
onProgress: (message) => this.dispatch({ type: "admin/packages/addDialog/progress", message }),
|
|
2985
|
+
});
|
|
2986
|
+
// Updating names its target up front, and a package's identity is
|
|
2987
|
+
// its manifest name — so a folder that builds a DIFFERENT package
|
|
2988
|
+
// would quietly create a second one instead of updating this. Say
|
|
2989
|
+
// so rather than publishing the surprise.
|
|
2990
|
+
if (dialog.updateName) {
|
|
2991
|
+
const manifestName = readImportedPackageName(files);
|
|
2992
|
+
if (manifestName && manifestName !== dialog.updateName) {
|
|
2993
|
+
this.dispatch({
|
|
2994
|
+
type: "admin/packages/addDialog/failed",
|
|
2995
|
+
error: `That folder builds "${manifestName}", not "${dialog.updateName}". `
|
|
2996
|
+
+ "Use Add package to publish it as its own package.",
|
|
2997
|
+
});
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
this.dispatch({ type: "admin/packages/addDialog/progress", message: `publishing ${files.length} file(s)…` });
|
|
3002
|
+
const outcome = await this.transport.uploadAgentPackage(files, dialog.scope === "shared" ? "shared" : "user");
|
|
3003
|
+
this.dispatch({ type: "admin/packages/addDialog/close" });
|
|
3004
|
+
await this.refreshAdminAgentPackages();
|
|
3005
|
+
if (outcome?.name) await this.selectAdminPackage(outcome.name);
|
|
3006
|
+
} catch (error) {
|
|
3007
|
+
const validation = Array.isArray(error?.validation?.errors)
|
|
3008
|
+
? `\n${error.validation.errors.map((issue) => `[${issue.code}] ${issue.message}`).join("\n")}`
|
|
3009
|
+
: "";
|
|
3010
|
+
this.dispatch({ type: "admin/packages/addDialog/failed", error: `${error?.message || error}${validation}` });
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
|
|
2633
3014
|
beginAdminEditGhcpKey() {
|
|
2634
3015
|
this.dispatch({ type: "admin/ghcpKey/beginEdit", draft: "" });
|
|
2635
3016
|
}
|
|
@@ -3639,6 +4020,7 @@ export class PilotSwarmUiController {
|
|
|
3639
4020
|
|
|
3640
4021
|
scheduleSessionDetailSync(sessionId, delayMs = 250) {
|
|
3641
4022
|
if (typeof this.transport.getSession !== "function" || !sessionId) return;
|
|
4023
|
+
if (isSessionGroupRowId(sessionId)) return;
|
|
3642
4024
|
if (this.activeSessionDetailTimer) clearTimeout(this.activeSessionDetailTimer);
|
|
3643
4025
|
this.activeSessionDetailSessionId = sessionId;
|
|
3644
4026
|
this.activeSessionDetailTimer = setTimeout(() => {
|
|
@@ -3659,6 +4041,7 @@ export class PilotSwarmUiController {
|
|
|
3659
4041
|
|
|
3660
4042
|
async syncSessionDetail(sessionId) {
|
|
3661
4043
|
if (typeof this.transport.getSession !== "function" || !sessionId) return;
|
|
4044
|
+
if (isSessionGroupRowId(sessionId)) return;
|
|
3662
4045
|
let session = null;
|
|
3663
4046
|
try {
|
|
3664
4047
|
session = await this.transport.getSession(sessionId);
|
|
@@ -3685,7 +4068,7 @@ export class PilotSwarmUiController {
|
|
|
3685
4068
|
await this.placeCreatedSessionInGroup(created, requestOptions.groupId ?? null);
|
|
3686
4069
|
await this.refreshSessions();
|
|
3687
4070
|
await this.loadSession(created.sessionId);
|
|
3688
|
-
this.
|
|
4071
|
+
this.revealCreatedSession();
|
|
3689
4072
|
this.dispatch({ type: "ui/status", text: `Created session ${created.sessionId.slice(0, 8)}` });
|
|
3690
4073
|
return created;
|
|
3691
4074
|
} catch (error) {
|
|
@@ -3704,7 +4087,7 @@ export class PilotSwarmUiController {
|
|
|
3704
4087
|
await this.placeCreatedSessionInGroup(created, requestOptions.groupId ?? null);
|
|
3705
4088
|
await this.refreshSessions();
|
|
3706
4089
|
await this.loadSession(created.sessionId);
|
|
3707
|
-
this.
|
|
4090
|
+
this.revealCreatedSession();
|
|
3708
4091
|
this.dispatch({
|
|
3709
4092
|
type: "ui/status",
|
|
3710
4093
|
text: `Created ${formatAgentDisplayTitle(agentName, options.title)} session ${created.sessionId.slice(0, 8)}`,
|
|
@@ -3732,9 +4115,16 @@ export class PilotSwarmUiController {
|
|
|
3732
4115
|
|
|
3733
4116
|
getMovableGroupSessionSelection() {
|
|
3734
4117
|
const state = this.getState();
|
|
4118
|
+
// Deselected means deselected. The active session still drives the
|
|
4119
|
+
// chat and inspector panes after an empty-space click, but it is no
|
|
4120
|
+
// longer a LIST selection — so list actions must not silently act on
|
|
4121
|
+
// it. With nothing selected the folder button offers only "New Group".
|
|
4122
|
+
const activeIds = state.sessions.activeSessionId && !state.sessions.listDeselected
|
|
4123
|
+
? [state.sessions.activeSessionId]
|
|
4124
|
+
: [];
|
|
3735
4125
|
const selectedIds = Array.isArray(state.sessions.selectedIds) && state.sessions.selectedIds.length > 0
|
|
3736
4126
|
? state.sessions.selectedIds
|
|
3737
|
-
:
|
|
4127
|
+
: activeIds;
|
|
3738
4128
|
return selectedIds
|
|
3739
4129
|
.map((id) => state.sessions.byId[id])
|
|
3740
4130
|
.filter((session) => session && !session.isSystem && !session.isGroup && !session.parentSessionId);
|
|
@@ -3745,8 +4135,9 @@ export class PilotSwarmUiController {
|
|
|
3745
4135
|
const eligible = this.getMovableGroupSessionSelection();
|
|
3746
4136
|
|
|
3747
4137
|
if (eligible.length === 0) {
|
|
3748
|
-
|
|
3749
|
-
|
|
4138
|
+
// Nothing selected is a legitimate intent: make an empty folder to
|
|
4139
|
+
// drag sessions into, rather than scolding the user.
|
|
4140
|
+
return this.openCreateEmptyGroupModal();
|
|
3750
4141
|
}
|
|
3751
4142
|
if (typeof this.transport.listSessionGroups !== "function") {
|
|
3752
4143
|
this.dispatch({ type: "ui/status", text: "Session groups are not supported by this transport" });
|
|
@@ -3812,6 +4203,29 @@ export class PilotSwarmUiController {
|
|
|
3812
4203
|
return this.openMoveToGroupModal();
|
|
3813
4204
|
}
|
|
3814
4205
|
|
|
4206
|
+
/** Name-and-create an EMPTY group (no sessions), ready to drag into. */
|
|
4207
|
+
async openCreateEmptyGroupModal() {
|
|
4208
|
+
if (typeof this.transport.createSessionGroup !== "function") {
|
|
4209
|
+
this.dispatch({ type: "ui/status", text: "Session grouping is not supported by this transport" });
|
|
4210
|
+
return null;
|
|
4211
|
+
}
|
|
4212
|
+
const state = this.getState();
|
|
4213
|
+
this.dispatch({
|
|
4214
|
+
type: "ui/modal",
|
|
4215
|
+
modal: {
|
|
4216
|
+
type: "sessionGroupName",
|
|
4217
|
+
title: "New Group",
|
|
4218
|
+
previousFocus: state.ui.focusRegion,
|
|
4219
|
+
sessionIds: [],
|
|
4220
|
+
value: "",
|
|
4221
|
+
cursorIndex: 0,
|
|
4222
|
+
maxLength: 80,
|
|
4223
|
+
},
|
|
4224
|
+
});
|
|
4225
|
+
this.dispatch({ type: "ui/status", text: "Name the new group and press Enter" });
|
|
4226
|
+
return null;
|
|
4227
|
+
}
|
|
4228
|
+
|
|
3815
4229
|
async moveSessionsToGroup(groupId, sessionIds, { statusTitle = null } = {}) {
|
|
3816
4230
|
const ids = Array.from(new Set((Array.isArray(sessionIds) ? sessionIds : []).map((id) => String(id || "").trim()).filter(Boolean)));
|
|
3817
4231
|
if (ids.length === 0) {
|
|
@@ -3958,7 +4372,15 @@ export class PilotSwarmUiController {
|
|
|
3958
4372
|
description: `${(modal.sessionIds || []).length} grouped session${(modal.sessionIds || []).length === 1 ? "" : "s"}`,
|
|
3959
4373
|
sessionIds: modal.sessionIds || [],
|
|
3960
4374
|
});
|
|
3961
|
-
|
|
4375
|
+
const ids = modal.sessionIds || [];
|
|
4376
|
+
if (ids.length === 0) {
|
|
4377
|
+
// An empty folder is the point — refresh so it appears, and
|
|
4378
|
+
// say so instead of reporting "nothing to move".
|
|
4379
|
+
this.dispatch({ type: "ui/status", text: `Created group "${group.title || title}"` });
|
|
4380
|
+
await this.refreshSessions().catch(() => {});
|
|
4381
|
+
} else {
|
|
4382
|
+
await this.moveSessionsToGroup(group.groupId, ids, { statusTitle: group.title || title });
|
|
4383
|
+
}
|
|
3962
4384
|
} catch (error) {
|
|
3963
4385
|
this.dispatch({ type: "ui/status", text: `Move failed: ${error?.message || String(error)}` });
|
|
3964
4386
|
}
|
|
@@ -3995,7 +4417,7 @@ export class PilotSwarmUiController {
|
|
|
3995
4417
|
}
|
|
3996
4418
|
|
|
3997
4419
|
setChatViewMode(mode) {
|
|
3998
|
-
if (mode !== "summary" && mode !== "transcript"
|
|
4420
|
+
if (mode !== "summary" && mode !== "transcript") return;
|
|
3999
4421
|
if (this.getActiveSession()?.isGroup) return;
|
|
4000
4422
|
this.dispatch({ type: "ui/chatViewMode", mode });
|
|
4001
4423
|
}
|
|
@@ -4021,24 +4443,14 @@ export class PilotSwarmUiController {
|
|
|
4021
4443
|
return;
|
|
4022
4444
|
}
|
|
4023
4445
|
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
kind: "generic",
|
|
4029
|
-
title: "Generic Session",
|
|
4030
|
-
description: "Open-ended session with no specialized agent boundary.",
|
|
4031
|
-
tools: [],
|
|
4032
|
-
splash: null,
|
|
4033
|
-
splashMobile: null,
|
|
4034
|
-
initialPrompt: null,
|
|
4035
|
-
});
|
|
4036
|
-
}
|
|
4037
|
-
|
|
4446
|
+
// Display order IS item order (Shared → My agents → Generic) so
|
|
4447
|
+
// keyboard navigation walks the list visually; the selector only
|
|
4448
|
+
// inserts non-clickable group headings between the segments.
|
|
4449
|
+
const agentItems = [];
|
|
4038
4450
|
for (const agent of agents) {
|
|
4039
4451
|
const agentName = String(agent?.name || "").trim();
|
|
4040
4452
|
if (!agentName) continue;
|
|
4041
|
-
|
|
4453
|
+
agentItems.push({
|
|
4042
4454
|
id: agentName,
|
|
4043
4455
|
kind: "agent",
|
|
4044
4456
|
agentName,
|
|
@@ -4048,6 +4460,39 @@ export class PilotSwarmUiController {
|
|
|
4048
4460
|
splash: typeof agent?.splash === "string" && agent.splash.trim() ? agent.splash : null,
|
|
4049
4461
|
splashMobile: typeof agent?.splashMobile === "string" && agent.splashMobile.trim() ? agent.splashMobile : null,
|
|
4050
4462
|
initialPrompt: typeof agent?.initialPrompt === "string" && agent.initialPrompt.trim() ? agent.initialPrompt : null,
|
|
4463
|
+
// Agent-package provenance (docs/proposals/agent-packages.md):
|
|
4464
|
+
// "mine" = my user-scope package agents; everything else
|
|
4465
|
+
// (baked/built-in + shared packages) groups under Shared.
|
|
4466
|
+
//
|
|
4467
|
+
// Ownership is the transport's call — it knows the viewer. An
|
|
4468
|
+
// admin sees OTHER users' user-scoped packages, so scope alone
|
|
4469
|
+
// put their agents under "My agents". A transport that does not
|
|
4470
|
+
// report `mine` is single-user or legacy, where user-scope does
|
|
4471
|
+
// mean mine.
|
|
4472
|
+
group: agent?.source === "package" && agent?.scope === "user" && agent?.mine !== false
|
|
4473
|
+
? "mine"
|
|
4474
|
+
: "shared",
|
|
4475
|
+
packageName: agent?.packageName || null,
|
|
4476
|
+
packageSemver: agent?.packageSemver || null,
|
|
4477
|
+
packageScope: agent?.scope || null,
|
|
4478
|
+
builtin: agent?.source !== "package",
|
|
4479
|
+
});
|
|
4480
|
+
}
|
|
4481
|
+
const items = [
|
|
4482
|
+
...agentItems.filter((item) => item.group === "shared"),
|
|
4483
|
+
...agentItems.filter((item) => item.group === "mine"),
|
|
4484
|
+
];
|
|
4485
|
+
if (allowGeneric) {
|
|
4486
|
+
items.push({
|
|
4487
|
+
id: "__generic__",
|
|
4488
|
+
kind: "generic",
|
|
4489
|
+
group: "generic",
|
|
4490
|
+
title: "Generic Session",
|
|
4491
|
+
description: "Open-ended session with no specialized agent boundary.",
|
|
4492
|
+
tools: [],
|
|
4493
|
+
splash: null,
|
|
4494
|
+
splashMobile: null,
|
|
4495
|
+
initialPrompt: null,
|
|
4051
4496
|
});
|
|
4052
4497
|
}
|
|
4053
4498
|
|
|
@@ -4899,16 +5344,15 @@ export class PilotSwarmUiController {
|
|
|
4899
5344
|
type: "ui/modal",
|
|
4900
5345
|
modal: {
|
|
4901
5346
|
type: "terminatePicker",
|
|
4902
|
-
title: `
|
|
5347
|
+
title: `Terminate (${shortSessionIdValue(sessionId)})`,
|
|
4903
5348
|
sessionId,
|
|
4904
5349
|
previousFocus: state.ui.focusRegion,
|
|
4905
5350
|
sessionTitle: String(session.title || "").trim(),
|
|
4906
5351
|
state: String(session.state || "").trim(),
|
|
4907
|
-
// Regenerate
|
|
4908
|
-
//
|
|
4909
|
-
//
|
|
4910
|
-
|
|
4911
|
-
canRegenerate: typeof this.transport.regenerateSession === "function" && !session.serviceKind,
|
|
5352
|
+
// Regenerate is NOT a terminal disposition — it lives in
|
|
5353
|
+
// Manage session (General) where the rest of the "change this
|
|
5354
|
+
// session" actions are. This picker is purely terminal.
|
|
5355
|
+
canRegenerate: false,
|
|
4912
5356
|
},
|
|
4913
5357
|
});
|
|
4914
5358
|
}
|
|
@@ -7716,6 +8160,27 @@ export class PilotSwarmUiController {
|
|
|
7716
8160
|
case UI_COMMANDS.ADMIN_REFRESH_PROFILE:
|
|
7717
8161
|
await this.refreshAdminProfile();
|
|
7718
8162
|
return;
|
|
8163
|
+
case UI_COMMANDS.ADMIN_SHOW_GHCP:
|
|
8164
|
+
this.setAdminSection("ghcp");
|
|
8165
|
+
return;
|
|
8166
|
+
case UI_COMMANDS.ADMIN_SHOW_PACKAGES:
|
|
8167
|
+
this.setAdminSection("packages");
|
|
8168
|
+
return;
|
|
8169
|
+
case UI_COMMANDS.ADMIN_SHOW_WORKERS:
|
|
8170
|
+
this.setAdminSection("workers");
|
|
8171
|
+
return;
|
|
8172
|
+
case UI_COMMANDS.ADMIN_WORKERS_REFRESH:
|
|
8173
|
+
await this.refreshAdminWorkers();
|
|
8174
|
+
return;
|
|
8175
|
+
case UI_COMMANDS.ADMIN_PACKAGES_REFRESH:
|
|
8176
|
+
await this.refreshAdminAgentPackages();
|
|
8177
|
+
return;
|
|
8178
|
+
case UI_COMMANDS.ADMIN_PACKAGES_NEXT:
|
|
8179
|
+
await this.stepAdminPackageSelection(1);
|
|
8180
|
+
return;
|
|
8181
|
+
case UI_COMMANDS.ADMIN_PACKAGES_PREV:
|
|
8182
|
+
await this.stepAdminPackageSelection(-1);
|
|
8183
|
+
return;
|
|
7719
8184
|
case UI_COMMANDS.ADMIN_BEGIN_EDIT_GHCP_KEY:
|
|
7720
8185
|
this.beginAdminEditGhcpKey();
|
|
7721
8186
|
return;
|