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
@@ -9,13 +9,30 @@ import {
9
9
  normalizeSessionOwnerFilter,
10
10
  normalizeStoredActiveSessionId,
11
11
  normalizeStoredCanvasPrefs,
12
+ canvasKey,
13
+ parseCanvasKey,
12
14
  normalizeStoredCollapsedSessionIds,
15
+ normalizeStoredDesktopPanes,
13
16
  normalizeStoredLayoutAdjustments,
14
17
  normalizeStoredPinnedSessionIds,
15
18
  normalizeStoredSessionOrder,
16
19
  createInitialState,
17
20
  } from "./state.js";
18
21
 
22
+ /**
23
+ * Is this package detail/tree/changelog response for a selection the user has
24
+ * already left? Responses carry the selectionSeq captured when their fetch
25
+ * started; the NAME alone cannot tell two copies of a shadowed name apart
26
+ * (scope shadowing: one name, two packages), so a name-only guard let a slow
27
+ * response for one copy land on top of the other. Name-only actions (older
28
+ * hosts) keep the historical name compare.
29
+ */
30
+ function packageResponseIsStale(packages, action) {
31
+ if (packages.selectedName !== action.name) return true;
32
+ if (typeof action.seq === "number") return (packages.selectionSeq ?? 0) !== action.seq;
33
+ return false;
34
+ }
35
+
19
36
  function cloneHistoryMap(historyMap) {
20
37
  return new Map(historyMap);
21
38
  }
@@ -112,10 +129,15 @@ function mergeCanvasPrefs(local, stored, byId) {
112
129
  optedOut: storedEntry ? storedEntry.optedOut : Boolean(localEntry?.optedOut),
113
130
  lastViewedRev: Math.max(storedEntry?.lastViewedRev || 0, localEntry?.lastViewedRev || 0),
114
131
  lastViewedDataRev: Math.max(storedEntry?.lastViewedDataRev || 0, localEntry?.lastViewedDataRev || 0),
132
+ // Latest write wins is unknowable here; the local value is the one
133
+ // this window just dragged, so it outranks the poll.
134
+ ...((localEntry?.zenRailPx || storedEntry?.zenRailPx)
135
+ ? { zenRailPx: localEntry?.zenRailPx || storedEntry?.zenRailPx }
136
+ : {}),
115
137
  };
116
- if (!merged.optedOut && merged.lastViewedRev === 0 && merged.lastViewedDataRev === 0) continue;
117
- const session = byId?.[sessionId];
118
- if (session && (session.parentSessionId || session.isGroup)) continue;
138
+ if (!merged.optedOut && merged.lastViewedRev === 0 && merged.lastViewedDataRev === 0 && !merged.zenRailPx) continue;
139
+ const session = byId?.[parseCanvasKey(sessionId).sessionId];
140
+ if (session && session.isGroup) continue;
119
141
  out[sessionId] = merged;
120
142
  }
121
143
  return out;
