pilotswarm 0.5.59 → 0.5.60

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 (79) hide show
  1. package/README.md +6 -0
  2. package/package.json +2 -2
  3. package/ui/core/src/controller.js +39 -13
  4. package/ui/core/src/history.js +15 -1
  5. package/ui/core/src/index.js +2 -0
  6. package/ui/core/src/moa.js +47 -0
  7. package/ui/core/src/reducer.js +19 -2
  8. package/ui/core/src/selectors.js +27 -13
  9. package/ui/core/src/session-errors.js +97 -0
  10. package/ui/core/src/state.js +1 -0
  11. package/ui/react/src/web-app.js +215 -77
  12. package/web/dist/assets/{abnfDiagram-VRR7QNED-D9Rv2VpV.js → abnfDiagram-VRR7QNED-CXEHJkTQ.js} +1 -1
  13. package/web/dist/assets/{arc-BATiucnq.js → arc-lInHlEVm.js} +1 -1
  14. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-CSTMt0si.js → architectureDiagram-ZJ3FMSHR-vrocx6re.js} +1 -1
  15. package/web/dist/assets/{blockDiagram-677ZJIJ3-C_RGqfeY.js → blockDiagram-677ZJIJ3-B1KA5QIB.js} +1 -1
  16. package/web/dist/assets/{c4Diagram-LMCZKHZV-DH__s__0.js → c4Diagram-LMCZKHZV-Du9xJMjz.js} +1 -1
  17. package/web/dist/assets/channel-6wPrR6sC.js +1 -0
  18. package/web/dist/assets/{chunk-2Q5K7J3B-C197fD3B.js → chunk-2Q5K7J3B-BuHucQOQ.js} +1 -1
  19. package/web/dist/assets/{chunk-32BRIVSS-BjpuBroU.js → chunk-32BRIVSS-T-AE5Yz0.js} +1 -1
  20. package/web/dist/assets/{chunk-5VM5RSS4-W_jL8euj.js → chunk-5VM5RSS4-B_kTsNzt.js} +1 -1
  21. package/web/dist/assets/{chunk-EX3LRPZG-a9h0dtlp.js → chunk-EX3LRPZG-CtqgceBQ.js} +1 -1
  22. package/web/dist/assets/{chunk-JWPE2WC7-CQN0qE7F.js → chunk-JWPE2WC7-C_Fv9QUd.js} +1 -1
  23. package/web/dist/assets/{chunk-MOJQB5TN-D-Y6KQ7t.js → chunk-MOJQB5TN-D5GeF8ZZ.js} +1 -1
  24. package/web/dist/assets/{chunk-RYQCIY6F-jPwseC5f.js → chunk-RYQCIY6F-Cg63wDst.js} +1 -1
  25. package/web/dist/assets/{chunk-V7JOEXUC-DRNCEcQh.js → chunk-V7JOEXUC-Bz6G3lev.js} +1 -1
  26. package/web/dist/assets/{chunk-VR4S4FIN-DzhUcoZ7.js → chunk-VR4S4FIN-DKs_hvUD.js} +1 -1
  27. package/web/dist/assets/{chunk-XXDRQBXY-ldJfyPXK.js → chunk-XXDRQBXY-B9AtUlbn.js} +1 -1
  28. package/web/dist/assets/classDiagram-OUVF2IWQ-vvXyIfbC.js +1 -0
  29. package/web/dist/assets/classDiagram-v2-EOCWNBFH-vvXyIfbC.js +1 -0
  30. package/web/dist/assets/{cose-bilkent-JH36ORCC-BBVo21wR.js → cose-bilkent-JH36ORCC-rAG6oS0i.js} +1 -1
  31. package/web/dist/assets/{cynefin-VYW2F7L2-CgRQfdW1.js → cynefin-VYW2F7L2-CUy2C8UT.js} +1 -1
  32. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-Ckz2X0t3.js → cynefinDiagram-TSTJHNR4-C1W8blXH.js} +1 -1
  33. package/web/dist/assets/{dagre-VKFMJZFB-C1SanJiY.js → dagre-VKFMJZFB-DC0XqwcC.js} +1 -1
  34. package/web/dist/assets/{diagram-FQU43EPY-dBdEifVy.js → diagram-FQU43EPY-COAY9zCP.js} +1 -1
  35. package/web/dist/assets/{diagram-G47NLZAW-Byqc1VCB.js → diagram-G47NLZAW-DLPr8eWG.js} +1 -1
  36. package/web/dist/assets/{diagram-NH7WQ7WH-9aeTaCsX.js → diagram-NH7WQ7WH-DoNgwoDz.js} +1 -1
  37. package/web/dist/assets/{diagram-OA4YK3LP-CL0a0o0k.js → diagram-OA4YK3LP-D3-5TINE.js} +1 -1
  38. package/web/dist/assets/{diagram-WEI45ONY-YlMQOJfY.js → diagram-WEI45ONY-DplCkBJC.js} +1 -1
  39. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-qMZQEnws.js → ebnfDiagram-CCIWWBDH-dvcVh7yr.js} +1 -1
  40. package/web/dist/assets/{erDiagram-Q63AITRT-C3CWBCtq.js → erDiagram-Q63AITRT-Cde8cTY6.js} +1 -1
  41. package/web/dist/assets/{flowDiagram-23GEKE2U-BN79sybL.js → flowDiagram-23GEKE2U-MGMNsh2E.js} +1 -1
  42. package/web/dist/assets/{ganttDiagram-NO4QXBWP-BOWhiAgB.js → ganttDiagram-NO4QXBWP-BiKw7AfS.js} +1 -1
  43. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-CKX9OACp.js → gitGraphDiagram-IHSO6WYX-vC7fsylq.js} +1 -1
  44. package/web/dist/assets/index-D9Ab1VTY.css +1 -0
  45. package/web/dist/assets/index-DDAyy127.js +24 -0
  46. package/web/dist/assets/{infoDiagram-FWYZ7A6U-QQEunPNs.js → infoDiagram-FWYZ7A6U-Du1uirB0.js} +1 -1
  47. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-CvMwrnLQ.js → ishikawaDiagram-FXEZZL3T-Dk--Qshr.js} +1 -1
  48. package/web/dist/assets/{journeyDiagram-5HDEW3XC-giNpEVpj.js → journeyDiagram-5HDEW3XC-iDE4zf_N.js} +1 -1
  49. package/web/dist/assets/{kanban-definition-HUTT4EX6-CUPPpo27.js → kanban-definition-HUTT4EX6-D7WI8iRR.js} +1 -1
  50. package/web/dist/assets/{linear-AXqadfOR.js → linear-B1_kxCBK.js} +1 -1
  51. package/web/dist/assets/{mermaid.core-C78wdk8Y.js → mermaid.core-DNmC5O4Y.js} +4 -4
  52. package/web/dist/assets/{mindmap-definition-LN4V7U3C-BGUmh9bK.js → mindmap-definition-LN4V7U3C-DIbLKLMA.js} +1 -1
  53. package/web/dist/assets/{papaparse.min-Bh8RLCk_.js → papaparse.min-BdWinjY-.js} +1 -1
  54. package/web/dist/assets/{pegDiagram-2B236MQR-BHsKXKhK.js → pegDiagram-2B236MQR-CkxxR07G.js} +1 -1
  55. package/web/dist/assets/{pieDiagram-ENE6RG2P-D3EvWTdH.js → pieDiagram-ENE6RG2P-Dxvp1mn7.js} +1 -1
  56. package/web/dist/assets/pilotswarm-493nqGmy.js +98 -0
  57. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-CbkxfLkV.js → quadrantDiagram-ABIIQ3AL-CrL0hjZW.js} +1 -1
  58. package/web/dist/assets/{railroadDiagram-RFXS5EU6-H4M6JSDW.js → railroadDiagram-RFXS5EU6-CmVmFBPa.js} +1 -1
  59. package/web/dist/assets/{requirementDiagram-TGXJPOKE-BJo7JW1v.js → requirementDiagram-TGXJPOKE-DJc3NRXb.js} +1 -1
  60. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-DL19MeOP.js → sankeyDiagram-HTMAVEWB-BJNqWqBb.js} +1 -1
  61. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-DyVFc5pQ.js → sequenceDiagram-DBY2YBRQ-C0wddECV.js} +1 -1
  62. package/web/dist/assets/{sizeCapture-X5ZJPWSS-CKbcKIEQ.js → sizeCapture-X5ZJPWSS-Cw0dqPWS.js} +1 -1
  63. package/web/dist/assets/{stateDiagram-2N3HPSRC-BSukSXzS.js → stateDiagram-2N3HPSRC-C5793RHZ.js} +1 -1
  64. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-qGaRayHj.js +1 -0
  65. package/web/dist/assets/{swimlanes-5IMT3BWC-BUd7UhsF.js → swimlanes-5IMT3BWC-Bhpm77Zu.js} +2 -2
  66. package/web/dist/assets/swimlanesDiagram-G3AALYLV-BXjNM_Zw.js +8 -0
  67. package/web/dist/assets/{timeline-definition-FHXFAJF6-Dt1gludn.js → timeline-definition-FHXFAJF6-CwbE4NsH.js} +1 -1
  68. package/web/dist/assets/{vennDiagram-L72KCM5P-BHxt00GV.js → vennDiagram-L72KCM5P-DZnJEL_w.js} +1 -1
  69. package/web/dist/assets/{wardleyDiagram-EHGQE667-BT3fIDbe.js → wardleyDiagram-EHGQE667-DwTgBDev.js} +1 -1
  70. package/web/dist/assets/{xychartDiagram-FW5EYKEG-ChoYDS2b.js → xychartDiagram-FW5EYKEG-BiPjN3Qc.js} +1 -1
  71. package/web/dist/index.html +3 -3
  72. package/web/dist/assets/channel-CqgY9oZh.js +0 -1
  73. package/web/dist/assets/classDiagram-OUVF2IWQ-Uz4DYZs2.js +0 -1
  74. package/web/dist/assets/classDiagram-v2-EOCWNBFH-Uz4DYZs2.js +0 -1
  75. package/web/dist/assets/index-B4s2lTsK.js +0 -24
  76. package/web/dist/assets/index-DhGdE2Nd.css +0 -1
  77. package/web/dist/assets/pilotswarm-UV6T1_QI.js +0 -98
  78. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-BhKG3pJB.js +0 -1
  79. package/web/dist/assets/swimlanesDiagram-G3AALYLV-CgoFkXJE.js +0 -8
