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.
Files changed (135) hide show
  1. package/mcp/dist/bin/pilotswarm-mcp.js +4 -0
  2. package/mcp/dist/bin/pilotswarm-mcp.js.map +1 -1
  3. package/mcp/dist/src/context.d.ts +43 -10
  4. package/mcp/dist/src/context.d.ts.map +1 -1
  5. package/mcp/dist/src/context.js +15 -7
  6. package/mcp/dist/src/context.js.map +1 -1
  7. package/mcp/dist/src/resources/artifacts.d.ts.map +1 -1
  8. package/mcp/dist/src/resources/artifacts.js +4 -3
  9. package/mcp/dist/src/resources/artifacts.js.map +1 -1
  10. package/mcp/dist/src/server.d.ts.map +1 -1
  11. package/mcp/dist/src/server.js +2 -0
  12. package/mcp/dist/src/server.js.map +1 -1
  13. package/mcp/dist/src/tools/agent-packages.d.ts +14 -0
  14. package/mcp/dist/src/tools/agent-packages.d.ts.map +1 -0
  15. package/mcp/dist/src/tools/agent-packages.js +126 -0
  16. package/mcp/dist/src/tools/agent-packages.js.map +1 -0
  17. package/mcp/dist/src/tools/agents.d.ts.map +1 -1
  18. package/mcp/dist/src/tools/agents.js +18 -0
  19. package/mcp/dist/src/tools/agents.js.map +1 -1
  20. package/mcp/dist/src/tools/artifacts.d.ts.map +1 -1
  21. package/mcp/dist/src/tools/artifacts.js +12 -11
  22. package/mcp/dist/src/tools/artifacts.js.map +1 -1
  23. package/mcp/dist/src/tools/capabilities.js +1 -1
  24. package/mcp/dist/src/tools/capabilities.js.map +1 -1
  25. package/mcp/dist/src/tools/observability.js +1 -1
  26. package/mcp/dist/src/tools/observability.js.map +1 -1
  27. package/mcp/dist/src/tools/sessions.js +2 -2
  28. package/mcp/dist/src/tools/sessions.js.map +1 -1
  29. package/mcp/dist/src/tools/system.d.ts.map +1 -1
  30. package/mcp/dist/src/tools/system.js +56 -5
  31. package/mcp/dist/src/tools/system.js.map +1 -1
  32. package/mcp/dist/src/tools/turn-control.js +1 -1
  33. package/mcp/dist/src/tools/turn-control.js.map +1 -1
  34. package/mcp/dist/src/util/command.d.ts +3 -3
  35. package/mcp/dist/src/util/command.d.ts.map +1 -1
  36. package/mcp/dist/src/util/command.js +1 -1
  37. package/mcp/dist/src/util/command.js.map +1 -1
  38. package/package.json +6 -6
  39. package/tui/bin/tui.js +6 -0
  40. package/tui/src/agents-cli.js +442 -0
  41. package/tui/src/app.js +44 -1
  42. package/tui/src/node-sdk-transport.js +386 -5
  43. package/tui/src/plugin-config.js +17 -0
  44. package/ui/core/src/commands.js +7 -0
  45. package/ui/core/src/controller.js +498 -33
  46. package/ui/core/src/formatting.js +60 -16
  47. package/ui/core/src/index.js +2 -0
  48. package/ui/core/src/reducer.js +421 -34
  49. package/ui/core/src/repo-import.js +298 -0
  50. package/ui/core/src/repo-links.js +54 -0
  51. package/ui/core/src/selectors.js +879 -91
  52. package/ui/core/src/session-tree.js +109 -8
  53. package/ui/core/src/state.js +108 -2
  54. package/ui/core/src/themes/helpers.js +4 -1
  55. package/ui/core/src/themes/index.js +4 -10
  56. package/ui/core/src/themes/workspace-dark-rich.js +65 -0
  57. package/ui/core/src/themes/workspace-dark.js +65 -0
  58. package/ui/react/src/components.js +181 -4
  59. package/ui/react/src/web-app.js +1410 -91
  60. package/web/api/router.js +20 -2
  61. package/web/authz.js +4 -10
  62. package/web/dist/assets/{abnfDiagram-VRR7QNED-DLV45wH4.js → abnfDiagram-VRR7QNED-Cimc0f-u.js} +1 -1
  63. package/web/dist/assets/{arc-CE4xAYdE.js → arc-D8-_Dx5J.js} +1 -1
  64. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-DfzR8-9F.js → architectureDiagram-ZJ3FMSHR-lzMAuQHJ.js} +1 -1
  65. package/web/dist/assets/{blockDiagram-677ZJIJ3-C2pZdqvH.js → blockDiagram-677ZJIJ3-Dp2CWjVS.js} +1 -1
  66. package/web/dist/assets/{c4Diagram-LMCZKHZV-CKRspTan.js → c4Diagram-LMCZKHZV-DF93UkOy.js} +1 -1
  67. package/web/dist/assets/channel-OwWeiCox.js +1 -0
  68. package/web/dist/assets/{chunk-2Q5K7J3B-CNjLrfHT.js → chunk-2Q5K7J3B-pbsQRM-F.js} +1 -1
  69. package/web/dist/assets/{chunk-32BRIVSS-Bv5EwyaJ.js → chunk-32BRIVSS-B_f-gG1O.js} +1 -1
  70. package/web/dist/assets/{chunk-5VM5RSS4-NDBesEbJ.js → chunk-5VM5RSS4-gqlpJDmt.js} +1 -1
  71. package/web/dist/assets/{chunk-EX3LRPZG-DHQht9ty.js → chunk-EX3LRPZG-Cms7hPmM.js} +1 -1
  72. package/web/dist/assets/{chunk-JWPE2WC7-r0OQpJRF.js → chunk-JWPE2WC7-Boq5PtPI.js} +1 -1
  73. package/web/dist/assets/{chunk-MOJQB5TN-fcwIDgtl.js → chunk-MOJQB5TN-DVn9i8zt.js} +1 -1
  74. package/web/dist/assets/{chunk-RYQCIY6F-x7XQlDO1.js → chunk-RYQCIY6F-a1vKs3UX.js} +1 -1
  75. package/web/dist/assets/{chunk-V7JOEXUC-U8ZNVy1m.js → chunk-V7JOEXUC-i60p5fHd.js} +1 -1
  76. package/web/dist/assets/{chunk-VR4S4FIN-DLEdPLcN.js → chunk-VR4S4FIN-a8p0o-8B.js} +1 -1
  77. package/web/dist/assets/{chunk-XXDRQBXY-TeyAtB-i.js → chunk-XXDRQBXY-BwP1yBKd.js} +1 -1
  78. package/web/dist/assets/classDiagram-OUVF2IWQ-CDaP7nuC.js +1 -0
  79. package/web/dist/assets/classDiagram-v2-EOCWNBFH-CDaP7nuC.js +1 -0
  80. package/web/dist/assets/{cose-bilkent-JH36ORCC-B3bAl1-Z.js → cose-bilkent-JH36ORCC-HW6kGgze.js} +1 -1
  81. package/web/dist/assets/{cynefin-VYW2F7L2-DexP8nG-.js → cynefin-VYW2F7L2-YUFMn-vE.js} +1 -1
  82. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-Doa2XnSs.js → cynefinDiagram-TSTJHNR4-Brdy17i4.js} +1 -1
  83. package/web/dist/assets/{dagre-VKFMJZFB-DZMBKQcJ.js → dagre-VKFMJZFB-CAiSJJj-.js} +1 -1
  84. package/web/dist/assets/{diagram-FQU43EPY-BkzibmSq.js → diagram-FQU43EPY-B63Cqe0J.js} +1 -1
  85. package/web/dist/assets/{diagram-G47NLZAW-CihyHR3a.js → diagram-G47NLZAW-ByktrDJ2.js} +1 -1
  86. package/web/dist/assets/{diagram-NH7WQ7WH-OMDUUxwW.js → diagram-NH7WQ7WH-BLSfRMoE.js} +1 -1
  87. package/web/dist/assets/{diagram-OA4YK3LP-Lc0imJba.js → diagram-OA4YK3LP-DNVu-zpk.js} +1 -1
  88. package/web/dist/assets/{diagram-WEI45ONY-Dlcy8hPe.js → diagram-WEI45ONY-CNf4Sz-I.js} +1 -1
  89. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-Dh3u17Dg.js → ebnfDiagram-CCIWWBDH-9CT3ergx.js} +1 -1
  90. package/web/dist/assets/{erDiagram-Q63AITRT-C0OhlUC2.js → erDiagram-Q63AITRT-BPJHAMgl.js} +1 -1
  91. package/web/dist/assets/{flowDiagram-23GEKE2U-CADKxT-x.js → flowDiagram-23GEKE2U-BVtpr6Iu.js} +1 -1
  92. package/web/dist/assets/{ganttDiagram-NO4QXBWP-iVVdxWB7.js → ganttDiagram-NO4QXBWP-BTHnv9sV.js} +1 -1
  93. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-BwDUec_a.js → gitGraphDiagram-IHSO6WYX-BfmtqbL5.js} +1 -1
  94. package/web/dist/assets/index-B37T1c2s.css +1 -0
  95. package/web/dist/assets/index-Bmb8qK39.js +24 -0
  96. package/web/dist/assets/{infoDiagram-FWYZ7A6U-CGPUif0s.js → infoDiagram-FWYZ7A6U-kU4i4HRu.js} +1 -1
  97. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-QYnrl-KA.js → ishikawaDiagram-FXEZZL3T-Ch7i-E8O.js} +1 -1
  98. package/web/dist/assets/{journeyDiagram-5HDEW3XC-CeKixxL0.js → journeyDiagram-5HDEW3XC-CPQxp0ps.js} +1 -1
  99. package/web/dist/assets/{kanban-definition-HUTT4EX6-CeYokFBS.js → kanban-definition-HUTT4EX6-BBgMVqoJ.js} +1 -1
  100. package/web/dist/assets/{linear-BbA6_gnz.js → linear-rwksvzXJ.js} +1 -1
  101. package/web/dist/assets/{mermaid.core--xGRoQTa.js → mermaid.core-o7PszRUc.js} +4 -4
  102. package/web/dist/assets/{mindmap-definition-LN4V7U3C-D53u8h0z.js → mindmap-definition-LN4V7U3C-wgOTdzlq.js} +1 -1
  103. package/web/dist/assets/{papaparse.min-BU0o7jXn.js → papaparse.min-BRXtjEI6.js} +1 -1
  104. package/web/dist/assets/{pegDiagram-2B236MQR-B7QSyG-3.js → pegDiagram-2B236MQR-BkhQqqTm.js} +1 -1
  105. package/web/dist/assets/{pieDiagram-ENE6RG2P-XtSGL7GG.js → pieDiagram-ENE6RG2P-CcMnvxUz.js} +1 -1
  106. package/web/dist/assets/pilotswarm-DYXtzuUN.js +99 -0
  107. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-LZoYSbTH.js → quadrantDiagram-ABIIQ3AL-_KD1QeEr.js} +1 -1
  108. package/web/dist/assets/{railroadDiagram-RFXS5EU6-B5FREx2U.js → railroadDiagram-RFXS5EU6-CqI0olIe.js} +1 -1
  109. package/web/dist/assets/{requirementDiagram-TGXJPOKE-1VRFkBQX.js → requirementDiagram-TGXJPOKE-1JxRwusF.js} +1 -1
  110. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-J456gWvu.js → sankeyDiagram-HTMAVEWB-9IpFoKzi.js} +1 -1
  111. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CaCcw6KO.js → sequenceDiagram-DBY2YBRQ-teyObSQ9.js} +1 -1
  112. package/web/dist/assets/{sizeCapture-X5ZJPWSS-DClsP3_O.js → sizeCapture-X5ZJPWSS-DVhU2J_6.js} +1 -1
  113. package/web/dist/assets/{stateDiagram-2N3HPSRC-D6VyrKhd.js → stateDiagram-2N3HPSRC-DHPoWpIQ.js} +1 -1
  114. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DJjZWYLS.js +1 -0
  115. package/web/dist/assets/{swimlanes-5IMT3BWC-q678iAuo.js → swimlanes-5IMT3BWC-BxTtDQoB.js} +2 -2
  116. package/web/dist/assets/swimlanesDiagram-G3AALYLV-CV12vDcK.js +8 -0
  117. package/web/dist/assets/{timeline-definition-FHXFAJF6-WnPK_zhe.js → timeline-definition-FHXFAJF6-D0wXZx-4.js} +1 -1
  118. package/web/dist/assets/{vennDiagram-L72KCM5P-WpumuLld.js → vennDiagram-L72KCM5P-BSFyI3iV.js} +1 -1
  119. package/web/dist/assets/{wardleyDiagram-EHGQE667-CBrYLUm_.js → wardleyDiagram-EHGQE667-BN-S7hh0.js} +1 -1
  120. package/web/dist/assets/{xychartDiagram-FW5EYKEG-DbX1rnDW.js → xychartDiagram-FW5EYKEG-nDNuPs7o.js} +1 -1
  121. package/web/dist/index.html +3 -3
  122. package/web/runtime.js +36 -4
  123. package/ui/core/src/themes/daylight.js +0 -62
  124. package/ui/core/src/themes/light-high-contrast.js +0 -65
  125. package/ui/core/src/themes/paper-ink.js +0 -62
  126. package/ui/core/src/themes/solarized-ops.js +0 -59
  127. package/ui/core/src/themes/tokyo-night.js +0 -56
  128. package/web/dist/assets/channel-D7p-E4El.js +0 -1
  129. package/web/dist/assets/classDiagram-OUVF2IWQ-yFZ-1yMb.js +0 -1
  130. package/web/dist/assets/classDiagram-v2-EOCWNBFH-yFZ-1yMb.js +0 -1
  131. package/web/dist/assets/index-BGWTu1KK.css +0 -1
  132. package/web/dist/assets/index-Djfdr7ts.js +0 -24
  133. package/web/dist/assets/pilotswarm-CtO819QH.js +0 -94
  134. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-D7eKFhHb.js +0 -1
  135. 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