@@ -611,6 +633,7 @@ function applyVisibleSessionSelection(state, nextSessions) {
611
633
  sessions = {
612
634
  ...sessions,
613
635
  activeSessionId: intentTargetId,
636
+ listDeselected: false,
614
637
  navigationIntent: intent.status === "resolved"
615
638
  ? intent
616
639
  : { sessionId: intent.sessionId, status: "resolved" },
@@ -808,6 +831,17 @@ export function appReducer(state, action) {
808
831
  && typeof settings.touchScale === "boolean";
809
832
  const hasRightPaneMode = Object.prototype.hasOwnProperty.call(settings, "rightPaneMode")
810
833
  && (settings.rightPaneMode === "canvas" || settings.rightPaneMode === "panes");
834
+ // The two independent columns. A profile written by a build that
835
+ // predates them carries only rightPaneMode, so fall back to
836
+ // migrating that — see normalizeStoredDesktopPanes.
837
+ const hasDesktopPanes = Object.prototype.hasOwnProperty.call(settings, "desktopPanes")
838
+ && settings.desktopPanes && typeof settings.desktopPanes === "object";
839
+ const nextDesktopPanes = (hasDesktopPanes || hasRightPaneMode)
840
+ ? normalizeStoredDesktopPanes(
841
+ hasDesktopPanes ? settings.desktopPanes : null,
842
+ hasRightPaneMode ? settings.rightPaneMode : null,
843
+ )
844
+ : null;
811
845
  const hasCanvasPrefs = Object.prototype.hasOwnProperty.call(settings, "canvasPrefs");
812
846
  const hasPins = Object.prototype.hasOwnProperty.call(settings, "pinnedSessionIds");
813
847
  const hasCollapsed = Object.prototype.hasOwnProperty.call(settings, "collapsedSessionIds");
@@ -872,6 +906,9 @@ export function appReducer(state, action) {
872
906
  themeId: hasTheme ? settings.themeId.trim() : selection.ui.themeId,
873
907
  touchScale: hasTouchScale ? settings.touchScale : selection.ui.touchScale,
874
908
  rightPaneMode: hasRightPaneMode ? settings.rightPaneMode : selection.ui.rightPaneMode,
909
+ ...(nextDesktopPanes
910
+ ? { canvasOpen: nextDesktopPanes.canvasOpen, diagnosticsOpen: nextDesktopPanes.diagnosticsOpen, canvasZen: nextDesktopPanes.zen === true }
911
+ : {}),
875
912
  layout: nextLayout,
876
913
  },
877
914
  canvas: hasCanvasPrefs
@@ -946,6 +983,46 @@ export function appReducer(state, action) {
946
983
  },
947
984
  };
948
985
 
986
+ // Where the canvas/diagnostics seam sits when both columns are open.
987
+ // A pixel delta from an even split of the right side, like the other
988
+ // adjusts. Closing a column is a separate action — see
989
+ // CanvasDiagnosticsResizeHandle, which decides that on release.
990
+ case "ui/canvasPaneAdjust":
991
+ return {
992
+ ...state,
993
+ ui: {
994
+ ...state.ui,
995
+ layout: {
996
+ ...(state.ui.layout || {}),
997
+ canvasPaneAdjust: Number(action.canvasPaneAdjust) || 0,
998
+ },
999
+ },
1000
+ };
1001
+
1002
+ case "ui/diagnosticsSplitAdjust":
1003
+ return {
1004
+ ...state,
1005
+ ui: {
1006
+ ...state.ui,
1007
+ layout: {
1008
+ ...(state.ui.layout || {}),
1009
+ diagnosticsSplitAdjust: Number(action.diagnosticsSplitAdjust) || 0,
1010
+ },
1011
+ },
1012
+ };
1013
+
1014
+ case "ui/diagnosticsPaneAdjust":
1015
+ return {
1016
+ ...state,
1017
+ ui: {
1018
+ ...state.ui,
1019
+ layout: {
1020
+ ...(state.ui.layout || {}),
1021
+ diagnosticsPaneAdjust: Number(action.diagnosticsPaneAdjust) || 0,
1022
+ },
1023
+ },
1024
+ };
1025
+
949
1026
  case "ui/activityPaneAdjust":
950
1027
  return {
951
1028
  ...state,
@@ -964,6 +1041,15 @@ export function appReducer(state, action) {
964
1041
  ui: {
965
1042
  ...state.ui,
966
1043
  focusRegion: action.focusRegion,
1044
+ // The RAW region the caller asked for, before layout
1045
+ // normalization. A workspace region (sessions/chat) with no
1046
+ // slot in the current layout normalizes to the first
1047
+ // focusable pane — inspector, on a phone — so the normalized
1048
+ // value alone cannot tell "focus the inspector" from
1049
+ // "sessions got rewritten to inspector". The mobile
1050
+ // follow-focus pane switch reads this to avoid opening
1051
+ // diagnostics when the user only asked for the workspace.
1052
+ requestedFocusRegion: action.requestedFocusRegion ?? action.focusRegion,
967
1053
  },
968
1054
  };
969
1055
 
@@ -1025,6 +1111,10 @@ export function appReducer(state, action) {
1025
1111
  if (!sessionId) return state;
1026
1112
  const nextSessions = {
1027
1113
  ...state.sessions,
1114
+ // A deep link is explicit navigation. Keep the list highlight
1115
+ // aligned with the linked chat even if empty-space click had
1116
+ // previously deselected the session row.
1117
+ listDeselected: false,
1028
1118
  navigationIntent: { sessionId, status: "pending" },
1029
1119
  filterExceptionId: null,
1030
1120
  };
@@ -2257,6 +2347,7 @@ export function appReducer(state, action) {
2257
2347
  workerState: Array.isArray(action.workerState) ? action.workerState : [],
2258
2348
  fetchedAt: Date.now(),
2259
2349
  selectedName: stillThere ? previous.selectedName : null,
2350
+ selectedSelector: stillThere ? (previous.selectedSelector ?? null) : null,
2260
2351
  ...(stillThere ? {} : { detail: null, workspace: { ...previous.workspace, tree: null, selectedPath: null, file: null } }),
2261
2352
  },
2262
2353
  },
@@ -2274,6 +2365,18 @@ export function appReducer(state, action) {
2274
2365
  packages: {
2275
2366
  ...state.admin.packages,
2276
2367
  selectedName: action.name || null,
2368
+ // WHICH copy of the name: {scope, owner} from the row
2369
+ // the user clicked. One name can be two packages
2370
+ // (scope shadowing) — without this every read and
2371
+ // action fell back to "own copy first" and could
2372
+ // target the row the user was NOT looking at.
2373
+ selectedSelector: action.selector || null,
2374
+ // Monotonic selection token. The stale-response guards
2375
+ // below compare it, because comparing the NAME alone
2376
+ // cannot tell two copies of a shadowed name apart — a
2377
+ // slow response for the user copy could land on top of
2378
+ // the shared copy the user just clicked.
2379
+ selectionSeq: (state.admin.packages.selectionSeq ?? 0) + 1,
2277
2380
  detail: null,
2278
2381
  detailLoading: Boolean(action.name),
2279
2382
  detailError: null,
@@ -2292,17 +2395,17 @@ export function appReducer(state, action) {
2292
2395
  };
2293
2396
  }
2294
2397
  case "admin/packages/detail/loaded": {
2295
- if (state.admin.packages.selectedName !== action.name) return state;
2398
+ if (packageResponseIsStale(state.admin.packages, action)) return state;
2296
2399
  return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, detail: action.detail, detailLoading: false, detailError: null } } };
2297
2400
  }
2298
2401
  case "admin/packages/detail/loadFailed": {
2299
- if (state.admin.packages.selectedName !== action.name) return state;
2402
+ if (packageResponseIsStale(state.admin.packages, action)) return state;
2300
2403
  return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, detail: null, detailLoading: false, detailError: action.error || "Failed to load package" } } };
