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 { INSPECTOR_TABS, FOCUS_REGIONS } from "./commands.js";
2
+ import { canvasKey as canvasSlotKey, parseCanvasKey } from "./state.js";
2
3
  import { isManuallyOrderableSession } from "./session-tree.js";
3
4
  import {
4
5
  createSplashCard,
@@ -1125,6 +1126,10 @@ export function selectSessionRows(state) {
1125
1126
  // built under one branding to a caller using another, and the root
1126
1127
  // session's name would flip between them.
1127
1128
  state.branding?.title,
1129
+ // The canvas marker below reads these. Without them the memo hands
1130
+ // back a row built before the draw and the marker never appears.
1131
+ state.canvas?.bySessionId?.[entry.sessionId],
1132
+ state.canvas?.prefs?.[entry.sessionId],
1128
1133
  ];
1129
1134
  const cached = memo.get(entry.sessionId);
1130
1135
  if (cached && sameRowDeps(cached.deps, deps)) return cached.row;
@@ -1160,6 +1165,15 @@ export function selectSessionRows(state) {
1160
1165
  : null,
1161
1166
  orderable: isManuallyOrderableSession(session),
1162
1167
  orderContainer: manualOrderContainerKey(session, pinned),
1168
+ // Canvas state for THIS row. The toolbar badge answers the same
1169
+ // question for the active session only; the list has to answer it
1170
+ // for every session at once, which is the point — a canvas drawn
1171
+ // in a session you are not looking at is otherwise invisible.
1172
+ //
1173
+ // null no canvas, or one that was cleared
1174
+ // "canvas" a canvas exists and nothing is new
1175
+ // "unseen" it changed since this user last looked at it
1176
+ canvasMark: sessionCanvasMark(state, entry.sessionId),
1163
1177
  };
1164
1178
  memo.set(entry.sessionId, { deps, row });
1165
1179
  return row;
@@ -2661,21 +2675,87 @@ export function selectChatLines(state, maxWidth = 80, options = {}) {
2661
2675
  * whether the yellow unseen-changes badge should light
2662
2676
  * (latestRev > lastViewedRev, and only while the canvas is not on screen).
2663
2677
  */
2678
+ /**
2679
+ * The canvas marker for one session row.
2680
+ *
2681
+ * Same rules selectCanvasView applies to the active session, minus the
2682
+ * on-screen check: a row's marker describes the SESSION, not the layout, so it
2683
+ * does not care which column happens to be open.
2684
+ *
2685
+ * A cleared canvas (draw_canvas("") -> sizeBytes 0) no longer exists. Unknown
2686
+ * size (null, from a degraded snapshot) is presumed drawn — a marker that is
2687
+ * occasionally early beats one that silently never shows.
2688
+ */
2689
+ export function sessionCanvasMark(state, sessionId) {
2690
+ if (!sessionId) return null;
2691
+ // Aggregate across the session's slots: ANY unseen slot lights the dot;
2692
+ // any drawn slot at all marks the row. Slot 1 is the bare id key.
2693
+ let mark = null;
2694
+ for (let slot = 1; slot <= 5; slot += 1) {
2695
+ const key = canvasSlotKey(sessionId, slot);
2696
+ const entry = state?.canvas?.bySessionId?.[key];
2697
+ const prefs = state?.canvas?.prefs?.[key];
2698
+ if (!entry) {
2699
+ // No live state loaded (cold page). canvasPrefs persist per user,
2700
+ // so a slot this user has ever VIEWED still marks — plain, never
2701
+ // "unseen": whether it changed since is unknowable client-side.
2702
+ if ((prefs?.lastViewedRev || 0) > 0) mark = mark || "canvas";
2703
+ continue;
2704
+ }
2705
+ const latestRev = entry.latestRev || 0;
2706
+ if (latestRev <= 0 || entry.sizeBytes === 0) continue;
2707
+ const unseen = latestRev > (prefs?.lastViewedRev || 0)
2708
+ || (entry.latestDataRev || 0) > (prefs?.lastViewedDataRev || 0);
2709
+ if (unseen) return "unseen";
2710
+ mark = "canvas";
2711
+ }
2712
+ return mark;
2713
+ }
2714
+
2664
2715
  export function selectCanvasView(state) {
2665
2716
  const sessionId = state?.sessions?.activeSessionId || null;
2666
- const entry = sessionId ? state?.canvas?.bySessionId?.[sessionId] : null;
2667
- const prefs = sessionId ? state?.canvas?.prefs?.[sessionId] : null;
2717
+ // The pane shows ONE slot at a time. An out-of-range or never-drawn slot
2718
+ // falls back to 1 so a stale selection can never strand the pane.
2719
+ const wantedSlot = Number(state?.ui?.canvasSlot) || 1;
2720
+ const bySessionId = state?.canvas?.bySessionId || {};
2721
+ // Every drawn slot of the active session, for the selector and arrows.
2722
+ const slots = [];
2723
+ if (sessionId) {
2724
+ for (let slot = 1; slot <= 5; slot += 1) {
2725
+ const e = bySessionId[canvasSlotKey(sessionId, slot)];
2726
+ if (!e || (e.latestRev || 0) <= 0 || e.sizeBytes === 0) continue;
2727
+ const p = state?.canvas?.prefs?.[canvasSlotKey(sessionId, slot)];
2728
+ slots.push({
2729
+ slot,
2730
+ name: typeof e.name === "string" ? e.name : "",
2731
+ latestRev: e.latestRev || 0,
2732
+ unseen: (e.latestRev || 0) > (p?.lastViewedRev || 0)
2733
+ || (e.latestDataRev || 0) > (p?.lastViewedDataRev || 0),
2734
+ });
2735
+ }
2736
+ }
2737
+ const slot = slots.some((s2) => s2.slot === wantedSlot) ? wantedSlot : 1;
2738
+ const key = sessionId ? canvasSlotKey(sessionId, slot) : null;
2739
+ const entry = key ? bySessionId[key] : null;
2740
+ const prefs = key ? state?.canvas?.prefs?.[key] : null;
2668
2741
  const latestRev = entry?.latestRev || 0;
2669
2742
  const lastViewedRev = prefs?.lastViewedRev || 0;
2670
2743
  const latestDataRev = entry?.latestDataRev || 0;
2671
2744
  const lastViewedDataRev = prefs?.lastViewedDataRev || 0;
2672
- const mode = state?.ui?.rightPaneMode === "canvas" ? "canvas" : "panes";
2745
+ // Read from canvasOpen, not the retired rightPaneMode enum. Diagnostics
2746
+ // moving to its own column means "not canvas" no longer implies "panes",
2747
+ // and this drives the unseen badge, which must light whenever the canvas
2748
+ // is off screen.
2749
+ const mode = state?.ui?.canvasOpen ? "canvas" : "panes";
2673
2750
  // A cleared canvas (draw_canvas("") → sizeBytes 0) no longer exists for
2674
2751
  // affordance purposes: no phone tab, no badge, blank teaching state.
2675
2752
  // Unknown size (null — snapshot degrade) is presumed drawn.
2676
2753
  const exists = latestRev > 0 && entry?.sizeBytes !== 0;
2677
2754
  return {
2678
2755
  sessionId,
2756
+ slot,
2757
+ slots,
2758
+ name: typeof entry?.name === "string" ? entry.name : "",
2679
2759
  mode,
2680
2760
  exists,
2681
2761
  latestRev,
@@ -3644,9 +3724,29 @@ export function selectAdminConsole(state) {
3644
3724
  distinctPackageOwners.add(key);
3645
3725
  }
3646
3726
  const decoratePackageOwners = distinctPackageOwners.size > 1;
3727
+ // Copy-identity helpers: one NAME can be two packages (scope shadowing),
3728
+ // so row selection and highlighting key on (name, scope, owner) — never
3729
+ // the bare name.
3730
+ const packageCopyKey = (name, scope, owner) =>
3731
+ `${name}\u0001${scope === "shared" ? "shared" : "user"}\u0001${scope === "shared" ? "" : (owner?.subject ?? "")}`;
3732
+ const selectedCopyKey = pkgState.selectedName
3733
+ ? packageCopyKey(
3734
+ pkgState.selectedName,
3735
+ pkgState.selectedSelector?.scope ?? null,
3736
+ pkgState.selectedSelector?.owner ?? principal ?? null,
3737
+ )
3738
+ : null;
3739
+ const rowMatchesSelection = (pkg) => {
3740
+ if (admin.section !== "packages" || pkgState.selectedName !== pkg.name) return false;
3741
+ // Legacy selection with no selector: fall back to name-only so an
3742
+ // in-flight selection from an older host still highlights something.
3743
+ if (!pkgState.selectedSelector?.scope) return true;
3744
+ return packageCopyKey(pkg.name, pkg.scope, pkg.owner) === selectedCopyKey;
3745
+ };
3647
3746
  const packageRow = (pkg) => ({
3648
3747
  name: pkg.name,
3649
3748
  scope: pkg.scope === "shared" ? "shared" : "user",
3749
+ owner: pkg.owner ?? null,
3650
3750
  enabled: Boolean(pkg.enabled),
3651
3751
  semver: pkg.active?.semver || null,
3652
3752
  sha7: pkg.active?.sha256 ? String(pkg.active.sha256).slice(0, 7) : null,
@@ -3662,7 +3762,7 @@ export function selectAdminConsole(state) {
3662
3762
  : ownerBadgeFor(resolvePackageOwner(pkg, ownerDirectory)),
3663
3763
  // Highlight only while the Agents section is active — otherwise the
3664
3764
  // GitHub Keys screen shows a double selection.
3665
- selected: admin.section === "packages" && pkgState.selectedName === pkg.name,
3765
+ selected: rowMatchesSelection(pkg),
3666
3766
  });
3667
3767
  // Admins are served every user-scope package, but another person's private
3668
3768
  // agents are not part of YOUR workspace — keep them out of the main tree
@@ -3707,13 +3807,13 @@ export function selectAdminConsole(state) {
3707
3807
  { id: "ghcp", kind: "section", depth: 0, label: "GitHub Keys", selected: section === "ghcp" },
3708
3808
  { id: "agents", kind: "section", depth: 0, label: "Agents", selected: section === "packages" && !pkgState.selectedName },
3709
3809
  { id: "group:shared", kind: "group", depth: 1, label: "Shared", count: sharedRows.length },
3710
- ...sharedRows.map((row) => ({ id: `pkg:${row.name}`, kind: "package", depth: 2, label: row.name, ...row })),
3810
+ ...sharedRows.map((row) => ({ id: `pkg:shared:${row.name}`, kind: "package", depth: 2, label: row.name, ...row })),
3711
3811
  { id: "group:user", kind: "group", depth: 1, label: "User", count: userRows.length },
3712
- ...userRows.map((row) => ({ id: `pkg:${row.name}`, kind: "package", depth: 2, label: row.name, ...row })),
3812
+ ...userRows.map((row) => ({ id: `pkg:user:${row.name}`, kind: "package", depth: 2, label: row.name, ...row })),
3713
3813
  ...(otherUserRows.length > 0
3714
3814
  ? [
3715
3815
  { id: "group:others", kind: "group", depth: 1, label: "Other users", count: otherUserRows.length },
3716
- ...otherUserRows.map((row) => ({ id: `pkg:${row.name}`, kind: "package", depth: 2, label: row.name, ...row })),
3816
+ ...otherUserRows.map((row) => ({ id: `pkg:user:${row.owner?.subject ?? "?"}:${row.name}`, kind: "package", depth: 2, label: row.name, ...row })),
3717
3817
  ]
3718
3818
  : []),
3719
3819
  // The worker registry is a hard-gated admin read; hide the section
@@ -3725,7 +3825,10 @@ export function selectAdminConsole(state) {
3725
3825
  const detail = pkgState.detail || null;
3726
3826
  let packageDetail = null;
3727
3827
  if (pkgState.selectedName) {
3728
- const summary = pkgList.find((pkg) => pkg.name === pkgState.selectedName) || null;
3828
+ const sameName = pkgList.filter((pkg) => pkg.name === pkgState.selectedName);
3829
+ const summary = (pkgState.selectedSelector?.scope
3830
+ ? sameName.find((pkg) => packageCopyKey(pkg.name, pkg.scope, pkg.owner) === selectedCopyKey)
3831
+ : sameName[0]) ?? sameName[0] ?? null;
3729
3832
  const activeVersion = detail?.versions?.find((v) => v.versionId === detail.activeVersionId) || null;
3730
3833
  const manifest = activeVersion?.manifest || summary?.active?.manifest || {};
3731
3834
  // Workers are ephemeral pods: rows for retired pod names linger until
@@ -3739,9 +3842,22 @@ export function selectAdminConsole(state) {
3739
3842
  return !Number.isNaN(at.getTime()) && at.getTime() >= liveCutoff;
3740
3843
  });
3741
3844
  const fleetTotal = workerRows.length;
3845
+ // Workers report installed state by bare name — except when two
3846
+ // same-named copies are installed at once, where each row gets a
3847
+ // qualified key (name#scope[:owner8]) so the collision stays visible.
3848
+ // Read whichever key this copy answers to.
3849
+ const installedRowFor = (worker) => {
3850
+ const installed = worker.installed || {};
3851
+ const scope = detail?.scope || summary?.scope || pkgState.selectedSelector?.scope || null;
3852
+ const ownerSubject = (detail?.owner?.subject || summary?.owner?.subject || pkgState.selectedSelector?.owner?.subject || "");
3853
+ const qualified = scope === "user"
3854
+ ? `${pkgState.selectedName}#user${ownerSubject ? `:${ownerSubject.slice(0, 8)}` : ""}`
3855
+ : `${pkgState.selectedName}#shared`;
3856
+ return installed[qualified] ?? installed[pkgState.selectedName];
3857
+ };
3742
3858
  const fleetCurrent = activeVersion
3743
- ? workerRows.filter((worker) => worker.installed?.[pkgState.selectedName]?.semver === activeVersion.semver
3744
- && worker.installed?.[pkgState.selectedName]?.status === "ok").length
3859
+ ? workerRows.filter((worker) => installedRowFor(worker)?.semver === activeVersion.semver
3860
+ && installedRowFor(worker)?.status === "ok").length
3745
3861
  : 0;
3746
3862
  const canManage = detail ? ownsPackage(detail) : (summary ? ownsPackage(summary) : false);
3747
3863
  packageDetail = {
@@ -3776,6 +3892,12 @@ export function selectAdminConsole(state) {
3776
3892
  : null,
3777
3893
  actionPending: pkgState.action?.pending || null,
3778
3894
  actionError: pkgState.action?.error || null,
3895
+ // Which same-named copies exist, for the republish affordance and
3896
+ // the shadowing note. Counterparts the caller can SEE only —
3897
+ // exactly what the list already enforces.
3898
+ hasSharedCounterpart: sameName.some((pkg) => pkg.scope === "shared"),
3899
+ hasOwnUserCounterpart: sameName.some((pkg) => pkg.scope !== "shared" && isMinePackage(pkg)),
3900
+ copyCount: sameName.length,
3779
3901
  };
3780
3902
  }
3781
3903
 
@@ -73,11 +73,23 @@ export function normalizeStoredLayoutAdjustments(layoutAdjustments) {
73
73
  const sessionPaneAdjust = Number(candidate.sessionPaneAdjust);
74
74
  const portalSessionColumnAdjust = Number(candidate.portalSessionColumnAdjust);
75
75
  const activityPaneAdjust = Number(candidate.activityPaneAdjust);
76
+ // Canvas is a column of its own now rather than a face the right column
77
+ // wears, so it needs its own remembered width. Same mechanism as the rest:
78
+ // a pixel delta from the computed base, persisted per user.
79
+ const canvasPaneAdjust = Number(candidate.canvasPaneAdjust);
80
+ const diagnosticsPaneAdjust = Number(candidate.diagnosticsPaneAdjust);
81
+ const diagnosticsSplitAdjust = Number(candidate.diagnosticsSplitAdjust);
76
82
  return {
77
83
  paneAdjust: Number.isFinite(paneAdjust) ? Math.trunc(paneAdjust) : 0,
78
84
  sessionPaneAdjust: Number.isFinite(sessionPaneAdjust) ? Math.trunc(sessionPaneAdjust) : 0,
79
85
  portalSessionColumnAdjust: Number.isFinite(portalSessionColumnAdjust) ? Math.trunc(portalSessionColumnAdjust) : 0,
80
86
  activityPaneAdjust: Number.isFinite(activityPaneAdjust) ? Math.trunc(activityPaneAdjust) : 0,
87
+ canvasPaneAdjust: Number.isFinite(canvasPaneAdjust) ? Math.trunc(canvasPaneAdjust) : 0,
88
+ diagnosticsPaneAdjust: Number.isFinite(diagnosticsPaneAdjust) ? Math.trunc(diagnosticsPaneAdjust) : 0,
89
+ // The inspector/activity seam inside Diagnostics, in PIXELS from an
90
+ // even split. The old row-quantized TUI adjust made browser drags
91
+ // jump in ~20px notches.
92
+ diagnosticsSplitAdjust: Number.isFinite(diagnosticsSplitAdjust) ? Math.trunc(diagnosticsSplitAdjust) : 0,
81
93
  };
82
94
  }
83
95
 
@@ -132,11 +144,66 @@ export function normalizeStoredCollapsedSessionIds(value) {
132
144
  * Right-column mode: the inspector/activity split, or the session canvas.
133
145
  * Anything unrecognized falls back to panes — a stored value from a newer
134
146
  * build must not strand an older portal in a mode it cannot render.
147
+ *
148
+ * @deprecated Superseded by the independent `canvasOpen` / `diagnosticsOpen`
149
+ * booleans. Kept only to migrate a value written by an older build — see
150
+ * normalizeStoredDesktopPanes.
135
151
  */
136
152
  export function normalizeStoredRightPaneMode(value) {
137
153
  return value === "canvas" ? "canvas" : "panes";
138
154
  }
139
155
 
156
+ /**
157
+ * Which of the two optional desktop columns are on screen.
158
+ *
159
+ * Canvas and Diagnostics used to be one enum (`rightPaneMode`), so the right
160
+ * column could only ever wear one face. They are independent now: either, both,
161
+ * or neither. Diagnostics is the old inspector/activity split, moved out of the
162
+ * chat pane and travelling as one unit.
163
+ *
164
+ * Both default to OFF. A pane that is off has no width and no resizer — it is
165
+ * absent, not collapsed to nothing.
166
+ *
167
+ * Migrating a stored `rightPaneMode`:
168
+ * "canvas" -> canvas on. That was a deliberate choice, so it is honored.
169
+ * "panes" -> both off. "panes" was the default value rather than a choice,
170
+ * and the new default is a clean two-column workspace.
171
+ */
172
+ export function normalizeStoredDesktopPanes(value, legacyRightPaneMode) {
173
+ const candidate = value && typeof value === "object" ? value : null;
174
+ if (candidate && (typeof candidate.canvasOpen === "boolean" || typeof candidate.diagnosticsOpen === "boolean")) {
175
+ return {
176
+ canvasOpen: candidate.canvasOpen === true,
177
+ diagnosticsOpen: candidate.diagnosticsOpen === true,
178
+ // Zen: chat as a narrow rail, canvas as the workbench. Only
179
+ // meaningful with the canvas open.
180
+ zen: candidate.zen === true && candidate.canvasOpen === true,
181
+ };
182
+ }
183
+ return {
184
+ canvasOpen: legacyRightPaneMode === "canvas",
185
+ diagnosticsOpen: false,
186
+ zen: false,
187
+ };
188
+ }
189
+
190
+ /**
191
+ * The store key for one canvas surface. Slot 1 keeps the bare session id so
192
+ * every reader written before slots existed keeps working untouched; slots
193
+ * 2-5 append "#<slot>". Used for canvas.bySessionId AND canvas.prefs.
194
+ */
195
+ export function canvasKey(sessionId, slot) {
196
+ const id = String(sessionId || "");
197
+ const n = Number(slot);
198
+ return Number.isInteger(n) && n >= 2 && n <= 5 ? `${id}#${n}` : id;
199
+ }
200
+
201
+ /** The inverse: `sessionId#3` -> { sessionId, slot: 3 }; bare id -> slot 1. */
202
+ export function parseCanvasKey(key) {
203
+ const m = /^(.*)#([2-5])$/.exec(String(key || ""));
204
+ return m ? { sessionId: m[1], slot: Number(m[2]) } : { sessionId: String(key || ""), slot: 1 };
205
+ }
206
+
140
207
  /**
141
208
  * Per-session canvas preferences: what this user has chosen and seen.
142
209
  *
@@ -154,10 +221,13 @@ export function normalizeStoredCanvasPrefs(value) {
154
221
  if (!id || !entry || typeof entry !== "object") continue;
155
222
  const lastViewedRev = Number(entry.lastViewedRev);
156
223
  const lastViewedDataRev = Number(entry.lastViewedDataRev);
224
+ const zenRailPx = Number(entry.zenRailPx);
157
225
  out[id] = {
158
226
  optedOut: entry.optedOut === true,
159
227
  lastViewedRev: Number.isFinite(lastViewedRev) && lastViewedRev > 0 ? Math.floor(lastViewedRev) : 0,
160
228
  lastViewedDataRev: Number.isFinite(lastViewedDataRev) && lastViewedDataRev > 0 ? Math.floor(lastViewedDataRev) : 0,
229
+ // Zen chat-rail width, remembered for THIS session only.
230
+ ...(Number.isFinite(zenRailPx) && zenRailPx > 0 ? { zenRailPx: Math.floor(zenRailPx) } : {}),
161
231
  };
162
232
  }
163
233
  return out;
@@ -169,7 +239,11 @@ export function normalizeStoredActiveSessionId(value) {
169
239
  return id ? id : null;
170
240
  }
171
241
 
172
- export function createInitialState({ mode = "local", branding = null, docs = null, themeId = null, sessionOwnerFilter = null, layoutAdjustments = null, pinnedSessionIds = null, collapsedSessionIds = null, activeSessionId = null, sessionOrder = null, rightPaneMode = null, canvasPrefs = null } = {}) {
242
+ export function createInitialState({ mode = "local", branding = null, docs = null, themeId = null, sessionOwnerFilter = null, layoutAdjustments = null, pinnedSessionIds = null, collapsedSessionIds = null, activeSessionId = null, sessionOrder = null, rightPaneMode = null, desktopPanes: storedDesktopPanes = null, canvasPrefs = null } = {}) {
243
+ const desktopPanes = normalizeStoredDesktopPanes(
244
+ storedDesktopPanes,
245
+ normalizeStoredRightPaneMode(rightPaneMode),
246
+ );
173
247
  const hasStoredSessionOwnerFilter = sessionOwnerFilter != null;
174
248
  const hasStoredCollapsedSessionIds = collapsedSessionIds != null;
175
249
  const initialLayoutAdjustments = normalizeStoredLayoutAdjustments(layoutAdjustments);
@@ -201,7 +275,21 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
201
275
  // through to the transcript.
202
276
  // Which face the right column wears: the inspector/activity panes
203
277
  // or the session canvas. Persisted.
204
- rightPaneMode: normalizeStoredRightPaneMode(rightPaneMode),
278
+ //
279
+ // @deprecated Read by nothing that decides layout any more — the
280
+ // two booleans below do that. Still derived so a build that rolls
281
+ // back, and anything still reading the old key, sees a sane value.
282
+ rightPaneMode: desktopPanes.canvasOpen ? "canvas" : "panes",
283
+ // The two optional desktop columns, independent of each other.
284
+ // Both off is the default workspace: sessions and chat only.
285
+ canvasOpen: desktopPanes.canvasOpen,
286
+ diagnosticsOpen: desktopPanes.diagnosticsOpen,
287
+ // Chat-rail + canvas-workbench mode. Persisted with the columns.
288
+ canvasZen: desktopPanes.zen,
289
+ // Canvas filling the workspace. Deliberately NOT persisted: it is
290
+ // a "look at this now" gesture, and returning to a portal with no
291
+ // chat and no session list would read as a broken app.
292
+ canvasMaximized: false,
205
293
  statsViewMode: "session",
206
294
  prompt: "",
207
295
  promptCursor: 0,
@@ -61,7 +61,7 @@ const THEMES = Object.freeze([
61
61
 
62
62
  const THEME_MAP = new Map(THEMES.map((theme) => [theme.id, theme]));
63
63
 
64
- export const DEFAULT_THEME_ID = noctisObscuroTheme.id;
64
+ export const DEFAULT_THEME_ID = workspaceDarkTheme.id;
65
65
 
66
66
  export { THEME_GROUP_ORDER } from "./helpers.js";
67
67