@@ -6,6 +6,7 @@ import React from "react";
6
6
  import { createPortal } from "react-dom";
7
7
  import { appendAnimatedDotsToRuns, useAnimatedDots, useSpinnerFrame } from "./chat-status.js";
8
8
  import {
9
+ normalizeMoa,
9
10
  UI_COMMANDS,
10
11
  INSPECTOR_TABS,
11
12
  ARTIFACT_DOWNLOAD_HINT,
@@ -286,6 +287,7 @@ function clearBrowserPreferenceCache() {
286
287
  function normalizeProfileSettings(settings) {
287
288
  const candidate = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
288
289
  const normalized = {};
290
+ if (hasOwn(candidate, "moa")) normalized.moa = normalizeMoa(candidate.moa);
289
291
  if (typeof candidate.themeId === "string" && candidate.themeId.trim()) {
290
292
  normalized.themeId = candidate.themeId.trim();
291
293
  }
@@ -431,8 +433,18 @@ function hasOwn(value, key) {
431
433
  return Boolean(value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key));
432
434
  }
433
435
 
436
+ function profileViewState(root) {
437
+ return {
438
+ ...root.ui, ...root.ui.layout,
439
+ ownerFilter: root.sessions.ownerFilter, pinnedIds: root.sessions.pinnedIds,
440
+ manualOrder: root.sessions.manualOrder, collapsedSessionIds: root.sessions.collapsedIds,
441
+ activeSessionId: root.sessions.activeSessionId, canvasPrefs: root.canvas?.prefs,
442
+ };
443
+ }
444
+
434
445
  function profileSettingsFromViewState(state, preservedOtherTouchScale = null, preservedRightPaneMode = null, preservedDesktopPanes = null) {
435
446
  return normalizeProfileSettings({
447
+ ...(state.moa ? { moa: state.moa } : {}),
436
448
  themeId: state.themeId,
437
449
  [touchScaleKey()]: state.touchScale,
438
450
  sessionDetailCollapsed: state.sessionDetailCollapsed,
@@ -498,6 +510,7 @@ function profileSettingsFromViewState(state, preservedOtherTouchScale = null, pr
498
510
 
499
511
  function buildDefaultProfileSettingsFromState(state, preservedOtherTouchScale = null, preservedRightPaneMode = null) {
500
512
  return normalizeProfileSettings({
513
+ ...(state?.ui?.moa ? { moa: state.ui.moa } : {}),
501
514
  themeId: state?.ui?.themeId,
502
515
  [touchScaleKey()]: state?.ui?.touchScale,
503
516
  sessionDetailCollapsed: state?.ui?.sessionDetailCollapsed,
@@ -541,6 +554,7 @@ function materializeProfileSettings(remoteSettings, defaults) {
541
554
  // the server has not persisted yet. An omitted key leaves the
542
555
  // profileSettings/apply guards preserving the current value.
543
556
  return normalizeProfileSettings({
557
+ ...(hasOwn(normalizedRemote, "moa") ? { moa: normalizedRemote.moa } : {}),
544
558
  themeId: hasOwn(normalizedRemote, "themeId")
545
559
  ? normalizedRemote.themeId
546
560
  : normalizedDefaults.themeId,
@@ -611,11 +625,18 @@ function materializeProfileSettings(remoteSettings, defaults) {
611
625
  });
612
626
  }
613
627
 
628
+ const profileSaveQueues = new WeakMap();
614
629
  async function saveProfileSettings(controller, settings) {
615
630
  if (typeof controller?.transport?.setCurrentUserProfileSettings !== "function") return null;
616
- return controller.transport.setCurrentUserProfileSettings({
631
+ // Whole-document writes must arrive in edit order. A slow earlier request
632
+ // must never land after a newer layout and erase it.
633
+ const previous = profileSaveQueues.get(controller) || Promise.resolve();
634
+ const next = previous.catch(() => {}).then(() => controller.transport.setCurrentUserProfileSettings({
617
635
  settings: normalizeProfileSettings(settings),
618
- });
636
+ }));
637
+ profileSaveQueues.set(controller, next);
638
+ try { return await next; }
639
+ finally { if (profileSaveQueues.get(controller) === next) profileSaveQueues.delete(controller); }
619
640
  }
620
641
 
621
642
  function getVisibleInspectorTabs(controller) {
@@ -993,10 +1014,11 @@ function applyDocumentTheme(themeId) {
993
1014
  root.dataset.psTheme = theme.id;
994
1015
  }
995
1016
 
996
- function useMeasuredViewport(ref) {
1017
+ function useMeasuredViewport(ref, suspended = false) {
997
1018
  const [viewport, setViewport] = React.useState({ width: 0, height: 0 });
998
1019
 
999
1020
  React.useLayoutEffect(() => {
1021
+ if (suspended) return undefined;
1000
1022
  const element = ref.current;
1001
1023
  if (!element) return undefined;
1002
1024
 
@@ -1015,7 +1037,7 @@ function useMeasuredViewport(ref) {
1015
1037
  observer.disconnect();
1016
1038
  window.removeEventListener("resize", update);
1017
1039
  };
1018
- }, [ref]);
1040
+ }, [ref, suspended]);
1019
1041
 
1020
1042
  return viewport;
1021
1043
  }
@@ -2862,7 +2884,7 @@ function useElementBox(ref) {
2862
2884
  return box;
2863
2885
  }
2864
2886
 
2865
- function fetchArtifactHtmlObjectUrl(controller, sessionId, filename) {
2887
+ function fetchArtifactHtmlObjectUrl(controller, sessionId, filename, panelKeys = false) {
2866
2888
  const downloadResponse = controller?.transport?.api?.downloadArtifactResponse;
2867
2889
  if (typeof downloadResponse !== "function") {
2868
2890
  return Promise.reject(new Error("artifact download unavailable"));
@@ -2875,7 +2897,18 @@ function fetchArtifactHtmlObjectUrl(controller, sessionId, filename) {
2875
2897
  // would otherwise make the iframe offer a download instead of
2876
2898
  // rendering, and a missing charset would mojibake UTF-8 content.
2877
2899
  const bytes = await response.arrayBuffer();
2878
- return URL.createObjectURL(new Blob([bytes], { type: "text/html;charset=utf-8" }));
2900
+ // Only MoA canvases opt into panel navigation. Ordinary canvases
2901
+ // retain native Tab behavior and their exact document bytes.
2902
+ const bridge = panelKeys ? `<script>window.addEventListener('keydown',function(e){if(e.isTrusted&&!e.altKey&&!e.metaKey&&!e.ctrlKey&&(e.key==='Tab'||e.key==='Escape')){e.preventDefault();e.stopImmediatePropagation();parent.postMessage({type:'moa-panel-key',key:e.key,backwards:e.shiftKey},'*')}},true);</script>` : "";
2903
+ let parts = [bytes];
2904
+ if (panelKeys) {
2905
+ const html = new TextDecoder().decode(bytes);
2906
+ // Keep the doctype first so panel navigation cannot change
2907
+ // the canvas from standards mode into quirks mode.
2908
+ const prolog = /^(?:\uFEFF|\s|<!--[\s\S]*?-->)*<!doctype[^>]*>/i.exec(html)?.[0] || "";
2909
+ parts = [html.slice(0, prolog.length), bridge, html.slice(prolog.length)];
2910
+ }
2911
+ return URL.createObjectURL(new Blob(parts, { type: "text/html;charset=utf-8" }));
2879
2912
  });
2880
2913
  }
2881
2914
 
@@ -3571,15 +3604,14 @@ const AssistantPreviewCard = React.memo(function AssistantPreviewCard({ line, th
3571
3604
  ? React.createElement(Runs, { runs: line.headerRuns, theme })
3572
3605
  : React.createElement(React.Fragment, null,
3573
3606
  React.createElement("span", { className: "ps-canvas-action-tag" }, "Agent"),
3574
- React.createElement("span", { className: "ps-system-notice-summary-text" }, "Message preview"),
3575
- React.createElement("span", { className: "ps-preview-status" },
3576
- line.isLive ? (line.body ? "Responding" : "Thinking") : "Saved"))),
3607
+ React.createElement("span", { className: "ps-system-notice-summary-text" }, line.text),
3608
+ line.statusText ? React.createElement("span", { className: "ps-preview-status" }, line.statusText) : null)),
3577
3609
  React.createElement("div", {
3578
3610
  ref: viewportRef,
3579
3611
  className: "ps-assistant-preview-viewport",
3580
3612
  tabIndex: expanded && !line.final ? 0 : undefined,
3581
3613
  role: line.final ? undefined : "region",
3582
- "aria-label": line.final ? undefined : "Agent message preview",
3614
+ "aria-label": line.final ? undefined : line.text,
3583
3615
  onScroll: (event) => {
3584
3616
  if (!line.final) {
3585
3617
  event.stopPropagation();
@@ -4868,7 +4900,7 @@ function useAxisLockedPan(ref, enabled = true) {
4868
4900
  }, [ref, enabled]);
4869
4901
  }
4870
4902
 
4871
- function SessionPane({ controller, actions = null, panelClassName = "", structuredRows = false, showDetailBox = null }) {
4903
+ function SessionPane({ controller, actions = null, panelClassName = "", structuredRows = false, showDetailBox = null, selection = null, actionsOnly = false, actionsHost = null, onAction = null, onDialogChange = null }) {
4872
4904
  // Mobile keeps its inline detail line and normally gets no detail box — a
4873
4905
  // reserved footer would eat a meaningful slice of a phone screen. The
4874
4906
  // sessions-ONLY layout is the exception: it has the whole screen and the
@@ -4894,23 +4926,23 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
4894
4926
  const sessionButtonRefs = React.useRef(new Map());
4895
4927
  const viewState = useControllerSelector(controller, (state) => ({
4896
4928
  branding: state.branding,
4897
- activeSessionId: state.sessions.activeSessionId,
4929
+ activeSessionId: selection ? selection.sessionId : state.sessions.activeSessionId,
4898
4930
  sessionsById: state.sessions.byId,
4899
4931
  sessionsFlat: state.sessions.flat,
4900
- filterQuery: state.sessions.filterQuery || "",
4932
+ filterQuery: selection ? selection.query || "" : state.sessions.filterQuery || "",
4901
4933
  ownerFilter: state.sessions.ownerFilter,
4902
4934
  pinnedIds: state.sessions.pinnedIds,
4903
4935
  manualOrder: state.sessions.manualOrder,
4904
- selectedIds: state.sessions.selectedIds,
4905
- selectMode: state.sessions.selectMode,
4936
+ selectedIds: selection ? [] : state.sessions.selectedIds,
4937
+ selectMode: selection ? false : state.sessions.selectMode,
4906
4938
  auth: state.auth,
4907
4939
  connectionMode: state.connection?.mode || "local",
4908
4940
  modalOpen: Boolean(state.ui.modal),
4909
- focused: state.ui.focusRegion === "sessions",
4941
+ focused: selection ? true : state.ui.focusRegion === "sessions",
4910
4942
  // Clicking empty space clears the list highlight; the row VM reads it,
4911
4943
  // so the reconstruction below must carry it or the click does nothing
4912
4944
  // visible (the same omission that blinded the Node Map).
4913
- listDeselected: Boolean(state.sessions.listDeselected),
4945
+ listDeselected: selection ? !selection.sessionId : Boolean(state.sessions.listDeselected),
4914
4946
  // The canvas markers read these. This synthetic state is exactly the
4915
4947
  // trap the branding comment below describes: omit a slice here and
4916
4948
  // every row computes as if it were empty — the markers were null on
@@ -4983,6 +5015,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
4983
5015
  setManageOpen(true);
4984
5016
  }
4985
5017
  }, [modelFlowOpen]);
5018
+ React.useEffect(() => { onDialogChange?.(manageOpen || Boolean(linkModal) || modelFlowOpen); }, [manageOpen, linkModal, modelFlowOpen, onDialogChange]);
4986
5019
  const requestSwitchModel = () => {
4987
5020
  reopenManageRef.current = true;
4988
5021
  setManageOpen(false);
@@ -5076,7 +5109,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5076
5109
  // rows/viewState from a ref that is refreshed on every render instead of
5077
5110
  // closing over them.
5078
5111
  const clickEnv = React.useRef(null);
5079
- clickEnv.current = { rows, viewState, controller };
5112
+ clickEnv.current = { rows, viewState, controller, selection };
5080
5113
  // ── Drag sessions into / out of groups ────────────────────────────
5081
5114
  // Pointer-driven so it works from <button> rows, can render a collection
5082
5115
  // ghost for a multi-selection, and can highlight the destination folder.
@@ -5401,7 +5434,12 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5401
5434
  }), [dragState.dragging, dragState.overGroupId, dragState.insertBeforeId, dragState.insertAfterId]);
5402
5435
 
5403
5436
  const handleRowClick = React.useCallback((event, row) => {
5404
- const { rows: currentRows, viewState: current, controller: ctl } = clickEnv.current;
5437
+ const { rows: currentRows, viewState: current, controller: ctl, selection: pick } = clickEnv.current;
5438
+ if (pick) {
5439
+ if (row.isGroup || (row.hasChildren && row.active)) ctl.dispatch({ type: row.collapsed ? "sessions/expand" : "sessions/collapse", sessionId: row.sessionId });
5440
+ if (!row.isGroup) pick.onSelect(row.sessionId);
5441
+ return;
5442
+ }
5405
5443
  // Cmd/Ctrl-click toggles multi-selection (any row).
5406
5444
  if (event.metaKey || event.ctrlKey) {
5407
5445
  event.preventDefault();
@@ -5516,14 +5554,14 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5516
5554
  }, [activeRowRevealKey, viewState.activeSessionId, viewState.focused, viewState.modalOpen, viewState.sessionsFlat]);
5517
5555
 
5518
5556
  const panelActions = React.createElement(React.Fragment, null,
5519
- isBulkSelection
5557
+ !actionsOnly && isBulkSelection
5520
5558
  ? React.createElement("span", {
5521
5559
  className: "ps-mini-button-label",
5522
5560
  style: { padding: "0 6px", fontSize: "12px", opacity: 0.85 },
5523
5561
  title: "Multiple sessions selected. Move to group or cancel selected sessions; click Clear to exit.",
5524
5562
  }, `${selectedCount} selected`)
5525
5563
  : null,
5526
- isBulkSelection
5564
+ !actionsOnly && (isBulkSelection
5527
5565
  ? React.createElement(IconButton, {
5528
5566
  className: "ps-mini-button",
5529
5567
  icon: "✕",
@@ -5539,8 +5577,8 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5539
5577
  label: canPinActiveSession
5540
5578
  ? (isActivePinned ? "Unpin this session" : "Pin this session to the top of the list")
5541
5579
  : "Only top-level non-system sessions can be pinned",
5542
- }),
5543
- React.createElement(IconButton, {
5580
+ })),
5581
+ !actionsOnly && React.createElement(IconButton, {
5544
5582
  className: "ps-mini-button",
5545
5583
  icon: groupableIds.length > 1
5546
5584
  ? React.createElement("span", { className: "ps-icon-badge" },
@@ -5581,9 +5619,9 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5581
5619
  ? "Rename folder"
5582
5620
  : !canModifyActiveSession
5583
5621
  ? "Only this session's owner or an admin can manage it"
5584
- : "Manage session — rename, switch model, and sharing",
5622
+ : actionsOnly ? "Manage session — rename and switch model" : "Manage session — rename, switch model, and sharing",
5585
5623
  }),
5586
- React.createElement(IconButton, {
5624
+ !actionsOnly && React.createElement(IconButton, {
5587
5625
  className: "ps-mini-button",
5588
5626
  icon: React.createElement(LinkGlyph),
5589
5627
  onClick: () => {
@@ -5643,16 +5681,31 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5643
5681
 
5644
5682
  return React.createElement(React.Fragment, null,
5645
5683
  dragGhost,
5646
- React.createElement(Panel, {
5684
+ actionsOnly ? (actionsHost ? createPortal(React.createElement("div", { className: "ps-moa-control-actions", onClick: onAction }, panelActions), actionsHost) : null) : React.createElement(Panel, {
5647
5685
  title: [{ text: "Sessions", color: "yellow", bold: true }],
5648
5686
  color: "yellow",
5649
5687
  focused: viewState.focused,
5650
5688
  theme,
5651
- actions: panelActions,
5689
+ actions: selection ? React.createElement(React.Fragment, null,
5690
+ selection.onCreate ? React.createElement(IconButton, {
5691
+ className: "ps-mini-button", icon: React.createElement(PlusGlyph),
5692
+ label: "Create New Session", onClick: selection.onCreate,
5693
+ }) : null, actions) : panelActions,
5652
5694
  className: combinedPanelClassName,
5653
5695
  },
5696
+ selection ? React.createElement("input", { className: "ps-modal-input", "aria-label": "Find a session", placeholder: "Find a session…", value: selection.query || "", onChange: e => selection.onQuery?.(e.target.value) }) : null,
5654
5697
  React.createElement("div", {
5655
5698
  ref: sessionListRef,
5699
+ onKeyDown: selection ? event => {
5700
+ if (!["ArrowUp", "ArrowDown", "Home", "End"].includes(event.key) || event.altKey || event.ctrlKey || event.metaKey) return;
5701
+ const buttons = [...sessionListRef.current.querySelectorAll(".ps-session-list-button")];
5702
+ const current = buttons.indexOf(event.target.closest(".ps-session-list-button"));
5703
+ const next = event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : Math.max(0, Math.min(buttons.length - 1, current + (event.key === "ArrowUp" ? -1 : 1)));
5704
+ const button = buttons[next]; if (!button) return;
5705
+ event.preventDefault(); event.stopPropagation();
5706
+ if (button.dataset.sessionId) selection.onSelect(button.dataset.sessionId);
5707
+ button.focus();
5708
+ } : undefined,
5656
5709
  className: `ps-action-list ps-session-list${dragState.dragging ? " is-dragging" : ""}`,
5657
5710
  // Clicking empty space below the rows clears the list selection while
5658
5711
  // the other panes keep showing the session. With nothing selected the
@@ -5661,9 +5714,11 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5661
5714
  // Empty space only: a click that lands on a row bubbles here too.
5662
5715
  const onRow = event.target instanceof Element && event.target.closest(".ps-session-list-button");
5663
5716
  if (onRow) return;
5664
- controller.dispatch({ type: "sessions/listDeselect" });
5717
+ if (selection) selection.onSelect(null);
5718
+ else controller.dispatch({ type: "sessions/listDeselect" });
5665
5719
  },
5666
5720
  },
5721
+
5667
5722
  rows.length === 0
5668
5723
  ? React.createElement("div", { className: "ps-empty-state" }, viewState.filterQuery
5669
5724
  ? `No sessions matched "@@${viewState.filterQuery}".`
@@ -5678,7 +5733,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5678
5733
  setRef: setSessionButtonRef,
5679
5734
  // Drag-to-folder is a fine-pointer gesture: arming it on
5680
5735
  // touch hijacks the finger that should be scrolling the list.
5681
- drag: touchInput ? null : dragHandlers,
5736
+ drag: selection || touchInput ? null : dragHandlers,
5682
5737
  }))),
5683
5738
  (showDetailBox === null ? !isMobilePane : showDetailBox)
5684
5739
  ? React.createElement(SessionDetailBox, {
@@ -5700,6 +5755,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
5700
5755
  controller,
5701
5756
  sessionId: activeSession.sessionId,
5702
5757
  initialTitle: activeSession.title || "",
5758
+ allowSharing: !actionsOnly,
5703
5759
  currentModel: activeSession.model || "",
5704
5760
  currentReasoningEffort: activeSession.reasoningEffort || "",
5705
5761
  principal: viewState.auth?.principal || null,
@@ -5898,6 +5954,13 @@ function SidebarGlyph() {
5898
5954
  // icon size without a label.
5899
5955
  // The Main pane, whatever layout it is in: a frame with a list rail and a
5900
5956
  // conversation beside it. Constant across the three-way cycle.
5957
+ // A tiled command surface: one tall pane beside two stacked panes.
5958
+ function MoaGlyph() {
5959
+ return React.createElement(Glyph, null,
5960
+ React.createElement("rect", { x: 3, y: 4, width: 7, height: 16, rx: 1 }),
5961
+ React.createElement("rect", { x: 13, y: 4, width: 8, height: 6.5, rx: 1 }),
5962
+ React.createElement("rect", { x: 13, y: 13.5, width: 8, height: 6.5, rx: 1 }));
5963
+ }
5901
5964
  function MainLayoutGlyph() {
5902
5965
  return React.createElement(Glyph, null,
5903
5966
  React.createElement("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }),
@@ -6082,7 +6145,7 @@ function useActiveSessionAccess(controller, activeSessionId, isGroup) {
6082
6145
  // rename, copy link, plus (for the owner/admin) sharing — visibility +
6083
6146
  // per-person grants. Fetches its own access snapshot so callers only pass the
6084
6147
  // session id + current title.
6085
- function SessionModifyModal({ controller, sessionId, initialTitle, currentModel, currentReasoningEffort, principal, onClose, onSwitchModel, onChanged }) {
6148
+ function SessionModifyModal({ controller, sessionId, initialTitle, currentModel, currentReasoningEffort, principal, onClose, onSwitchModel, onChanged, allowSharing = true }) {
6086
6149
  const [tab, setTab] = React.useState("general");
6087
6150
  const [access, setAccess] = React.useState(null);
6088
6151
  const [title, setTitle] = React.useState(initialTitle || "");
@@ -6101,20 +6164,20 @@ function SessionModifyModal({ controller, sessionId, initialTitle, currentModel,
6101
6164
  .then((a) => { if (!cancelled && a) { setAccess(a); setVisibility(a.visibility || "private"); } })
6102
6165
  .catch(() => {});
6103
6166
  // Member directory for name autocomplete (excludes synthetic principals).
6104
- if (typeof controller.transport.listKnownUsers === "function") {
6167
+ if (allowSharing && typeof controller.transport.listKnownUsers === "function") {
6105
6168
  controller.transport.listKnownUsers({ limit: 500 })
6106
6169
  .then((users) => { if (!cancelled) setDirectory(Array.isArray(users) ? users : []); })
6107
6170
  .catch(() => {});
6108
6171
  }
6109
6172
  return () => { cancelled = true; };
6110
- }, [controller, sessionId]);
6173
+ }, [controller, sessionId, allowSharing]);
6111
6174
 
6112
6175
  const loadShares = React.useCallback(() => {
6113
6176
  controller.transport.listSessionShares(sessionId)
6114
6177
  .then((rows) => { const list = Array.isArray(rows) ? rows : []; setShares(list); setDraftShares(list); })
6115
6178
  .catch(() => { setShares([]); setDraftShares([]); });
6116
6179
  }, [controller, sessionId]);
6117
- React.useEffect(() => { loadShares(); }, [loadShares]);
6180
+ React.useEffect(() => { if (allowSharing) loadShares(); }, [loadShares, allowSharing]);
6118
6181
 
6119
6182
  const run = async (fn) => {
6120
6183
  setBusy(true); setError(null);
@@ -6220,7 +6283,7 @@ function SessionModifyModal({ controller, sessionId, initialTitle, currentModel,
6220
6283
  // only rename/switch model on their own session.
6221
6284
  const tabs = [
6222
6285
  { id: "general", label: "General" },
6223
- ...(canManage ? [{ id: "access", label: "Access" }] : []),
6286
+ ...(allowSharing && canManage ? [{ id: "access", label: "Access" }] : []),
6224
6287
  ];
6225
6288
  const activeTab = tabs.some((t) => t.id === tab) ? tab : "general";
6226
6289
  // Regenerate is a change-this-session action, so it belongs here rather
@@ -6344,6 +6407,20 @@ function SessionModifyModal({ controller, sessionId, initialTitle, currentModel,
6344
6407
  error ? React.createElement("div", { className: "ps-share-error" }, error) : null));
6345
6408
  }
6346
6409
 
6410
+ function SessionComposer({ controller, onReadOnlyFocus = null }) {
6411
+ const modal = useControllerSelector(controller, state => state.ui.modal);
6412
+ const session = useControllerSelector(controller, state => state.sessions.byId[state.sessions.activeSessionId]);
6413
+ const { access } = useActiveSessionAccess(controller, session?.sessionId, session?.isGroup);
6414
+ const readOnly = session?.serviceKind || access?.canWrite === false;
6415
+ React.useLayoutEffect(() => { if (readOnly && !modal) onReadOnlyFocus?.(); }, [readOnly, modal, onReadOnlyFocus]);
6416
+ if (!session || session.isGroup || modal) return null;
6417
+ return React.createElement("div", { className: "ps-chat-composer" }, readOnly
6418
+ ? React.createElement("div", { className: "ps-composer-readonly" }, session.serviceKind
6419
+ ? "⚗ Service session — runtime machinery. Its transcript is a read-only trace; it does not accept messages."
6420
+ : `You have view access to this session. Ask ${access.owner?.displayName || access.owner?.email || "the owner"} for write access to participate.`)
6421
+ : React.createElement(PromptComposer, { controller, mobile: false, active: true }));
6422
+ }
6423
+
6347
6424
  function ChatPane({ controller, mobile = false, fullWidth = false, showComposer = true }) {
6348
6425
  const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
6349
6426
  const theme = getTheme(themeId);
@@ -7233,7 +7310,9 @@ function RestoreGlyph() {
7233
7310
  * loads hidden-but-painted behind the live frame and is promoted on load, so
7234
7311
  * a live redraw is a composite swap of something already drawn.
7235
7312
  */
7236
- function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible = true, focusOnPromote = false, dataRev = 0, dataPayload = null, dataPatch = null }) {
7313
+ function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible = true, focusOnPromote = false, dataRev = 0, dataPayload = null, dataPatch = null, onPanelKey = null }) {
7314
+ const panelKeyRef = React.useRef(onPanelKey); panelKeyRef.current = onPanelKey;
7315
+ const panelKeys = Boolean(onPanelKey);
7237
7316
  const [live, setLive] = React.useState(null); // { url, rev }
7238
7317
  const [staging, setStaging] = React.useState(null); // { url, rev }
7239
7318
  const [loadError, setLoadError] = React.useState(null); // { rev }
@@ -7285,9 +7364,13 @@ function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible
7285
7364
  // window that asked). Writes and actions stay live-frame only.
7286
7365
  const fromStaging = Boolean(stagingIframeElRef.current && event.source === stagingIframeElRef.current.contentWindow);
7287
7366
  if (!fromLive && !fromStaging) return;
7367
+ if (payload.type === "moa-panel-key") {
7368
+ if (fromLive && document.activeElement === frameEl && ["Tab", "Escape"].includes(payload.key)) panelKeyRef.current?.(payload.key, payload.backwards === true);
7369
+ return;
7370
+ }
7288
7371
  if (payload.type === "canvas-action") {
7289
7372
  if (!fromLive) return;
7290
- controller.submitCanvasAction(sessionId, payload).then((result) => {
7373
+ controller.submitCanvasAction(sessionId, payload, slot).then((result) => {
7291
7374
  if (result && result.ok === false && typeof console !== "undefined") {
7292
7375
  console.warn(`canvas action rejected: ${result.reason}`);
7293
7376
  }
@@ -7372,7 +7455,7 @@ function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible
7372
7455
  if (!controller || !sessionId || !latestRev) return undefined;
7373
7456
  let cancelled = false;
7374
7457
  let retryTimer = null;
7375
- fetchArtifactHtmlObjectUrl(controller, sessionId, slot <= 1 ? "canvas.html" : `canvas${slot}.html`)
7458
+ fetchArtifactHtmlObjectUrl(controller, sessionId, slot <= 1 ? "canvas.html" : `canvas${slot}.html`, panelKeys)
7376
7459
  .then((url) => {
7377
7460
  if (cancelled) { URL.revokeObjectURL(url); return; }
7378
7461
  // A staging revision that never loaded is superseded here —
@@ -7400,7 +7483,7 @@ function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible
7400
7483
  cancelled = true;
7401
7484
  if (retryTimer) clearTimeout(retryTimer);
7402
7485
  };
7403
- }, [controller, sessionId, slot, latestRev, retryTick]);
7486
+ }, [controller, sessionId, slot, latestRev, retryTick, panelKeys]);
7404
7487
 
7405
7488
  // Session switch / unmount: drop both frames — a different session is a
7406
7489
  // different document, and showing the old canvas under a new session is a
@@ -8605,7 +8688,7 @@ function formatAttachmentSize(sizeBytes) {
8605
8688
  return `${bytes} B`;
8606
8689
  }
8607
8690
 
8608
- function PromptComposer({ controller, mobile, active = true, onAfterSend = null }) {
8691
+ function PromptComposer({ controller, mobile, active = true, onAfterSend = null, autoFocus = true }) {
8609
8692
  const promptState = useControllerSelector(controller, (state) => {
8610
8693
  const activeSessionId = state.sessions.activeSessionId;
8611
8694
  const activeSession = activeSessionId ? state.sessions.byId[activeSessionId] || null : null;
@@ -8634,6 +8717,20 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
8634
8717
  const inputRef = React.useRef(null);
8635
8718
  const attachInputRef = React.useRef(null);
8636
8719
  const [dragOver, setDragOver] = React.useState(false);
8720
+ const selectedQueued = promptState.selectedOutboxPhase === "queued";
8721
+ const selectedCancelling = promptState.selectedOutboxPhase === "cancelling";
8722
+ const selectedReadOnly = selectedQueued || selectedCancelling;
8723
+ const placeholder = promptState.answerMode
8724
+ ? "Type an answer and press Enter"
8725
+ : promptState.editingPending
8726
+ ? selectedCancelling
8727
+ ? "Cancellation requested"
8728
+ : selectedQueued
8729
+ ? "Queued message selected"
8730
+ : "Edit the pending message, then send or cancel it"
8731
+ : promptState.hasOutbox
8732
+ ? "Type a message and press Enter to queue it behind the pending batch"
8733
+ : "Type a message and press Enter";
8637
8734
 
8638
8735
  // Auto-grow: one line idle, sized to the RENDERED content (scrollHeight
8639
8736
  // sees soft wrap; counting "\n" does not). CSS max-height provides the
@@ -8661,7 +8758,10 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
8661
8758
  const mayHaveShrunk = length <= lastPromptLengthRef.current;
8662
8759
  lastPromptLengthRef.current = length;
8663
8760
  growInput(mayHaveShrunk || length === 0);
8664
- }, [growInput, promptState.value]);
8761
+ // Empty textareas measure their placeholder too. Send briefly shows
8762
+ // the long outbox hint; acknowledgement shortens it without changing
8763
+ // the value or width, so neither input nor ResizeObserver will fire.
8764
+ }, [growInput, promptState.value, placeholder]);
8665
8765
  React.useEffect(() => {
8666
8766
  // Width changes re-wrap the content; re-measure on viewport resizes
8667
8767
  // (covers rotation and the on-screen keyboard shrinking the pane).
@@ -8724,6 +8824,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
8724
8824
  const inputNode = inputRef.current;
8725
8825
  if (!active || promptState.modalOpen || !promptState.focused || !inputNode) return;
8726
8826
  if (document.activeElement !== inputNode) {
8827
+ if (!autoFocus) return;
8727
8828
  // Programmatic focus pops the on-screen keyboard on touch devices;
8728
8829
  // there, focus only ever comes from the user's own tap.
8729
8830
  if (mobile) return;
@@ -8743,7 +8844,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
8743
8844
  if (inputNode.value === promptState.value && inputNode.selectionStart !== promptState.cursor) {
8744
8845
  inputNode.setSelectionRange(promptState.cursor, promptState.cursor);
8745
8846
  }
8746
- }, [active, mobile, promptState.cursor, promptState.value, promptState.focused, promptState.modalOpen]);
8847
+ }, [active, mobile, autoFocus, promptState.cursor, promptState.value, promptState.focused, promptState.modalOpen]);
8747
8848
 
8748
8849
  const sendPrompt = React.useCallback(() => {
8749
8850
  controller.handleCommand(UI_COMMANDS.SEND_PROMPT)
@@ -8780,9 +8881,6 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
8780
8881
  : promptState.hasOutbox
8781
8882
  ? "+"
8782
8883
  : "❯";
8783
- const selectedQueued = promptState.selectedOutboxPhase === "queued";
8784
- const selectedCancelling = promptState.selectedOutboxPhase === "cancelling";
8785
- const selectedReadOnly = selectedQueued || selectedCancelling;
8786
8884
 
8787
8885
  return React.createElement("div", {
8788
8886
  className: `ps-prompt-shell${mobile ? " is-mobile" : ""}${dragOver ? " is-drag-over" : ""}`,
@@ -8852,17 +8950,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
8852
8950
  rows: 1,
8853
8951
  value: promptState.value,
8854
8952
  readOnly: selectedReadOnly,
8855
- placeholder: promptState.answerMode
8856
- ? "Type an answer and press Enter"
8857
- : promptState.editingPending
8858
- ? selectedCancelling
8859
- ? "Cancellation requested"
8860
- : selectedQueued
8861
- ? "Queued message selected"
8862
- : "Edit the pending message, then send or cancel it"
8863
- : promptState.hasOutbox
8864
- ? "Type a message and press Enter to queue it behind the pending batch"
8865
- : "Type a message and press Enter",
8953
+ placeholder,
8866
8954
  // On touch, Enter inserts a newline (the chevron sends) — the
8867
8955
  // keyboard must not advertise a send that will not happen.
8868
8956
  enterKeyHint: mobile ? "enter" : "send",
@@ -9135,7 +9223,7 @@ function IconButton({ icon, label, onClick, disabled = false, active = false, cl
9135
9223
  tooltipNode);
9136
9224
  }
9137
9225
 
9138
- function Toolbar({ controller, mobile, canvasPaneOpen = false, onToggleCanvasPane = null, mobilePane = "workspace", onSelectMobilePane = null, mobileMainLayout = "split" }) {
9226
+ function Toolbar({ controller, mobile, moa = null, canvasPaneOpen = false, onToggleCanvasPane = null, mobilePane = "workspace", onSelectMobilePane = null, mobileMainLayout = "split" }) {
9139
9227
  const [headerSlot, setHeaderSlot] = React.useState(null);
9140
9228
  React.useEffect(() => {
9141
9229
  if (typeof document === "undefined") return;
@@ -9274,10 +9362,21 @@ function Toolbar({ controller, mobile, canvasPaneOpen = false, onToggleCanvasPan
9274
9362
  icon: React.createElement(MainLayoutGlyph),
9275
9363
  label: "Workspace — sessions, chat and panels",
9276
9364
  onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_WORKSPACE).catch(() => {}),
9277
- active: !adminVisible && !budgetOpen,
9365
+ active: !moa?.active && !adminVisible && !budgetOpen,
9278
9366
  }]),
9279
9367
  ];
9280
9368
 
9369
+ if (!mobile && moa?.desktop) buttonDefs.push({
9370
+ key: "moa", icon: React.createElement(MoaGlyph),
9371
+ label: moa.returnTo ? "Back to MoA — Master of Agents" : "Master of Agents",
9372
+ active: moa.active, disabled: !moa.loaded,
9373
+ onClick: () => {
9374
+ controller.dispatch({ type: "ui/canvasMaximized", on: false });
9375
+ controller.handleCommand(UI_COMMANDS.OPEN_WORKSPACE).catch(() => {});
9376
+ moa.open();
9377
+ },
9378
+ });
9379
+
9281
9380
  const renderButton = (def) => {
9282
9381
  const button = React.createElement(IconButton, {
9283
9382
  key: def.badge ? undefined : def.key,
@@ -9355,10 +9454,10 @@ function Toolbar({ controller, mobile, canvasPaneOpen = false, onToggleCanvasPan
9355
9454
  // mode. Budget and the Admin Console replace the workspace; nothing on
9356
9455
  // the left applies there. Modes are exclusive (the controller closes one
9357
9456
  // when the other opens) and the Workspace button is the way back.
9358
- const mode = adminVisible ? "admin" : (budgetOpen ? "budget" : "workspace");
9457
+ const mode = moa?.active ? "moa" : adminVisible ? "admin" : (budgetOpen ? "budget" : "workspace");
9359
9458
  const ACTIONS = ["new", "filter"];
9360
9459
  const PANELS = ["canvas", "diagnostics"];
9361
- const MODES = ["workspace", "budget", "admin"];
9460
+ const MODES = ["workspace", "moa", "budget", "admin"];
9362
9461
 
9363
9462
  // While the canvas is full screen, ANY other button first drops full
9364
9463
  // screen and then does its own job. Pressing Filter and watching nothing
@@ -9366,10 +9465,11 @@ function Toolbar({ controller, mobile, canvasPaneOpen = false, onToggleCanvasPan
9366
9465
  // it — is the confusing half of every full-screen mode. The Canvas toggle
9367
9466
  // is exempt: it already means "put the canvas away", and closing drops
9368
9467
  // the flag in the reducer.
9369
- const withRestore = (def) => (!canvasMaximized || def.key === "canvas" ? def : {
9468
+ const withRestore = (def) => ((!canvasMaximized || def.key === "canvas") && !(moa?.active && ["workspace", "budget", "admin"].includes(def.key)) ? def : {
9370
9469
  ...def,
9371
9470
  onClick: (...args) => {
9372
9471
  controller.dispatch({ type: "ui/canvasMaximized", on: false });
9472
+ if (moa?.active && ["workspace", "budget", "admin"].includes(def.key)) moa.leave();
9373
9473
  return def.onClick?.(...args);
9374
9474
  },
9375
9475
  });
@@ -9390,14 +9490,14 @@ function Toolbar({ controller, mobile, canvasPaneOpen = false, onToggleCanvasPan
9390
9490
  ...pick(["theme"]),
9391
9491
  ];
9392
9492
 
9393
- const toolbar = React.createElement("div", { className: "ps-toolbar" },
9493
+ const toolbar = React.createElement("div", { className: `ps-toolbar${moa?.active ? " is-moa" : ""}` },
9394
9494
  // Three columns: left rail | main controls | right rail. Full-screen
9395
9495
  // canvas uses the otherwise-empty left rail for the normal actions so
9396
9496
  // they do not crowd the canvas metadata and controls across the top.
9397
9497
  React.createElement("div", { className: "ps-toolbar-side is-left" },
9398
- canvasMaximized ? leftCluster : null),
9498
+ moa?.active ? React.createElement("div", { id: "ps-moa-status-slot" }) : canvasMaximized ? leftCluster : null),
9399
9499
  React.createElement("div", { className: "ps-toolbar-actions" },
9400
- canvasMaximized ? null : leftCluster),
9500
+ moa?.active ? React.createElement("div", { id: "ps-moa-header-slot" }) : canvasMaximized ? null : leftCluster),
9401
9501
  React.createElement("div", { className: "ps-toolbar-side is-right" },
9402
9502
  // The portal header parks its version/status meta here, LEFT of
9403
9503
  // the tool buttons, and its sign-out glyph in the slot after
@@ -12596,8 +12696,17 @@ function DistillerModelPickers({ controller, extras }) {
12596
12696
  );
12597
12697
  }
12598
12698
 
12599
- function useKeyboardShortcuts(controller, mobile) {
12699
+ function ScopedModalLayer({ controller }) {
12700
+ const modal = useControllerSelector(controller, state => state.ui.modal);
12701
+ // Isolated MoA controllers own keyboard commands only while their dialog
12702
+ // is open. They must never install normal-workspace shortcuts globally.
12703
+ useKeyboardShortcuts(controller, false, !modal);
12704
+ return React.createElement(ModalLayer, { controller });
12705
+ }
12706
+
12707
+ function useKeyboardShortcuts(controller, mobile, suspended = false) {
12600
12708
  React.useEffect(() => {
12709
+ if (suspended) return undefined;
12601
12710
  const handler = (event) => {
12602
12711
  // A focused control that handled the key owns it. The diagnostics
12603
12712
  // seam answers ArrowUp/ArrowDown itself; letting the same keydown
@@ -12942,7 +13051,7 @@ function useKeyboardShortcuts(controller, mobile) {
12942
13051
 
12943
13052
  window.addEventListener("keydown", handler);
12944
13053
  return () => window.removeEventListener("keydown", handler);
12945
- }, [controller, mobile]);
13054
+ }, [controller, mobile, suspended]);
12946
13055
  }
12947
13056
 
12948
13057
  function formatAdminPrincipalLabel(principal) {
@@ -14143,11 +14252,11 @@ export function createWebPilotSwarmController({ transport, mode = "remote", bran
14143
14252
  return new PilotSwarmUiController({ store, transport });
14144
14253
  }
14145
14254
 
14146
- export function PilotSwarmWebApp({ controller }) {
14255
+ export function PilotSwarmWebApp({ controller, suspended = false, moa = null }) {
14147
14256
  const viewportRef = React.useRef(null);
14148
14257
  const mainGridRef = React.useRef(null);
14149
- const viewport = useMeasuredViewport(viewportRef);
14150
- const mainGridViewport = useMeasuredViewport(mainGridRef);
14258
+ const viewport = useMeasuredViewport(viewportRef, suspended);
14259
+ const mainGridViewport = useMeasuredViewport(mainGridRef, suspended);
14151
14260
  const gridViewport = computeGridViewport(viewport);
14152
14261
 
14153
14262
  // Publish the real viewport in character cells. Without this the ui-core
@@ -14209,6 +14318,7 @@ export function PilotSwarmWebApp({ controller }) {
14209
14318
  // toolbar button). Desktop has real columns and needs no such cycle.
14210
14319
  const [mobileMainLayout, setMobileMainLayout] = React.useState("split");
14211
14320
  const state = useControllerSelector(controller, (rootState) => ({
14321
+ moa: rootState.ui.moa,
14212
14322
  themeId: rootState.ui.themeId,
14213
14323
  touchScale: Boolean(rootState.ui.touchScale),
14214
14324
  sessionDetailCollapsed: Boolean(rootState.ui.sessionDetailCollapsed),
@@ -14224,6 +14334,7 @@ export function PilotSwarmWebApp({ controller }) {
14224
14334
  // Conversion to a sorted array happens inside `normalizeProfileSettings`.
14225
14335
  collapsedSessionIds: rootState.sessions.collapsedIds,
14226
14336
  activeSessionId: rootState.sessions.activeSessionId || null,
14337
+ revealedCreatedSessionId: rootState.ui.revealedCreatedSessionId || null,
14227
14338
  promptRows: getStatePromptRows(rootState),
14228
14339
  rightPaneMode: rootState.ui.rightPaneMode || "panes",
14229
14340
  // The two optional desktop columns, independent of each other.
@@ -14278,7 +14389,21 @@ export function PilotSwarmWebApp({ controller }) {
14278
14389
  const readOnlyChatPane = state.activeSessionIsGroup;
14279
14390
  const effectivePromptRows = readOnlyChatPane ? 0 : state.promptRows;
14280
14391
 
14281
- useKeyboardShortcuts(controller, mobile);
14392
+ useKeyboardShortcuts(controller, mobile, suspended);
14393
+
14394
+ const lastCreatedSessionRef = React.useRef(state.revealedCreatedSessionId);
14395
+ React.useEffect(() => {
14396
+ if (lastCreatedSessionRef.current === state.revealedCreatedSessionId) return;
14397
+ lastCreatedSessionRef.current = state.revealedCreatedSessionId;
14398
+ if (!state.revealedCreatedSessionId || state.activeSessionId !== state.revealedCreatedSessionId) return;
14399
+ if (mobile) {
14400
+ setMobilePane("workspace");
14401
+ setMobileMainLayout("chat");
14402
+ }
14403
+ // Creation opens the ordinary session workspace, without modifying
14404
+ // any of the user's saved Master of Agents panels.
14405
+ if (moa?.active) moa.leave();
14406
+ }, [state.revealedCreatedSessionId, state.activeSessionId, mobile, moa]);
14282
14407
 
14283
14408
  // Tell the reducer which device slot of the per-session views this tab
14284
14409
  // owns. Only a desktop applies and records them; a phone keeps its
@@ -14440,7 +14565,7 @@ export function PilotSwarmWebApp({ controller }) {
14440
14565
  );
14441
14566
  const settingsJson = JSON.stringify(settings);
14442
14567
  const currentSettingsBeforeApply = profileSettingsFromViewState(
14443
- controller.getState(), otherTouchScaleRef.current, desktopRightPaneModeRef.current, desktopPanesRef.current,
14568
+ profileViewState(controller.getState()), otherTouchScaleRef.current, desktopRightPaneModeRef.current, desktopPanesRef.current,
14444
14569
  );
14445
14570
  const currentSettingsBeforeApplyJson = JSON.stringify(currentSettingsBeforeApply);
14446
14571
  const hasUnpersistedLocalChange = profileSettingsHydratedRef.current
@@ -14448,7 +14573,8 @@ export function PilotSwarmWebApp({ controller }) {
14448
14573
  && currentSettingsBeforeApplyJson !== lastProfileSettingsJsonRef.current;
14449
14574
  const hasPendingLocalWrite = Boolean(profileSettingsSaveTimerRef.current)
14450
14575
  || profileSettingsSaveInFlightRef.current
14451
- || hasUnpersistedLocalChange;
14576
+ || hasUnpersistedLocalChange
14577
+ || controller.getState().ui.moaDirty === true;
14452
14578
  if (!hasPendingLocalWrite && appliedProfileSettingsJsonRef.current !== settingsJson) {
14453
14579
  controller.dispatch({ type: "profileSettings/apply", settings });
14454
14580
  appliedProfileSettingsJsonRef.current = settingsJson;
@@ -14483,7 +14609,7 @@ export function PilotSwarmWebApp({ controller }) {
14483
14609
  // remote state by definition and there is nothing to lose).
14484
14610
  if (!profileSettingsHydratedRef.current || !hasPendingLocalWrite) {
14485
14611
  const currentSettings = profileSettingsFromViewState(
14486
- controller.getState(), otherTouchScaleRef.current, desktopRightPaneModeRef.current, desktopPanesRef.current,
14612
+ profileViewState(controller.getState()), otherTouchScaleRef.current, desktopRightPaneModeRef.current, desktopPanesRef.current,
14487
14613
  );
14488
14614
  lastProfileSettingsJsonRef.current = JSON.stringify(currentSettings);
14489
14615
  }
@@ -14527,14 +14653,20 @@ export function PilotSwarmWebApp({ controller }) {
14527
14653
  profileSettingsSaveTimerRef.current = setTimeout(() => {
14528
14654
  profileSettingsSaveTimerRef.current = null;
14529
14655
  profileSettingsSaveInFlightRef.current = true;
14656
+ const moaRevision = controller.getState().ui.moaRevision;
14657
+ controller.dispatch({ type: "ui/moaSaveStatus", status: "saving" });
14530
14658
  saveProfileSettings(controller, settings)
14531
- .catch(() => {})
14659
+ .then(() => controller.dispatch({ type: "ui/moaSaveStatus", status: "saved", revision: moaRevision }))
14660
+ .catch(() => {
14661
+ lastProfileSettingsJsonRef.current = null;
14662
+ controller.dispatch({ type: "ui/moaSaveStatus", status: "error" });
14663
+ })
14532
14664
  .finally(() => {
14533
14665
  profileSettingsSaveInFlightRef.current = false;
14534
14666
  });
14535
14667
  }, 400);
14536
14668
  return undefined;
14537
- }, [controller, state.activeSessionId, state.activityPaneAdjust, state.agentPickerUsage, state.canvasOpen, state.canvasPaneAdjust, state.canvasPrefs, state.canvasZen, state.collapsedSessionIds, state.diagnosticsOpen, state.diagnosticsPaneAdjust, state.diagnosticsSplitAdjust, state.ownerFilter, state.paneAdjust, state.manualOrder, state.pinnedIds, state.portalSessionColumnAdjust, state.rightPaneMode, state.sessionDetailCollapsed, state.sessionPaneAdjust, state.sessionViews, state.themeId, state.touchScale]);
14669
+ }, [controller, state.moa, state.activeSessionId, state.activityPaneAdjust, state.agentPickerUsage, state.canvasOpen, state.canvasPaneAdjust, state.canvasPrefs, state.canvasZen, state.collapsedSessionIds, state.diagnosticsOpen, state.diagnosticsPaneAdjust, state.diagnosticsSplitAdjust, state.ownerFilter, state.paneAdjust, state.manualOrder, state.pinnedIds, state.portalSessionColumnAdjust, state.rightPaneMode, state.sessionDetailCollapsed, state.sessionPaneAdjust, state.sessionViews, state.themeId, state.touchScale]);
14538
14670
 
14539
14671
  React.useEffect(() => {
14540
14672
  applyDocumentTheme(state.themeId);
@@ -14645,7 +14777,7 @@ export function PilotSwarmWebApp({ controller }) {
14645
14777
  Number(state.canvasPrefs?.[state.activeSessionId]?.zenRailPx) || 380));
14646
14778
  // Escape steps down one rung: full screen -> zen -> normal workspace.
14647
14779
  React.useEffect(() => {
14648
- if (!canvasMaximized && !zenActive) return undefined;
14780
+ if (suspended || (!canvasMaximized && !zenActive)) return undefined;
14649
14781
  const onKey = (e) => {
14650
14782
  if (e.key !== "Escape") return;
14651
14783
  controller.dispatch(canvasMaximized
@@ -14654,7 +14786,7 @@ export function PilotSwarmWebApp({ controller }) {
14654
14786
  };
14655
14787
  window.addEventListener("keydown", onKey);
14656
14788
  return () => window.removeEventListener("keydown", onKey);
14657
- }, [controller, canvasMaximized, zenActive]);
14789
+ }, [controller, canvasMaximized, zenActive, suspended]);
14658
14790
 
14659
14791
  // The right block is exactly as wide as the columns in it. Pixels, not a
14660
14792
  // share of the window: when a column closes, its pixels go to chat and
@@ -14877,13 +15009,16 @@ export function PilotSwarmWebApp({ controller }) {
14877
15009
  })
14878
15010
  : null;
14879
15011
 
15012
+ if (suspended) return React.createElement(ControllerContext.Provider, { value: controller },
15013
+ React.createElement(Toolbar, { controller, mobile: false, moa }),
15014
+ React.createElement(ModalLayer, { controller }));
14880
15015
  return React.createElement(ControllerContext.Provider, { value: controller },
14881
15016
  React.createElement("div", { ref: viewportRef, className: "ps-web-shell" },
14882
15017
  // The phone's toolbar carries its NAVIGATION (the three view modes,
14883
15018
  // plus the Main layout cycle), so it renders on every pane — hiding it
14884
15019
  // anywhere would trap the user with no way back.
14885
15020
  React.createElement(Toolbar, {
14886
- controller,
15021
+ controller, moa,
14887
15022
  mobile,
14888
15023
  canvasPaneOpen: mobileCanvasOpen,
14889
15024
  onToggleCanvasPane: toggleMobileCanvas,
@@ -14921,3 +15056,6 @@ export function PilotSwarmWebApp({ controller }) {
14921
15056
  : null,
14922
15057
  React.createElement(ModalLayer, { controller })));
14923
15058
  }
15059
+
15060
+ // Browser hosts may compose these existing surfaces with isolated controllers.
15061
+ export { ChatPane, CanvasFrame, SessionPane, SessionRowContent, SessionDetailBox, SessionComposer, ScopedModalLayer, ControllerContext };