2301
2404
  }
2302
2405
  case "admin/packages/changelog/loaded": {
2303
2406
  // Stale-response guard, same as the tree: a slow changelog for a
2304
2407
  // previously-selected package must not surface under the current one.
2305
- if (state.admin.packages.selectedName !== action.name) return state;
2408
+ if (packageResponseIsStale(state.admin.packages, action)) return state;
2306
2409
  return {
2307
2410
  ...state,
2308
2411
  admin: {
@@ -2315,7 +2418,7 @@ export function appReducer(state, action) {
2315
2418
  };
2316
2419
  }
2317
2420
  case "admin/packages/tree/loaded": {
2318
- if (state.admin.packages.selectedName !== action.name) return state;
2421
+ if (packageResponseIsStale(state.admin.packages, action)) return state;
2319
2422
  const topDirs = Array.isArray(action.tree?.dirs)
2320
2423
  ? action.tree.dirs.filter((dir) => !dir.includes("/"))
2321
2424
  : [];
@@ -2338,7 +2441,7 @@ export function appReducer(state, action) {
2338
2441
  };
2339
2442
  }
2340
2443
  case "admin/packages/tree/loadFailed": {
2341
- if (state.admin.packages.selectedName !== action.name) return state;
2444
+ if (packageResponseIsStale(state.admin.packages, action)) return state;
2342
2445
  return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, workspace: { ...state.admin.packages.workspace, treeLoading: false, treeError: action.error || "Failed to load package files" } } } };
2343
2446
  }
