pilotswarm 0.5.37 → 0.5.39

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 (85) hide show
  1. package/mcp/dist/src/tools/agent-packages.d.ts.map +1 -1
  2. package/mcp/dist/src/tools/agent-packages.js +34 -14
  3. package/mcp/dist/src/tools/agent-packages.js.map +1 -1
  4. package/package.json +3 -3
  5. package/tui/src/node-sdk-transport.js +113 -18
  6. package/ui/core/src/controller.js +196 -49
  7. package/ui/core/src/layout.js +10 -0
  8. package/ui/core/src/reducer.js +265 -20
  9. package/ui/core/src/selectors.js +132 -10
  10. package/ui/core/src/state.js +90 -2
  11. package/ui/core/src/themes/index.js +1 -1
  12. package/ui/react/src/web-app.js +1097 -220
  13. package/web/auth/config.js +3 -0
  14. package/web/auth/index.js +8 -1
  15. package/web/auth/normalize/proxy.js +48 -0
  16. package/web/auth/providers/proxy.js +141 -0
  17. package/web/dist/assets/{abnfDiagram-VRR7QNED-DlvPH-8O.js → abnfDiagram-VRR7QNED-Hsly1fgf.js} +1 -1
  18. package/web/dist/assets/{arc-r1liz5e5.js → arc-DiQLL9vM.js} +1 -1
  19. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-Bci5dNH3.js → architectureDiagram-ZJ3FMSHR-B8ry8WNZ.js} +1 -1
  20. package/web/dist/assets/{blockDiagram-677ZJIJ3-Bug32AXp.js → blockDiagram-677ZJIJ3-Z4PcukIo.js} +1 -1
  21. package/web/dist/assets/{c4Diagram-LMCZKHZV-DwxSFqgE.js → c4Diagram-LMCZKHZV-Cahkc0gc.js} +1 -1
  22. package/web/dist/assets/channel-BHLBPE50.js +1 -0
  23. package/web/dist/assets/{chunk-2Q5K7J3B-B_dbI_xg.js → chunk-2Q5K7J3B-CtZuWH4J.js} +1 -1
  24. package/web/dist/assets/{chunk-32BRIVSS-BkF68W-R.js → chunk-32BRIVSS-C0M390-k.js} +1 -1
  25. package/web/dist/assets/{chunk-5VM5RSS4-BSbNIbJu.js → chunk-5VM5RSS4-ClvX9u5j.js} +1 -1
  26. package/web/dist/assets/{chunk-EX3LRPZG-Blv_Iqa2.js → chunk-EX3LRPZG-BVAet5KG.js} +1 -1
  27. package/web/dist/assets/{chunk-JWPE2WC7-Dk_dBFKd.js → chunk-JWPE2WC7-BccmZmrr.js} +1 -1
  28. package/web/dist/assets/{chunk-MOJQB5TN-DqNFBrfw.js → chunk-MOJQB5TN-BN5MKe94.js} +1 -1
  29. package/web/dist/assets/{chunk-RYQCIY6F-Dlv7Svpb.js → chunk-RYQCIY6F-DfWw-ACP.js} +1 -1
  30. package/web/dist/assets/{chunk-V7JOEXUC-DozGikwu.js → chunk-V7JOEXUC-BgUEoLnN.js} +1 -1
  31. package/web/dist/assets/{chunk-VR4S4FIN-CmkqvGT4.js → chunk-VR4S4FIN-k1eKfFNA.js} +1 -1
  32. package/web/dist/assets/{chunk-XXDRQBXY-DyByai5N.js → chunk-XXDRQBXY-DPyV10yR.js} +1 -1
  33. package/web/dist/assets/classDiagram-OUVF2IWQ-rj9D2TSL.js +1 -0
  34. package/web/dist/assets/classDiagram-v2-EOCWNBFH-rj9D2TSL.js +1 -0
  35. package/web/dist/assets/{cose-bilkent-JH36ORCC-Bv-gRVvA.js → cose-bilkent-JH36ORCC-Bt_rpukK.js} +1 -1
  36. package/web/dist/assets/{cynefin-VYW2F7L2-DRnAsyZU.js → cynefin-VYW2F7L2-Z8eMzFMv.js} +1 -1
  37. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-B1fnUtjo.js → cynefinDiagram-TSTJHNR4-BJSNawFi.js} +1 -1
  38. package/web/dist/assets/{dagre-VKFMJZFB-DM6-QENB.js → dagre-VKFMJZFB-D6dNl8jc.js} +1 -1
  39. package/web/dist/assets/{diagram-FQU43EPY-BPxxnLdU.js → diagram-FQU43EPY-Cy1nbGsU.js} +1 -1
  40. package/web/dist/assets/{diagram-G47NLZAW-BgtJT3qF.js → diagram-G47NLZAW-CmrbuA_d.js} +1 -1
  41. package/web/dist/assets/{diagram-NH7WQ7WH-uekeIzsy.js → diagram-NH7WQ7WH-PoLNQE2j.js} +1 -1
  42. package/web/dist/assets/{diagram-OA4YK3LP-BVy3JZWE.js → diagram-OA4YK3LP-BZwNFcSV.js} +1 -1
  43. package/web/dist/assets/{diagram-WEI45ONY-BWdjUr32.js → diagram-WEI45ONY-pKw0b_fN.js} +1 -1
  44. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-DD4fwhNK.js → ebnfDiagram-CCIWWBDH-UNctuQCV.js} +1 -1
  45. package/web/dist/assets/{erDiagram-Q63AITRT-BllD_6Kh.js → erDiagram-Q63AITRT-DxIETMif.js} +1 -1
  46. package/web/dist/assets/{flowDiagram-23GEKE2U-QEYKkAUN.js → flowDiagram-23GEKE2U-C06IdNKV.js} +1 -1
  47. package/web/dist/assets/{ganttDiagram-NO4QXBWP-uZcfKTSW.js → ganttDiagram-NO4QXBWP-fU7aGyP1.js} +1 -1
  48. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-XPdbPaH-.js → gitGraphDiagram-IHSO6WYX-9UKkPJm6.js} +1 -1
  49. package/web/dist/assets/index-B8nGlHwb.js +24 -0
  50. package/web/dist/assets/index-DtL0lbis.css +1 -0
  51. package/web/dist/assets/{infoDiagram-FWYZ7A6U-DXCnQOJs.js → infoDiagram-FWYZ7A6U-C6F06SQJ.js} +1 -1
  52. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-687_4FBy.js → ishikawaDiagram-FXEZZL3T-CB-O9NfO.js} +1 -1
  53. package/web/dist/assets/{journeyDiagram-5HDEW3XC-GXlKuda8.js → journeyDiagram-5HDEW3XC-B-8MOf-H.js} +1 -1
  54. package/web/dist/assets/{kanban-definition-HUTT4EX6-C38-2DvN.js → kanban-definition-HUTT4EX6-DX0XA3hI.js} +1 -1
  55. package/web/dist/assets/{linear-lGPXiBcz.js → linear-D60PrjTW.js} +1 -1
  56. package/web/dist/assets/{mermaid.core-C_neEtab.js → mermaid.core-bonzwapf.js} +4 -4
  57. package/web/dist/assets/{mindmap-definition-LN4V7U3C-BaJ9tpEG.js → mindmap-definition-LN4V7U3C-Y-6Dum33.js} +1 -1
  58. package/web/dist/assets/{papaparse.min-rDwKYbBf.js → papaparse.min-Cas1JfRG.js} +1 -1
  59. package/web/dist/assets/{pegDiagram-2B236MQR-BdXJLYgY.js → pegDiagram-2B236MQR-_paO0yDw.js} +1 -1
  60. package/web/dist/assets/{pieDiagram-ENE6RG2P-DU19t6fM.js → pieDiagram-ENE6RG2P-xT1_Gpn4.js} +1 -1
  61. package/web/dist/assets/pilotswarm-BT6tOknQ.js +96 -0
  62. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-CBkOdPp0.js → quadrantDiagram-ABIIQ3AL-1usdLFOF.js} +1 -1
  63. package/web/dist/assets/{railroadDiagram-RFXS5EU6-D2DMcGDw.js → railroadDiagram-RFXS5EU6-R3lwHOz9.js} +1 -1
  64. package/web/dist/assets/{requirementDiagram-TGXJPOKE-Bx9tF1pr.js → requirementDiagram-TGXJPOKE-BL8J1__Z.js} +1 -1
  65. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-DcLlRfsi.js → sankeyDiagram-HTMAVEWB-Dz6--0pl.js} +1 -1
  66. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-xr6UVRBO.js → sequenceDiagram-DBY2YBRQ-CG2Xpkct.js} +1 -1
  67. package/web/dist/assets/{sizeCapture-X5ZJPWSS-0fUkSXmg.js → sizeCapture-X5ZJPWSS-BPbYfjxj.js} +1 -1
  68. package/web/dist/assets/{stateDiagram-2N3HPSRC-BWgJGGpg.js → stateDiagram-2N3HPSRC-46x_y_aA.js} +1 -1
  69. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-C2tnolSo.js +1 -0
  70. package/web/dist/assets/{swimlanes-5IMT3BWC-CZKya4_i.js → swimlanes-5IMT3BWC-SXdMo4zJ.js} +2 -2
  71. package/web/dist/assets/swimlanesDiagram-G3AALYLV-Dh6Sn__t.js +8 -0
  72. package/web/dist/assets/{timeline-definition-FHXFAJF6-Df8BpeAu.js → timeline-definition-FHXFAJF6-CGgT-b38.js} +1 -1
  73. package/web/dist/assets/{vennDiagram-L72KCM5P-ChX5bjT6.js → vennDiagram-L72KCM5P-BGS3xBa_.js} +1 -1
  74. package/web/dist/assets/{wardleyDiagram-EHGQE667-Dr48BgaJ.js → wardleyDiagram-EHGQE667-m2U3wEZJ.js} +1 -1
  75. package/web/dist/assets/{xychartDiagram-FW5EYKEG-Cia_MFfc.js → xychartDiagram-FW5EYKEG-DI6tQysg.js} +1 -1
  76. package/web/dist/index.html +3 -3
  77. package/web/runtime.js +30 -7
  78. package/web/dist/assets/channel-jVpSYat3.js +0 -1
  79. package/web/dist/assets/classDiagram-OUVF2IWQ-C78zSajE.js +0 -1
  80. package/web/dist/assets/classDiagram-v2-EOCWNBFH-C78zSajE.js +0 -1
  81. package/web/dist/assets/index-BuDSoRIL.css +0 -1
  82. package/web/dist/assets/index-D1itTxvB.js +0 -24
  83. package/web/dist/assets/pilotswarm-BNKGG936.js +0 -96
  84. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DmAC8DdB.js +0 -1
  85. package/web/dist/assets/swimlanesDiagram-G3AALYLV-CVYx20_X.js +0 -8