- const groups = await this.transport.listSessionGroups().catch(() => []);
2060
- const groupRows = (Array.isArray(groups) ? groups : []).map(sessionGroupToRow).filter(Boolean);
2061
- if (groupRows.length > 0) {
2062
- sessions = [...groupRows, ...sessions];
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
- .filter((sessionId) => sessionId && !excludedIds.has(sessionId)),
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.setFocus(FOCUS_REGIONS.PROMPT);
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.setFocus(FOCUS_REGIONS.PROMPT);
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
- : (state.sessions.activeSessionId ? [state.sessions.activeSessionId] : []);
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
- this.dispatch({ type: "ui/status", text: "Select a top-level non-system session to move" });
3749
- return null;
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
- await this.moveSessionsToGroup(group.groupId, modal.sessionIds || [], { statusTitle: group.title || title });
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" && mode !== "rich") return;
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
- const items = [];
4025
- if (allowGeneric) {
4026
- items.push({
4027
- id: "__generic__",
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
- items.push({
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: `Lifecycle (${shortSessionIdValue(sessionId)})`,
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 (epoch rebirth) is a single-session, non-system,
4908
- // non-group action; the picker surfaces it above the terminal
4909
- // dispositions when the transport supports it. Service sessions
4910
- // (⚗ machinery, e.g. the distiller itself) are never regenerated.
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;