2344
2447
  case "admin/packages/toggleDir": {
@@ -2352,11 +2455,17 @@ export function appReducer(state, action) {
2352
2455
  return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, workspace: { ...state.admin.packages.workspace, selectedPath: action.path, file: null, fileLoading: true, fileError: null } } } };
2353
2456
  }
2354
2457
  case "admin/packages/file/loaded": {
2458
+ // Path AND selection token: two copies of a shadowed name have the
2459
+ // SAME file paths (both ship plugin.json), so a slow preview for
2460
+ // the previously-selected copy would otherwise land under the
2461
+ // current one. The seq check tells the two copies apart.
2355
2462
  if (state.admin.packages.workspace.selectedPath !== action.file?.path) return state;
2463
+ if (typeof action.seq === "number" && (state.admin.packages.selectionSeq ?? 0) !== action.seq) return state;
2356
2464
  return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, workspace: { ...state.admin.packages.workspace, file: action.file, fileLoading: false, fileError: null } } } };
2357
2465
  }
2358
2466
  case "admin/packages/file/loadFailed": {
2359
2467
  if (state.admin.packages.workspace.selectedPath !== action.path) return state;
2468
+ if (typeof action.seq === "number" && (state.admin.packages.selectionSeq ?? 0) !== action.seq) return state;
2360
2469
  return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, workspace: { ...state.admin.packages.workspace, fileLoading: false, fileError: action.error || "Failed to load file" } } } };
2361
2470
  }
2362
2471
  case "admin/packages/action/pending": {
@@ -2638,24 +2747,114 @@ export function appReducer(state, action) {
2638
2747
  }
2639
2748
  return {
2640
2749
  ...state,
2641
- ui: { ...state.ui, rightPaneMode: mode },
2750
+ ui: { ...state.ui, rightPaneMode: mode, canvasOpen: mode === "canvas" },
2751
+ canvas: { ...state.canvas, prefs },
2752
+ };
2753
+ }
2754
+
2755
+ // Canvas as a column of its own. Same opt-out bookkeeping as the old
2756
+ // mode switch above — closing by hand still says "do not flip me back",
2757
+ // opening by hand still withdraws that — but closing the canvas no
2758
+ // longer implies showing anything else, because Diagnostics is now a
2759
+ // separate column with its own toggle.
2760
+ //
2761
+ // `open` may be omitted to mean "toggle".
2762
+ case "ui/canvasOpen": {
2763
+ const open = typeof action.open === "boolean" ? action.open : !state.ui.canvasOpen;
2764
+ const sessionId = action.sessionId || state.sessions.activeSessionId || null;
2765
+ let prefs = state.canvas.prefs;
2766
+ if (sessionId) {
2767
+ const prev = prefs[sessionId] || { optedOut: false, lastViewedRev: 0 };
2768
+ if (open && prev.optedOut) {
2769
+ prefs = { ...prefs, [sessionId]: { ...prev, optedOut: false } };
2770
+ } else if (!open && action.manual && !prev.optedOut) {
2771
+ prefs = { ...prefs, [sessionId]: { ...prev, optedOut: true } };
2772
+ }
2773
+ }
2774
+ return {
2775
+ ...state,
2776
+ // rightPaneMode is kept in step for anything still reading it.
2777
+ // Closing always drops full screen: otherwise the next open
2778
+ // comes back covering the workspace with no warning.
2779
+ ui: {
2780
+ ...state.ui,
2781
+ canvasOpen: open,
2782
+ rightPaneMode: open ? "canvas" : "panes",
2783
+ ...(open ? {} : { canvasMaximized: false, canvasZen: false }),
2784
+ },
2642
2785
  canvas: { ...state.canvas, prefs },
2643
2786
  };
2644
2787
  }
2645
2788
 