@@ -1,4 +1,5 @@
1
1
  import { UI_COMMANDS, FOCUS_REGIONS, INSPECTOR_TABS, cycleValue } from "./commands.js";
2
+ import { canvasKey as canvasPrefKey } from "./state.js";
2
3
  import { parseAgentSourceLink } from "./repo-links.js";
3
4
  import { importPackageFilesFromLink, readImportedPackageName } from "./repo-import.js";
4
5
  import {
@@ -50,6 +51,22 @@ import {
50
51
  import { findArtifactEntry } from "./state.js";
51
52
  import { getTheme, listThemes } from "./themes/index.js";
52
53
 
54
+ /**
55
+ * The copy selector for a package LIST row: which same-named copy it is.
56
+ * Scope shadowing means one name can be a shared package AND a user copy at
57
+ * once; the row's scope (+ owner, for admins looking at someone else's user
58
+ * copy) is what disambiguates every read and action on it.
59
+ */
60
+ export function packageRowSelector(row) {
61
+ if (!row) return null;
62
+ return {
63
+ ...(row.scope === "shared" || row.scope === "user" ? { scope: row.scope } : {}),
64
+ ...(row.scope === "user" && row.owner?.provider && row.owner?.subject
65
+ ? { owner: { provider: row.owner.provider, subject: row.owner.subject } }
66
+ : {}),
67
+ };
68
+ }
69
+
53
70
  const ORCHESTRATION_STATS_REFRESH_MS = 20_000;
54
71
  const SESSION_STATS_REFRESH_MS = 20_000;
55
72
  const FLEET_STATS_REFRESH_MS = 30_000;
@@ -2414,6 +2431,17 @@ export class PilotSwarmUiController {
2414
2431
  if (pendingGroupRows) {
2415
2432
  this.dispatch({ type: "sessions/groupsLoaded", groups: pendingGroupRows, sessions });
2416
2433
  }
2434
+ // Canvas summaries ride the list rows (session_canvases join) — seed
2435
+ // the store so rows mark on cold load, before any session is selected
2436
+ // or any live event arrives. Never regresses what live events already
2437
+ // know; see the canvas/seed reducer.
2438
+ const canvasSeed = [];
2439
+ for (const row of sessions) {
2440
+ for (const c of Array.isArray(row?.canvases) ? row.canvases : []) {
2441
+ canvasSeed.push({ sessionId: row.sessionId, slot: c.slot, name: c.name, latestRev: c.latestRev, sizeBytes: c.sizeBytes });
2442
+ }
2443
+ }
2444
+ if (canvasSeed.length) this.dispatch({ type: "canvas/seed", entries: canvasSeed });
2417
2445
  this.dispatch({ type: "sessions/loaded", sessions });
2418
2446
  this.refreshOpenSessionOwnerFilterModal();
2419
2447
  const selected = this.getState().sessions.activeSessionId;
@@ -3050,27 +3078,39 @@ export class PilotSwarmUiController {
3050
3078
  }
3051
3079
  }
3052
3080
 
3053
- /** Select a package: loads its detail and workspace tree. */
3054
- async selectAdminPackage(name) {
3055
- this.dispatch({ type: "admin/packages/select", name });
3081
+ /**
3082
+ * Select a package: loads its detail and workspace tree.
3083
+ *
3084
+ * `selector` = { scope, owner? } from the clicked ROW. One name can be
3085
+ * two packages at once (scope shadowing: a shared copy plus a user
3086
+ * copy), and without the selector every read here resolved "own copy
3087
+ * first" — both rows showed the same history, and actions could land on
3088
+ * the copy the user was not looking at.
3089
+ */
3090
+ async selectAdminPackage(name, selector = null) {
3091
+ this.dispatch({ type: "admin/packages/select", name, selector });
3056
3092
  if (!name) return;
3093
+ // Selection token minted by the reducer just now; every response from
3094
+ // this selection carries it so a slow response for another COPY of
3095
+ // the same name (scope shadowing) can never land on this one.
3096
+ const seq = this.getState().admin?.packages?.selectionSeq ?? 0;
3057
3097
  await Promise.all([
3058
3098
  (async () => {
3059
3099
  try {
3060
- const detail = await this.transport.getAgentPackage(name);
3100
+ const detail = await this.transport.getAgentPackage(name, selector);
3061
3101
  if (!detail) throw new Error(`package "${name}" not found`);
3062
- this.dispatch({ type: "admin/packages/detail/loaded", name, detail });
3102
+ this.dispatch({ type: "admin/packages/detail/loaded", name, seq, detail });
3063
3103
  } catch (error) {
3064
- this.dispatch({ type: "admin/packages/detail/loadFailed", name, error: error?.message || String(error) });
3104
+ this.dispatch({ type: "admin/packages/detail/loadFailed", name, seq, error: error?.message || String(error) });
3065
3105
  }
3066
3106
  })(),
3067
3107
  (async () => {
3068
3108
  try {
3069
- const tree = await this.transport.getAgentPackageTree(name);
3109
+ const tree = await this.transport.getAgentPackageTree(name, null, selector);
3070
3110
  // A slower tree for a PREVIOUS selection must not clobber
3071
3111
  // the current package's workspace (stale-response race).
3072
- if (this.getState().admin?.packages?.selectedName !== name) return;
3073
- this.dispatch({ type: "admin/packages/tree/loaded", name, tree });
3112
+ if ((this.getState().admin?.packages?.selectionSeq ?? 0) !== seq) return;
3113
+ this.dispatch({ type: "admin/packages/tree/loaded", name, seq, tree });
3074
3114
  // The package's own CHANGELOG, when it ships one. Loaded
3075
3115
  // alongside the tree rather than on demand because it is
3076
3116
  // the first thing a reviewer wants: what changed, why, and
@@ -3079,11 +3119,12 @@ export class PilotSwarmUiController {
3079
3119
  if (hasChangelog && typeof this.transport.getAgentPackageFile === "function") {
3080
3120
  void (async () => {
3081
3121
  try {
3082
- const file = await this.transport.getAgentPackageFile(name, null, "CHANGELOG.md");
3083
- if (this.getState().admin?.packages?.selectedName !== name) return;
3122
+ const file = await this.transport.getAgentPackageFile(name, null, "CHANGELOG.md", selector);
3123
+ if ((this.getState().admin?.packages?.selectionSeq ?? 0) !== seq) return;
3084
3124
  this.dispatch({
3085
3125
  type: "admin/packages/changelog/loaded",
3086
3126
  name,
3127
+ seq,
3087
3128
  content: typeof file === "string" ? file : (file?.content ?? file?.text ?? ""),
3088
3129
  });
3089
3130
  } catch {
@@ -3092,13 +3133,13 @@ export class PilotSwarmUiController {
3092
3133
  }
3093
3134
  })();
3094
3135
  } else {
3095
- this.dispatch({ type: "admin/packages/changelog/loaded", name, content: null });
3136
+ this.dispatch({ type: "admin/packages/changelog/loaded", name, seq, content: null });
3096
3137
  }
3097
3138
  // Default preview: plugin.json (always present in a valid package).
3098
3139
  const first = tree?.files?.find((f) => f.path === "plugin.json") ?? tree?.files?.[0];
3099
3140
  if (first) void this.selectAdminPackageFile(first.path);
3100
3141
  } catch (error) {
3101
- this.dispatch({ type: "admin/packages/tree/loadFailed", name, error: error?.message || String(error) });
3142
+ this.dispatch({ type: "admin/packages/tree/loadFailed", name, seq, error: error?.message || String(error) });
3102
3143
  }
3103
3144
  })(),
3104
3145
  ]);
@@ -3109,14 +3150,22 @@ export class PilotSwarmUiController {
3109
3150
  const pkgs = this.getState().admin?.packages;
3110
3151
  if (!pkgs?.list?.length) return;
3111
3152
  // Same shared→user projection the settings tree renders, so j/k walks
3112
- // the list in VISUAL order regardless of transport ordering.
3113
- const names = [
3153
+ // the list in VISUAL order regardless of transport ordering. Rows are
3154
+ // COPIES, not names: a shadowed name appears once per scope.
3155
+ const rows = [
3114
3156
  ...pkgs.list.filter((p) => p.scope === "shared"),
3115
3157
  ...pkgs.list.filter((p) => p.scope !== "shared"),
3116
- ].map((p) => p.name);
3117
- const index = names.indexOf(pkgs.selectedName);
3118
- const next = names[Math.min(names.length - 1, Math.max(0, (index < 0 ? (delta > 0 ? -1 : 0) : index) + delta))];
3119
- if (next && next !== pkgs.selectedName) await this.selectAdminPackage(next);
3158
+ ];
3159
+ const keyOf = (row) => `${row.name}\u0001${row.scope ?? ""}\u0001${row.owner?.subject ?? ""}`;
3160
+ const selectedKey = pkgs.selectedName
3161
+ ? `${pkgs.selectedName}\u0001${pkgs.selectedSelector?.scope ?? ""}\u0001${pkgs.selectedSelector?.owner?.subject ?? ""}`
3162
+ : null;
3163
+ let index = rows.findIndex((row) => keyOf(row) === selectedKey);
3164
+ if (index < 0) index = rows.findIndex((row) => row.name === pkgs.selectedName);
3165
+ const next = rows[Math.min(rows.length - 1, Math.max(0, (index < 0 ? (delta > 0 ? -1 : 0) : index) + delta))];
3166
+ if (next && (next.name !== pkgs.selectedName || keyOf(next) !== selectedKey)) {
3167
+ await this.selectAdminPackage(next.name, packageRowSelector(next));
3168
+ }
3120
3169
  }
3121
3170
 
3122
3171
  toggleAdminPackageDir(dir) {
@@ -3127,12 +3176,17 @@ export class PilotSwarmUiController {
3127
3176
  const pkgs = this.getState().admin?.packages;
3128
3177
  const name = pkgs?.selectedName;
3129
3178
  if (!name || !filePath) return;
3179
+ // Tag the preview with the current selection token so a slow file for
3180
+ // one copy can't render under another copy of the same name (their
3181
+ // file paths are identical — both ship plugin.json).
3182
+ const seq = pkgs?.selectionSeq ?? 0;
3183
+ const selector = pkgs?.selectedSelector ?? null;
3130
3184
  this.dispatch({ type: "admin/packages/file/loading", path: filePath });
3131
3185
  try {
3132
- const file = await this.transport.getAgentPackageFile(name, null, filePath);
3133
- this.dispatch({ type: "admin/packages/file/loaded", file });
3186
+ const file = await this.transport.getAgentPackageFile(name, null, filePath, selector);
3187
+ this.dispatch({ type: "admin/packages/file/loaded", seq, file });
3134
3188
  } catch (error) {
3135
- this.dispatch({ type: "admin/packages/file/loadFailed", path: filePath, error: error?.message || String(error) });
3189
+ this.dispatch({ type: "admin/packages/file/loadFailed", seq, path: filePath, error: error?.message || String(error) });
3136
3190
  }
3137
3191
  }
3138
3192
 
@@ -3145,32 +3199,53 @@ export class PilotSwarmUiController {
3145
3199
  async runAdminPackageAction(kind, arg = null) {
3146
3200
  const pkgs = this.getState().admin?.packages;
3147
3201
  const name = pkgs?.selectedName;
3202
+ const selector = pkgs?.selectedSelector ?? null;
3148
3203
  if (!name) return;
3149
3204
  this.dispatch({ type: "admin/packages/action/pending", action: kind });
3150
3205
  try {
3151
3206
  switch (kind) {
3152
3207
  case "promote":
3153
- await this.transport.setAgentPackageScope(name, "shared");
3208
+ await this.transport.setAgentPackageScope(name, "shared", selector);
3154
3209
  break;
3155
3210
  case "demote":
3156
- await this.transport.setAgentPackageScope(name, "user");
3211
+ await this.transport.setAgentPackageScope(name, "user", selector);
3212
+ break;
3213
+ // Publish the selected copy's version (arg, default active)
3214
+ // into the same-named package in the OTHER scope. THE update
3215
+ // path when the shared name already exists — promote can only
3216
+ // move a row to an unused name.
3217
+ case "republish":
3218
+ await this.transport.republishAgentPackageVersion(
3219
+ name, arg, selector?.scope === "shared" ? "user" : "shared",
3220
+ );
3157
3221
  break;
3158
3222
  case "pin":
3159
- await this.transport.pinAgentPackageVersion(name, arg);
3223
+ await this.transport.pinAgentPackageVersion(name, arg, selector);
3160
3224
  break;
3161
3225
  case "enable":
3162
3226
  case "disable":
3163
- await this.transport.setAgentPackageEnabled(name, kind === "enable");
3227
+ await this.transport.setAgentPackageEnabled(name, kind === "enable", selector);
3164
3228
  break;
3165
3229
  case "delete":
3166
- await this.transport.deleteAgentPackage(name);
3230
+ await this.transport.deleteAgentPackage(name, selector);
3167
3231
  break;
3168
3232
  default:
3169
3233
  throw new Error(`unknown package action: ${kind}`);
3170
3234
  }
3171
3235
  this.dispatch({ type: "admin/packages/action/done" });
3172
3236
  await this.refreshAdminAgentPackages();
3173
- if (kind !== "delete") await this.selectAdminPackage(name);
3237
+ // Re-select the copy that still exists. promote/demote MOVE the row
3238
+ // to the other scope, so the pre-action selector now points at a
3239
+ // copy that is gone — re-selecting it would error the pane with
3240
+ // "package not found". Every other action leaves the copy in place.
3241
+ if (kind !== "delete") {
3242
+ const nextSelector = kind === "promote"
3243
+ ? { scope: "shared", ...(selector?.owner ? { owner: selector.owner } : {}) }
3244
+ : kind === "demote"
3245
+ ? { scope: "user", ...(selector?.owner ? { owner: selector.owner } : {}) }
3246
+ : selector;
3247
+ await this.selectAdminPackage(name, nextSelector);
3248
+ }
3174
3249
  } catch (error) {
3175
3250
  this.dispatch({ type: "admin/packages/action/failed", error: error?.message || String(error) });
3176
3251
  }
@@ -4254,9 +4329,13 @@ export class PilotSwarmUiController {
4254
4329
  applyCanvasUpdate(sessionId, event) {
4255
4330
  const rev = Number(event?.data?.rev);
4256
4331
  if (!sessionId || !Number.isFinite(rev) || rev <= 0) return false;
4332
+ const rawSlot = Number(event?.data?.slot);
4333
+ const slot = Number.isInteger(rawSlot) && rawSlot >= 1 && rawSlot <= 5 ? rawSlot : 1;
4257
4334
  this.dispatch({
4258
4335
  type: "canvas/updated",
4259
4336
  sessionId,
4337
+ slot,
4338
+ ...(typeof event?.data?.name === "string" ? { name: event.data.name } : {}),
4260
4339
  rev,
4261
4340
  note: typeof event?.data?.note === "string" ? event.data.note : "",
4262
4341
  sizeBytes: event?.data?.sizeBytes,
@@ -4265,7 +4344,9 @@ export class PilotSwarmUiController {
4265
4344
 
4266
4345
  const state = this.getState();
4267
4346
  if (sessionId !== state.sessions.activeSessionId) return true;
4268
- if (state.canvas.prefs[sessionId]?.optedOut) return true;
4347
+ // The opt-out is PER SLOT: leaving one canvas by hand must not
4348
+ // silence a different canvas's first draw.
4349
+ if (state.canvas.prefs[canvasPrefKey(sessionId, slot)]?.optedOut) return true;
4269
4350
  const createdAt = event?.createdAt ? Date.parse(event.createdAt) : Number.NaN;
4270
4351
  if (Number.isFinite(createdAt) && Date.now() - createdAt > PRESENTED_ARTIFACT_FRESHNESS_MS) {
4271
4352
  return true;
@@ -4274,7 +4355,7 @@ export class PilotSwarmUiController {
4274
4355
  // when the mode is already "canvas", because a phone can be off its
4275
4356
  // canvas tab while the mode state still says canvas — the tick is
4276
4357
  // what brings the tab back. On desktop the repeat dispatch is a no-op.
4277
- this.dispatch({ type: "canvas/flip", sessionId });
4358
+ this.dispatch({ type: "canvas/flip", sessionId, slot });
4278
4359
  return true;
4279
4360
  }
4280
4361
 
@@ -4337,29 +4418,55 @@ export class PilotSwarmUiController {
4337
4418
  return;
4338
4419
  }
4339
4420
  try {
4421
+ // A 30-event window, not 1: five interleaved slots mean the top
4422
+ // event only describes the most recently drawn slot. Latest per
4423
+ // slot wins; every drawn slot gets its snapshot.
4340
4424
  const rows = await this.transport.getSessionEventsBefore(
4341
- sessionId, Number.MAX_SAFE_INTEGER, 1, ["session.canvas_updated"],
4425
+ sessionId, Number.MAX_SAFE_INTEGER, 30, ["session.canvas_updated"],
4342
4426
  );
4343
- const top = Array.isArray(rows) ? rows[0] : null;
4344
- this.dispatch({
4345
- type: "canvas/snapshot",
4346
- sessionId,
4347
- rev: Number(top?.data?.rev) || 0,
4348
- note: typeof top?.data?.note === "string" ? top.data.note : "",
4349
- sizeBytes: top?.data?.sizeBytes,
4350
- responseContract: top?.data?.responseContract,
4351
- });
4352
- // The latest data tick is the page's cold-load state — replay it
4353
- // so a freshly loaded shell can reconstruct without the agent.
4354
- if ((Number(top?.data?.rev) || 0) > 0) {
4427
+ const bySlot = new Map();
4428
+ for (const row of Array.isArray(rows) ? rows : []) {
4429
+ const slot = Number(row?.data?.slot) || 1;
4430
+ const rev = Number(row?.data?.rev) || 0;
4431
+ if (!bySlot.has(slot) || rev > (Number(bySlot.get(slot)?.data?.rev) || 0)) bySlot.set(slot, row);
4432
+ }
4433
+ // Always at least a slot-1 snapshot (rev 0 marks snapshotLoaded
4434
+ // so this does not respin).
4435
+ if (!bySlot.has(1)) bySlot.set(1, null);
4436
+ let anyDrawn = false;
4437
+ for (const [slot, top] of bySlot) {
4438
+ const rev = Number(top?.data?.rev) || 0;
4439
+ if (rev > 0) anyDrawn = true;
4440
+ this.dispatch({
4441
+ type: "canvas/snapshot",
4442
+ sessionId,
4443
+ slot,
4444
+ rev,
4445
+ ...(typeof top?.data?.name === "string" ? { name: top.data.name } : {}),
4446
+ note: typeof top?.data?.note === "string" ? top.data.note : "",
4447
+ sizeBytes: top?.data?.sizeBytes,
4448
+ responseContract: top?.data?.responseContract,
4449
+ });
4450
+ }
4451
+ // The latest data tick per slot is that page's cold-load state —
4452
+ // replay it so a freshly loaded shell reconstructs without the
4453
+ // agent.
4454
+ if (anyDrawn) {
4355
4455
  const dataRows = await this.transport.getSessionEventsBefore(
4356
- sessionId, Number.MAX_SAFE_INTEGER, 1, ["session.canvas_data"],
4456
+ sessionId, Number.MAX_SAFE_INTEGER, 30, ["session.canvas_data"],
4357
4457
  ).catch(() => []);
4358
- const tick = Array.isArray(dataRows) ? dataRows[0] : null;
4359
- if (tick?.data?.dataRev) {
4458
+ const tickBySlot = new Map();
4459
+ for (const row of Array.isArray(dataRows) ? dataRows : []) {
4460
+ const slot = Number(row?.data?.slot) || 1;
4461
+ const dr = Number(row?.data?.dataRev) || 0;
4462
+ if (!tickBySlot.has(slot) || dr > (Number(tickBySlot.get(slot)?.data?.dataRev) || 0)) tickBySlot.set(slot, row);
4463
+ }
4464
+ for (const [slot, tick] of tickBySlot) {
4465
+ if (!tick?.data?.dataRev) continue;
4360
4466
  this.dispatch({
4361
4467
  type: "canvas/data",
4362
4468
  sessionId,
4469
+ slot,
4363
4470
  dataRev: Number(tick.data.dataRev),
4364
4471
  payload: tick.data.payload,
4365
4472
  note: typeof tick.data.note === "string" ? tick.data.note : "",
@@ -4371,6 +4478,27 @@ export class PilotSwarmUiController {
4371
4478
  }
4372
4479
  }
4373
4480
 
4481
+ /**
4482
+ * An agent asked to PRESENT an already-drawn canvas — no bytes, no rev,
4483
+ * nothing marked unseen. Same guards as a draw's auto-flip: only the
4484
+ * active session, only a fresh event (a reconnect replay must not yank
4485
+ * the view), and the user's manual dismissal of that slot still wins.
4486
+ */
4487
+ applyCanvasPresented(sessionId, event) {
4488
+ const rawSlot = Number(event?.data?.slot);
4489
+ const slot = Number.isInteger(rawSlot) && rawSlot >= 1 && rawSlot <= 5 ? rawSlot : 1;
4490
+ if (!sessionId) return false;
4491
+ const state = this.getState();
4492
+ if (sessionId !== state.sessions.activeSessionId) return false;
4493
+ if (state.canvas.prefs[canvasPrefKey(sessionId, slot)]?.optedOut) return false;
4494
+ const createdAt = event?.createdAt ? Date.parse(event.createdAt) : Number.NaN;
4495
+ if (Number.isFinite(createdAt) && Date.now() - createdAt > PRESENTED_ARTIFACT_FRESHNESS_MS) {
4496
+ return false;
4497
+ }
4498
+ this.dispatch({ type: "canvas/flip", sessionId, slot });
4499
+ return true;
4500
+ }
4501
+
4374
4502
  mergeSessionEvent(sessionId, event) {
4375
4503
  if (!sessionId || !event) return false;
4376
4504
  const state = this.getState();
@@ -4429,10 +4557,14 @@ export class PilotSwarmUiController {
4429
4557
  if (event.eventType === "session.canvas_updated") {
4430
4558
  this.applyCanvasUpdate(sessionId, event);
4431
4559
  }
4560
+ if (event.eventType === "session.canvas_presented") {
4561
+ this.applyCanvasPresented(sessionId, event);
4562
+ }
4432
4563
  if (event.eventType === "session.canvas_data") {
4433
4564
  this.dispatch({
4434
4565
  type: "canvas/data",
4435
4566
  sessionId,
4567
+ slot: Number(event?.data?.slot) || 1,
4436
4568
  dataRev: Number(event?.data?.dataRev),
4437
4569
  payload: event?.data?.payload,
4438
4570
  note: typeof event?.data?.note === "string" ? event.data.note : "",
@@ -7156,7 +7288,14 @@ export class PilotSwarmUiController {
7156
7288
  }
7157
7289
 
7158
7290
  setFocus(focusRegion) {
7159
- this.dispatch({ type: "ui/focus", focusRegion: normalizeFocusRegion(focusRegion, this.getCurrentLayout()) });
7291
+ // Carry the raw request alongside the normalized region: on a phone a
7292
+ // workspace region (sessions/chat) normalizes to inspector, and the
7293
+ // mobile follow-focus effect must not read that as "open diagnostics".
7294
+ this.dispatch({
7295
+ type: "ui/focus",
7296
+ focusRegion: normalizeFocusRegion(focusRegion, this.getCurrentLayout()),
7297
+ requestedFocusRegion: focusRegion,
7298
+ });
7160
7299
  }
7161
7300
 
7162
7301
  focusNext() {
@@ -7449,7 +7588,15 @@ export class PilotSwarmUiController {
7449
7588
 
7450
7589
  applyPaneVisualScrollOffset(pane, offset, options = {}, state = this.getState()) {
7451
7590
  const maxOffset = this.getPaneMaxScrollOffset(pane, state);
7452
- const nextOffset = Math.max(0, Math.min(Number(offset) || 0, maxOffset));
7591
+ // fromViewport: the offset was measured off the real DOM (scrollTop /
7592
+ // row height), which the browser has already bounded. Clamping it
7593
+ // against the CONTROLLER's max — counted in TUI render-metric rows, a
7594
+ // different unit — teleported the pane on the first wheel tick: one
7595
+ // 100px notch re-applied as metric-max × 16px, thousands of pixels
7596
+ // away. The two row spaces must never clamp each other.
7597
+ const nextOffset = options.fromViewport
7598
+ ? Math.max(0, Number(offset) || 0)
7599
+ : Math.max(0, Math.min(Number(offset) || 0, maxOffset));
7453
7600
  if (this.paneUsesStickyBottomFollow(pane, state)) {
7454
7601
  const followBottom = options.followBottom !== undefined
7455
7602
  ? Boolean(options.followBottom)
@@ -7468,7 +7615,7 @@ export class PilotSwarmUiController {
7468
7615
  updatePaneScrollFromViewport(pane, offset, options = {}) {
7469
7616
  const state = this.getState();
7470
7617
  if (this.paneUsesStickyBottomFollow(pane, state)) {
7471
- this.applyPaneVisualScrollOffset(pane, offset, options, state);
7618
+ this.applyPaneVisualScrollOffset(pane, offset, { ...options, fromViewport: true }, state);
7472
7619
  return;
7473
7620
  }
7474
7621
  this.scrollPaneTo(pane, offset);
@@ -57,6 +57,16 @@ export function getMaxSessionPaneHeight(totalHeight, bodyHeight) {
57
57
  return Math.max(MIN_SESSION_PANE_HEIGHT, Math.min(maxByChatMinimum, maxByWindow));
58
58
  }
59
59
 
60
+ export function getDiagnosticsSplitAdjustBounds(columnHeight, dividerHeight) {
61
+ const height = Math.max(0, Number(columnHeight) || 0);
62
+ const divider = clamp(Math.max(0, Number(dividerHeight) || 0), 0, height);
63
+ const minAdjust = -(height / 2);
64
+ return {
65
+ minAdjust,
66
+ maxAdjust: Math.max(minAdjust, (height / 2) - divider),
67
+ };
68
+ }
69
+
60
70
  export function computeLegacyLayout(viewport, paneAdjust = 0, promptRows = 1, sessionPaneAdjust = 0, activityPaneAdjust = 0, fullscreenPane = null) {
61
71
  const safeViewport = normalizeViewport(viewport);
62
72
  const totalWidth = safeViewport.width;