2789
+ // Diagnostics — today's Inspector + Activity, travelling as one column.
2790
+ // No canvas bookkeeping: it has nothing to do with the canvas, which is
2791
+ // the whole point of splitting the old enum in two.
2792
+ // Zen: sessions hidden, chat a narrow rail, canvas the workbench.
2793
+ // Turning it on opens the canvas; it means nothing without one.
2794
+ case "ui/canvasZen": {
2795
+ const on = typeof action.on === "boolean" ? action.on : !state.ui.canvasZen;
2796
+ if (on === Boolean(state.ui.canvasZen)) return state;
2797
+ return {
2798
+ ...state,
2799
+ ui: { ...state.ui, canvasZen: on, ...(on ? { canvasOpen: true, rightPaneMode: "canvas" } : {}) },
2800
+ };
2801
+ }
2802
+
2803
+ // The zen chat-rail width for one session. Clamped so a wild drag can
2804
+ // never wedge the rail off screen or crush the canvas.
2805
+ case "canvas/zenRail": {
2806
+ const sessionId = String(action.sessionId || "").trim();
2807
+ const px = Number(action.px);
2808
+ if (!sessionId || !Number.isFinite(px)) return state;
2809
+ const prev = state.canvas.prefs[sessionId] || { optedOut: false, lastViewedRev: 0 };
2810
+ const clamped = Math.max(260, Math.min(720, Math.round(px)));
2811
+ return {
2812
+ ...state,
2813
+ canvas: { ...state.canvas, prefs: { ...state.canvas.prefs, [sessionId]: { ...prev, zenRailPx: clamped } } },
2814
+ };
2815
+ }
2816
+
2817
+ // Full-screen canvas. The header stays put and the canvas covers the
2818
+ // workspace below it, so there is always a way back.
2819
+ case "ui/canvasMaximized": {
2820
+ const on = typeof action.on === "boolean" ? action.on : !state.ui.canvasMaximized;
2821
+ if (on === Boolean(state.ui.canvasMaximized)) return state;
2822
+ return { ...state, ui: { ...state.ui, canvasMaximized: on } };
2823
+ }
2824
+
2825
+ case "ui/diagnosticsOpen": {
2826
+ const open = typeof action.open === "boolean" ? action.open : !state.ui.diagnosticsOpen;
2827
+ if (open === state.ui.diagnosticsOpen) return state;
2828
+ return { ...state, ui: { ...state.ui, diagnosticsOpen: open } };
2829
+ }
2830
+
2646
2831
  // The agent-driven flip. The guards (active session, freshness,
2647
2832
  // opt-out) live in the controller; by the time this dispatches the
2648
2833
  // flip is decided. flipSeq ticks even when the mode is already
2649
2834
  // "canvas" — a phone can be off the canvas tab while the persisted
2650
2835
  // mode still says canvas, and the tick is what brings its tab back.
2651
2836
  case "canvas/flip": {
2837
+ const slot = Number.isInteger(Number(action.slot)) && Number(action.slot) >= 1 && Number(action.slot) <= 5
2838
+ ? Number(action.slot) : 1;
2652
2839
  return {
2653
2840
  ...state,
2654
- ui: { ...state.ui, rightPaneMode: "canvas" },
2841
+ // Opens the canvas column ON THE SLOT THAT DREW; leaves
2842
+ // Diagnostics exactly as the user left it. An agent drawing
2843
+ // must not close their panels.
2844
+ ui: { ...state.ui, rightPaneMode: "canvas", canvasOpen: true, canvasSlot: slot },
2655
2845
  canvas: { ...state.canvas, flipSeq: (state.canvas.flipSeq || 0) + 1 },
2656
2846
  };
2657
2847
  }
2658
2848
 
2849
+ // Which of the active session's canvases the pane shows. In-memory
2850
+ // only: a reload lands on slot 1 (or wherever the next flip points).
2851
+ case "ui/canvasSlot": {
2852
+ const slot = Number.isInteger(Number(action.slot)) && Number(action.slot) >= 1 && Number(action.slot) <= 5
2853
+ ? Number(action.slot) : 1;
2854
+ if (slot === (state.ui.canvasSlot || 1)) return state;
2855
+ return { ...state, ui: { ...state.ui, canvasSlot: slot } };
2856
+ }
2857
+
2659
2858
  // The surface's receipt: revision `rev` (a draw) and/or `dataRev` (a
2660
2859
  // tick) actually rendered on screen. Both feed the unseen badge.
2661
2860
  case "canvas/viewed": {
@@ -2663,7 +2862,8 @@ export function appReducer(state, action) {
2663
2862
  if (!sessionId) return state;
2664
2863
  const rev = Number(action.rev);
2665
2864
  const dataRev = Number(action.dataRev);
2666
- const prev = state.canvas.prefs[sessionId] || { optedOut: false, lastViewedRev: 0, lastViewedDataRev: 0 };
2865
+ const key = canvasKey(sessionId, action.slot);
2866
+ const prev = state.canvas.prefs[key] || { optedOut: false, lastViewedRev: 0, lastViewedDataRev: 0 };
2667
2867
  const nextRev = Number.isFinite(rev) && rev > (prev.lastViewedRev || 0) ? rev : (prev.lastViewedRev || 0);
2668
2868
  const nextDataRev = Number.isFinite(dataRev) && dataRev > (prev.lastViewedDataRev || 0) ? dataRev : (prev.lastViewedDataRev || 0);
2669
2869
  if (nextRev === (prev.lastViewedRev || 0) && nextDataRev === (prev.lastViewedDataRev || 0)) return state;
@@ -2671,16 +2871,49 @@ export function appReducer(state, action) {
2671
2871
  ...state,
2672
2872
  canvas: {
2673
2873
  ...state.canvas,
2674
- prefs: { ...state.canvas.prefs, [sessionId]: { ...prev, lastViewedRev: nextRev, lastViewedDataRev: nextDataRev } },
2675
- },
2676
- };
2874
+ prefs: { ...state.canvas.prefs, [key]: { ...prev, lastViewedRev: nextRev, lastViewedDataRev: nextDataRev } },
2875
+ },
2876
+ };
2877
+ }
2878
+
2879
+ // The sessions-list seed: rev+name per slot from session_canvases,
2880
+ // merged under the live entries. Never regresses a rev a live event
2881
+ // already delivered, never sets snapshotLoaded (the full snapshot —
2882
+ // note, contract, data replay — still loads on selection).
2883
+ case "canvas/seed": {
2884
+ const entries = Array.isArray(action.entries) ? action.entries : [];
2885
+ if (!entries.length) return state;
2886
+ let next = null;
2887
+ for (const e of entries) {
2888
+ const sessionId = String(e?.sessionId || "").trim();
2889
+ const rev = Number(e?.latestRev);
2890
+ const slotN = Number(e?.slot);
2891
+ if (!sessionId || !Number.isFinite(rev) || rev <= 0) continue;
2892
+ if (!Number.isInteger(slotN) || slotN < 1 || slotN > 5) continue;
2893
+ const key = canvasKey(sessionId, slotN);
2894
+ const existing = (next || state.canvas.bySessionId)[key] || {};
2895
+ if ((existing.latestRev || 0) >= rev && existing.name !== undefined) continue;
2896
+ next = next || { ...state.canvas.bySessionId };
2897
+ next[key] = {
2898
+ ...existing,
2899
+ slot: slotN,
2900
+ latestRev: Math.max(existing.latestRev || 0, rev),
2901
+ ...(typeof e.name === "string" && existing.name === undefined ? { name: e.name } : {}),
2902
+ ...(existing.sizeBytes === undefined && e.sizeBytes !== undefined ? { sizeBytes: e.sizeBytes } : {}),
2903
+ };
2904
+ }
2905
+ if (!next) return state;
2906
+ return { ...state, canvas: { ...state.canvas, bySessionId: next } };
2677
2907
  }
2678
2908
 
2679
2909
  case "canvas/updated": {
2680
2910
  const sessionId = String(action.sessionId || "").trim();
2681
2911
  const rev = Number(action.rev);
2682
2912
  if (!sessionId || !Number.isFinite(rev) || rev <= 0) return state;
2683
- const existing = state.canvas.bySessionId[sessionId] || {};
2913
+ const slot = Number.isInteger(Number(action.slot)) && Number(action.slot) >= 1 && Number(action.slot) <= 5
2914
+ ? Number(action.slot) : 1;
2915
+ const key = canvasKey(sessionId, slot);
2916
+ const existing = state.canvas.bySessionId[key] || {};
2684
2917
  // Monotonic: replays and out-of-order merges must never regress
2685
2918
  // the known revision (freshness gates the FLIP, never the data).
2686
2919
  if ((existing.latestRev || 0) >= rev) return state;
@@ -2690,8 +2923,10 @@ export function appReducer(state, action) {
2690
2923
  ...state.canvas,
2691
2924
  bySessionId: {
2692
2925
  ...state.canvas.bySessionId,
2693
- [sessionId]: {
2926
+ [key]: {
2694
2927
  ...existing,
2928
+ slot,
2929
+ ...(typeof action.name === "string" ? { name: action.name } : {}),
2695
2930
  latestRev: rev,
2696
2931
  note: typeof action.note === "string" ? action.note : "",
2697
2932
  sizeBytes: Number.isFinite(Number(action.sizeBytes)) ? Number(action.sizeBytes) : null,
@@ -2712,9 +2947,12 @@ export function appReducer(state, action) {
2712
2947
  // latestDataRev vs lastViewedDataRev. Monotonic like the draw rev.
2713
2948
  case "canvas/data": {
2714
2949
  const sessionId = String(action.sessionId || "").trim();
2950
+ const slot = Number.isInteger(Number(action.slot)) && Number(action.slot) >= 1 && Number(action.slot) <= 5
2951
+ ? Number(action.slot) : 1;
2952
+ const key = canvasKey(sessionId, slot);
2715
2953
  const dataRev = Number(action.dataRev);
2716
2954
  if (!sessionId || !Number.isFinite(dataRev) || dataRev <= 0) return state;
2717
- const existing = state.canvas.bySessionId[sessionId] || {};
2955
+ const existing = state.canvas.bySessionId[key] || {};
2718
2956
  if ((existing.latestDataRev || 0) >= dataRev) return state;
2719
2957
  return {
2720
2958
  ...state,
@@ -2722,7 +2960,7 @@ export function appReducer(state, action) {
2722
2960
  ...state.canvas,
2723
2961
  bySessionId: {
2724
2962
  ...state.canvas.bySessionId,
2725
- [sessionId]: {
2963
+ [key]: {
2726
2964
  ...existing,
2727
2965
  latestDataRev: dataRev,
2728
2966
  dataPayload: action.payload && typeof action.payload === "object" ? action.payload : null,
@@ -2735,7 +2973,10 @@ export function appReducer(state, action) {
2735
2973
  case "canvas/snapshot": {
2736
2974
  const sessionId = String(action.sessionId || "").trim();
2737
2975
  if (!sessionId) return state;
2738
- const existing = state.canvas.bySessionId[sessionId] || {};
2976
+ const slot = Number.isInteger(Number(action.slot)) && Number(action.slot) >= 1 && Number(action.slot) <= 5
2977
+ ? Number(action.slot) : 1;
2978
+ const key = canvasKey(sessionId, slot);
2979
+ const existing = state.canvas.bySessionId[key] || {};
2739
2980
  const rev = Number(action.rev) || 0;
2740
2981
  return {
2741
2982
  ...state,
@@ -2743,8 +2984,10 @@ export function appReducer(state, action) {
2743
2984
  ...state.canvas,
2744
2985
  bySessionId: {
2745
2986
  ...state.canvas.bySessionId,
2746
- [sessionId]: {
2987
+ [key]: {
2747
2988
  ...existing,
2989
+ slot,
2990
+ ...(typeof action.name === "string" && ((existing.latestRev || 0) <= rev) ? { name: action.name } : {}),
2748
2991
  // Events may have beaten the snapshot; never regress.
2749
2992
  latestRev: Math.max(existing.latestRev || 0, rev),
2750
2993
  note: (existing.latestRev || 0) > rev ? existing.note : (typeof action.note === "string" ? action.note : existing.note || ""),
@@ -2765,6 +3008,8 @@ export function appReducer(state, action) {
2765
3008
  const sessionId = String(action.sessionId || "").trim();
2766
3009
  const existing = state.canvas.bySessionId[sessionId];
2767
3010
  if (!sessionId || !existing?.snapshotLoaded) return state;
3011
+ // Slots 2-5 of the same session invalidate with slot 1: the flag
3012
+ // that gates re-take lives on the slot-1 entry.
2768
3013
  return {
2769
3014
  ...state,
2770
3015
  canvas: {