pilotswarm 0.5.59 → 0.5.61
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.
- package/README.md +7 -0
- package/package.json +2 -2
- package/ui/core/src/controller.js +56 -13
- package/ui/core/src/history.js +15 -1
- package/ui/core/src/index.js +2 -0
- package/ui/core/src/moa.js +47 -0
- package/ui/core/src/reducer.js +70 -49
- package/ui/core/src/selectors.js +27 -13
- package/ui/core/src/session-errors.js +97 -0
- package/ui/core/src/state.js +1 -0
- package/ui/react/src/web-app.js +327 -136
- package/web/dist/assets/{abnfDiagram-VRR7QNED-D9Rv2VpV.js → abnfDiagram-VRR7QNED-CCVkGWsr.js} +1 -1
- package/web/dist/assets/{arc-BATiucnq.js → arc-y040btO3.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-CSTMt0si.js → architectureDiagram-ZJ3FMSHR-24Yt5esy.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-C_RGqfeY.js → blockDiagram-677ZJIJ3-BlbS-_GI.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-DH__s__0.js → c4Diagram-LMCZKHZV-_mamn0Fv.js} +1 -1
- package/web/dist/assets/channel-COFs9dqk.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-C197fD3B.js → chunk-2Q5K7J3B-TCbIB2Ni.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-BjpuBroU.js → chunk-32BRIVSS-C9q5KNKG.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-W_jL8euj.js → chunk-5VM5RSS4-BvoqfU8P.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-a9h0dtlp.js → chunk-EX3LRPZG-zFJjZeNd.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-CQN0qE7F.js → chunk-JWPE2WC7-Ce_6C5M-.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-D-Y6KQ7t.js → chunk-MOJQB5TN-CPjpLM65.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-jPwseC5f.js → chunk-RYQCIY6F-CsmVUSDH.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-DRNCEcQh.js → chunk-V7JOEXUC-CgrmPPOd.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-DzhUcoZ7.js → chunk-VR4S4FIN-BxAHDR7h.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-ldJfyPXK.js → chunk-XXDRQBXY-BGfFoO5_.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-B5-KmsCx.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-B5-KmsCx.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-BBVo21wR.js → cose-bilkent-JH36ORCC-B6KcngJW.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-CgRQfdW1.js → cynefin-VYW2F7L2-DaY7l4Ce.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-Ckz2X0t3.js → cynefinDiagram-TSTJHNR4-BldfGsoa.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-C1SanJiY.js → dagre-VKFMJZFB-C4T9X5Bs.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-dBdEifVy.js → diagram-FQU43EPY-DCVA_t-e.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-Byqc1VCB.js → diagram-G47NLZAW-pbTZGgE5.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-9aeTaCsX.js → diagram-NH7WQ7WH-P8Iks122.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-CL0a0o0k.js → diagram-OA4YK3LP-hEjZPipp.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-YlMQOJfY.js → diagram-WEI45ONY-CVAzVCGu.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-qMZQEnws.js → ebnfDiagram-CCIWWBDH-BLL398qt.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-C3CWBCtq.js → erDiagram-Q63AITRT-CLTADyUG.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-BN79sybL.js → flowDiagram-23GEKE2U-BAdlWAt0.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-BOWhiAgB.js → ganttDiagram-NO4QXBWP-C7Z2wavr.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-CKX9OACp.js → gitGraphDiagram-IHSO6WYX-BI546pYu.js} +1 -1
- package/web/dist/assets/index-CvW-4eXF.css +1 -0
- package/web/dist/assets/index-DsPaDrf9.js +24 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-QQEunPNs.js → infoDiagram-FWYZ7A6U-8C7iQydo.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-CvMwrnLQ.js → ishikawaDiagram-FXEZZL3T-Co4STVxQ.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-giNpEVpj.js → journeyDiagram-5HDEW3XC-DbUt11q-.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-CUPPpo27.js → kanban-definition-HUTT4EX6-DUNcFmme.js} +1 -1
- package/web/dist/assets/{linear-AXqadfOR.js → linear-Cjaxj94d.js} +1 -1
- package/web/dist/assets/{mermaid.core-C78wdk8Y.js → mermaid.core-B3cPFf4f.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-BGUmh9bK.js → mindmap-definition-LN4V7U3C-DaZtxB4N.js} +1 -1
- package/web/dist/assets/{papaparse.min-Bh8RLCk_.js → papaparse.min-vfdso2fs.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-BHsKXKhK.js → pegDiagram-2B236MQR-DkBpYV7m.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-D3EvWTdH.js → pieDiagram-ENE6RG2P-By3tI-vc.js} +1 -1
- package/web/dist/assets/pilotswarm-BL_NbZug.js +98 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-CbkxfLkV.js → quadrantDiagram-ABIIQ3AL-DZmVm0xK.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-H4M6JSDW.js → railroadDiagram-RFXS5EU6-BC5Mbz7q.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-BJo7JW1v.js → requirementDiagram-TGXJPOKE-CyZNX_O1.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-DL19MeOP.js → sankeyDiagram-HTMAVEWB-7603NEmu.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-DyVFc5pQ.js → sequenceDiagram-DBY2YBRQ-CXJ-i2Xj.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-CKbcKIEQ.js → sizeCapture-X5ZJPWSS-CKT0ATnS.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-BSukSXzS.js → stateDiagram-2N3HPSRC-BIDPX4YU.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-Dr8oD9T6.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-BUd7UhsF.js → swimlanes-5IMT3BWC-CPoy6vnO.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BV0EQRlB.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-Dt1gludn.js → timeline-definition-FHXFAJF6-BGdNkplk.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-BHxt00GV.js → vennDiagram-L72KCM5P-ByFzarUb.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-BT3fIDbe.js → wardleyDiagram-EHGQE667-CAk0SYuv.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-ChoYDS2b.js → xychartDiagram-FW5EYKEG-Np7h6_A0.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/dist/assets/channel-CqgY9oZh.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-Uz4DYZs2.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-Uz4DYZs2.js +0 -1
- package/web/dist/assets/index-B4s2lTsK.js +0 -24
- package/web/dist/assets/index-DhGdE2Nd.css +0 -1
- package/web/dist/assets/pilotswarm-UV6T1_QI.js +0 -98
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-BhKG3pJB.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-CgoFkXJE.js +0 -8
package/ui/react/src/web-app.js
CHANGED
|
@@ -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,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
selectArtifactPickerModal,
|
|
36
37
|
selectArtifactUploadModal,
|
|
37
38
|
selectLiveActivityLines,
|
|
39
|
+
selectActiveOutboxMessages,
|
|
38
40
|
selectChatLines,
|
|
39
41
|
selectChatPaneChrome,
|
|
40
42
|
selectOutboxOverlayLines,
|
|
@@ -286,6 +288,7 @@ function clearBrowserPreferenceCache() {
|
|
|
286
288
|
function normalizeProfileSettings(settings) {
|
|
287
289
|
const candidate = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
|
|
288
290
|
const normalized = {};
|
|
291
|
+
if (hasOwn(candidate, "moa")) normalized.moa = normalizeMoa(candidate.moa);
|
|
289
292
|
if (typeof candidate.themeId === "string" && candidate.themeId.trim()) {
|
|
290
293
|
normalized.themeId = candidate.themeId.trim();
|
|
291
294
|
}
|
|
@@ -431,8 +434,18 @@ function hasOwn(value, key) {
|
|
|
431
434
|
return Boolean(value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key));
|
|
432
435
|
}
|
|
433
436
|
|
|
437
|
+
function profileViewState(root) {
|
|
438
|
+
return {
|
|
439
|
+
...root.ui, ...root.ui.layout,
|
|
440
|
+
ownerFilter: root.sessions.ownerFilter, pinnedIds: root.sessions.pinnedIds,
|
|
441
|
+
manualOrder: root.sessions.manualOrder, collapsedSessionIds: root.sessions.collapsedIds,
|
|
442
|
+
activeSessionId: root.sessions.activeSessionId, canvasPrefs: root.canvas?.prefs,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
434
446
|
function profileSettingsFromViewState(state, preservedOtherTouchScale = null, preservedRightPaneMode = null, preservedDesktopPanes = null) {
|
|
435
447
|
return normalizeProfileSettings({
|
|
448
|
+
...(state.moa ? { moa: state.moa } : {}),
|
|
436
449
|
themeId: state.themeId,
|
|
437
450
|
[touchScaleKey()]: state.touchScale,
|
|
438
451
|
sessionDetailCollapsed: state.sessionDetailCollapsed,
|
|
@@ -498,6 +511,7 @@ function profileSettingsFromViewState(state, preservedOtherTouchScale = null, pr
|
|
|
498
511
|
|
|
499
512
|
function buildDefaultProfileSettingsFromState(state, preservedOtherTouchScale = null, preservedRightPaneMode = null) {
|
|
500
513
|
return normalizeProfileSettings({
|
|
514
|
+
...(state?.ui?.moa ? { moa: state.ui.moa } : {}),
|
|
501
515
|
themeId: state?.ui?.themeId,
|
|
502
516
|
[touchScaleKey()]: state?.ui?.touchScale,
|
|
503
517
|
sessionDetailCollapsed: state?.ui?.sessionDetailCollapsed,
|
|
@@ -541,6 +555,7 @@ function materializeProfileSettings(remoteSettings, defaults) {
|
|
|
541
555
|
// the server has not persisted yet. An omitted key leaves the
|
|
542
556
|
// profileSettings/apply guards preserving the current value.
|
|
543
557
|
return normalizeProfileSettings({
|
|
558
|
+
...(hasOwn(normalizedRemote, "moa") ? { moa: normalizedRemote.moa } : {}),
|
|
544
559
|
themeId: hasOwn(normalizedRemote, "themeId")
|
|
545
560
|
? normalizedRemote.themeId
|
|
546
561
|
: normalizedDefaults.themeId,
|
|
@@ -611,11 +626,18 @@ function materializeProfileSettings(remoteSettings, defaults) {
|
|
|
611
626
|
});
|
|
612
627
|
}
|
|
613
628
|
|
|
629
|
+
const profileSaveQueues = new WeakMap();
|
|
614
630
|
async function saveProfileSettings(controller, settings) {
|
|
615
631
|
if (typeof controller?.transport?.setCurrentUserProfileSettings !== "function") return null;
|
|
616
|
-
|
|
632
|
+
// Whole-document writes must arrive in edit order. A slow earlier request
|
|
633
|
+
// must never land after a newer layout and erase it.
|
|
634
|
+
const previous = profileSaveQueues.get(controller) || Promise.resolve();
|
|
635
|
+
const next = previous.catch(() => {}).then(() => controller.transport.setCurrentUserProfileSettings({
|
|
617
636
|
settings: normalizeProfileSettings(settings),
|
|
618
|
-
});
|
|
637
|
+
}));
|
|
638
|
+
profileSaveQueues.set(controller, next);
|
|
639
|
+
try { return await next; }
|
|
640
|
+
finally { if (profileSaveQueues.get(controller) === next) profileSaveQueues.delete(controller); }
|
|
619
641
|
}
|
|
620
642
|
|
|
621
643
|
function getVisibleInspectorTabs(controller) {
|
|
@@ -993,10 +1015,11 @@ function applyDocumentTheme(themeId) {
|
|
|
993
1015
|
root.dataset.psTheme = theme.id;
|
|
994
1016
|
}
|
|
995
1017
|
|
|
996
|
-
function useMeasuredViewport(ref) {
|
|
1018
|
+
function useMeasuredViewport(ref, suspended = false) {
|
|
997
1019
|
const [viewport, setViewport] = React.useState({ width: 0, height: 0 });
|
|
998
1020
|
|
|
999
1021
|
React.useLayoutEffect(() => {
|
|
1022
|
+
if (suspended) return undefined;
|
|
1000
1023
|
const element = ref.current;
|
|
1001
1024
|
if (!element) return undefined;
|
|
1002
1025
|
|
|
@@ -1015,7 +1038,7 @@ function useMeasuredViewport(ref) {
|
|
|
1015
1038
|
observer.disconnect();
|
|
1016
1039
|
window.removeEventListener("resize", update);
|
|
1017
1040
|
};
|
|
1018
|
-
}, [ref]);
|
|
1041
|
+
}, [ref, suspended]);
|
|
1019
1042
|
|
|
1020
1043
|
return viewport;
|
|
1021
1044
|
}
|
|
@@ -2862,7 +2885,7 @@ function useElementBox(ref) {
|
|
|
2862
2885
|
return box;
|
|
2863
2886
|
}
|
|
2864
2887
|
|
|
2865
|
-
function fetchArtifactHtmlObjectUrl(controller, sessionId, filename) {
|
|
2888
|
+
function fetchArtifactHtmlObjectUrl(controller, sessionId, filename, panelKeys = false) {
|
|
2866
2889
|
const downloadResponse = controller?.transport?.api?.downloadArtifactResponse;
|
|
2867
2890
|
if (typeof downloadResponse !== "function") {
|
|
2868
2891
|
return Promise.reject(new Error("artifact download unavailable"));
|
|
@@ -2875,7 +2898,18 @@ function fetchArtifactHtmlObjectUrl(controller, sessionId, filename) {
|
|
|
2875
2898
|
// would otherwise make the iframe offer a download instead of
|
|
2876
2899
|
// rendering, and a missing charset would mojibake UTF-8 content.
|
|
2877
2900
|
const bytes = await response.arrayBuffer();
|
|
2878
|
-
|
|
2901
|
+
// Only MoA canvases opt into panel navigation. Ordinary canvases
|
|
2902
|
+
// retain native Tab behavior and their exact document bytes.
|
|
2903
|
+
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>` : "";
|
|
2904
|
+
let parts = [bytes];
|
|
2905
|
+
if (panelKeys) {
|
|
2906
|
+
const html = new TextDecoder().decode(bytes);
|
|
2907
|
+
// Keep the doctype first so panel navigation cannot change
|
|
2908
|
+
// the canvas from standards mode into quirks mode.
|
|
2909
|
+
const prolog = /^(?:\uFEFF|\s|<!--[\s\S]*?-->)*<!doctype[^>]*>/i.exec(html)?.[0] || "";
|
|
2910
|
+
parts = [html.slice(0, prolog.length), bridge, html.slice(prolog.length)];
|
|
2911
|
+
}
|
|
2912
|
+
return URL.createObjectURL(new Blob(parts, { type: "text/html;charset=utf-8" }));
|
|
2879
2913
|
});
|
|
2880
2914
|
}
|
|
2881
2915
|
|
|
@@ -3571,15 +3605,14 @@ const AssistantPreviewCard = React.memo(function AssistantPreviewCard({ line, th
|
|
|
3571
3605
|
? React.createElement(Runs, { runs: line.headerRuns, theme })
|
|
3572
3606
|
: React.createElement(React.Fragment, null,
|
|
3573
3607
|
React.createElement("span", { className: "ps-canvas-action-tag" }, "Agent"),
|
|
3574
|
-
React.createElement("span", { className: "ps-system-notice-summary-text" },
|
|
3575
|
-
React.createElement("span", { className: "ps-preview-status" },
|
|
3576
|
-
line.isLive ? (line.body ? "Responding" : "Thinking") : "Saved"))),
|
|
3608
|
+
React.createElement("span", { className: "ps-system-notice-summary-text" }, line.text),
|
|
3609
|
+
line.statusText ? React.createElement("span", { className: "ps-preview-status" }, line.statusText) : null)),
|
|
3577
3610
|
React.createElement("div", {
|
|
3578
3611
|
ref: viewportRef,
|
|
3579
3612
|
className: "ps-assistant-preview-viewport",
|
|
3580
3613
|
tabIndex: expanded && !line.final ? 0 : undefined,
|
|
3581
3614
|
role: line.final ? undefined : "region",
|
|
3582
|
-
"aria-label": line.final ? undefined :
|
|
3615
|
+
"aria-label": line.final ? undefined : line.text,
|
|
3583
3616
|
onScroll: (event) => {
|
|
3584
3617
|
if (!line.final) {
|
|
3585
3618
|
event.stopPropagation();
|
|
@@ -3947,7 +3980,7 @@ function focusRegionForPaneKey(paneKey, override = null) {
|
|
|
3947
3980
|
return PANE_KEY_FOCUS_REGIONS[String(paneKey || "")] || null;
|
|
3948
3981
|
}
|
|
3949
3982
|
|
|
3950
|
-
function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, lines, stickyLines = [], bottomStickyLines = [], scrollOffset = 0, scrollMode = "top", paneKey, controller, className = "", panelClassName = "", topContent = null, bottomContent = null, structuredBlocks = false, stickyBottom = false, renderBody = null, focusRegion = null, panelRef = null, ariaLive = null }) {
|
|
3983
|
+
function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, lines, stickyLines = [], bottomStickyLines = [], reserveBottomSticky = false, scrollOffset = 0, scrollMode = "top", paneKey, controller, className = "", panelClassName = "", topContent = null, bottomContent = null, structuredBlocks = false, stickyBottom = false, renderBody = null, focusRegion = null, panelRef = null, ariaLive = null }) {
|
|
3951
3984
|
const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
|
|
3952
3985
|
const theme = getTheme(themeId);
|
|
3953
3986
|
const ref = React.useRef(null);
|
|
@@ -4066,9 +4099,9 @@ function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, l
|
|
|
4066
4099
|
? React.createElement(StructuredChatBlocks, { lines: normalizedLines, theme, controller })
|
|
4067
4100
|
: normalizedLines.map((line, index) => React.createElement(Line, { key: `line:${index}`, line, theme })),
|
|
4068
4101
|
),
|
|
4069
|
-
normalizedBottomSticky.length > 0
|
|
4102
|
+
(normalizedBottomSticky.length > 0 || reserveBottomSticky)
|
|
4070
4103
|
? React.createElement("div", {
|
|
4071
|
-
className:
|
|
4104
|
+
className: `ps-panel-bottom-sticky${reserveBottomSticky ? " is-reserved" : ""}`,
|
|
4072
4105
|
},
|
|
4073
4106
|
normalizedBottomSticky.map((line, index) => React.createElement(Line, { key: `bottom-sticky:${index}`, line, theme })),
|
|
4074
4107
|
)
|
|
@@ -4868,7 +4901,7 @@ function useAxisLockedPan(ref, enabled = true) {
|
|
|
4868
4901
|
}, [ref, enabled]);
|
|
4869
4902
|
}
|
|
4870
4903
|
|
|
4871
|
-
function SessionPane({ controller, actions = null, panelClassName = "", structuredRows = false, showDetailBox = null }) {
|
|
4904
|
+
function SessionPane({ controller, actions = null, panelClassName = "", structuredRows = false, showDetailBox = null, selection = null, actionsOnly = false, actionsHost = null, onAction = null, onDialogChange = null }) {
|
|
4872
4905
|
// Mobile keeps its inline detail line and normally gets no detail box — a
|
|
4873
4906
|
// reserved footer would eat a meaningful slice of a phone screen. The
|
|
4874
4907
|
// sessions-ONLY layout is the exception: it has the whole screen and the
|
|
@@ -4894,23 +4927,23 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
4894
4927
|
const sessionButtonRefs = React.useRef(new Map());
|
|
4895
4928
|
const viewState = useControllerSelector(controller, (state) => ({
|
|
4896
4929
|
branding: state.branding,
|
|
4897
|
-
activeSessionId: state.sessions.activeSessionId,
|
|
4930
|
+
activeSessionId: selection ? selection.sessionId : state.sessions.activeSessionId,
|
|
4898
4931
|
sessionsById: state.sessions.byId,
|
|
4899
4932
|
sessionsFlat: state.sessions.flat,
|
|
4900
|
-
filterQuery: state.sessions.filterQuery || "",
|
|
4933
|
+
filterQuery: selection ? selection.query || "" : state.sessions.filterQuery || "",
|
|
4901
4934
|
ownerFilter: state.sessions.ownerFilter,
|
|
4902
4935
|
pinnedIds: state.sessions.pinnedIds,
|
|
4903
4936
|
manualOrder: state.sessions.manualOrder,
|
|
4904
|
-
selectedIds: state.sessions.selectedIds,
|
|
4905
|
-
selectMode: state.sessions.selectMode,
|
|
4937
|
+
selectedIds: selection ? [] : state.sessions.selectedIds,
|
|
4938
|
+
selectMode: selection ? false : state.sessions.selectMode,
|
|
4906
4939
|
auth: state.auth,
|
|
4907
4940
|
connectionMode: state.connection?.mode || "local",
|
|
4908
4941
|
modalOpen: Boolean(state.ui.modal),
|
|
4909
|
-
focused: state.ui.focusRegion === "sessions",
|
|
4942
|
+
focused: selection ? true : state.ui.focusRegion === "sessions",
|
|
4910
4943
|
// Clicking empty space clears the list highlight; the row VM reads it,
|
|
4911
4944
|
// so the reconstruction below must carry it or the click does nothing
|
|
4912
4945
|
// visible (the same omission that blinded the Node Map).
|
|
4913
|
-
listDeselected: Boolean(state.sessions.listDeselected),
|
|
4946
|
+
listDeselected: selection ? !selection.sessionId : Boolean(state.sessions.listDeselected),
|
|
4914
4947
|
// The canvas markers read these. This synthetic state is exactly the
|
|
4915
4948
|
// trap the branding comment below describes: omit a slice here and
|
|
4916
4949
|
// every row computes as if it were empty — the markers were null on
|
|
@@ -4983,6 +5016,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
4983
5016
|
setManageOpen(true);
|
|
4984
5017
|
}
|
|
4985
5018
|
}, [modelFlowOpen]);
|
|
5019
|
+
React.useEffect(() => { onDialogChange?.(manageOpen || Boolean(linkModal) || modelFlowOpen); }, [manageOpen, linkModal, modelFlowOpen, onDialogChange]);
|
|
4986
5020
|
const requestSwitchModel = () => {
|
|
4987
5021
|
reopenManageRef.current = true;
|
|
4988
5022
|
setManageOpen(false);
|
|
@@ -5076,7 +5110,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
5076
5110
|
// rows/viewState from a ref that is refreshed on every render instead of
|
|
5077
5111
|
// closing over them.
|
|
5078
5112
|
const clickEnv = React.useRef(null);
|
|
5079
|
-
clickEnv.current = { rows, viewState, controller };
|
|
5113
|
+
clickEnv.current = { rows, viewState, controller, selection };
|
|
5080
5114
|
// ── Drag sessions into / out of groups ────────────────────────────
|
|
5081
5115
|
// Pointer-driven so it works from <button> rows, can render a collection
|
|
5082
5116
|
// ghost for a multi-selection, and can highlight the destination folder.
|
|
@@ -5401,7 +5435,12 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
5401
5435
|
}), [dragState.dragging, dragState.overGroupId, dragState.insertBeforeId, dragState.insertAfterId]);
|
|
5402
5436
|
|
|
5403
5437
|
const handleRowClick = React.useCallback((event, row) => {
|
|
5404
|
-
const { rows: currentRows, viewState: current, controller: ctl } = clickEnv.current;
|
|
5438
|
+
const { rows: currentRows, viewState: current, controller: ctl, selection: pick } = clickEnv.current;
|
|
5439
|
+
if (pick) {
|
|
5440
|
+
if (row.isGroup || (row.hasChildren && row.active)) ctl.dispatch({ type: row.collapsed ? "sessions/expand" : "sessions/collapse", sessionId: row.sessionId });
|
|
5441
|
+
if (!row.isGroup) pick.onSelect(row.sessionId);
|
|
5442
|
+
return;
|
|
5443
|
+
}
|
|
5405
5444
|
// Cmd/Ctrl-click toggles multi-selection (any row).
|
|
5406
5445
|
if (event.metaKey || event.ctrlKey) {
|
|
5407
5446
|
event.preventDefault();
|
|
@@ -5516,14 +5555,14 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
5516
5555
|
}, [activeRowRevealKey, viewState.activeSessionId, viewState.focused, viewState.modalOpen, viewState.sessionsFlat]);
|
|
5517
5556
|
|
|
5518
5557
|
const panelActions = React.createElement(React.Fragment, null,
|
|
5519
|
-
isBulkSelection
|
|
5558
|
+
!actionsOnly && isBulkSelection
|
|
5520
5559
|
? React.createElement("span", {
|
|
5521
5560
|
className: "ps-mini-button-label",
|
|
5522
5561
|
style: { padding: "0 6px", fontSize: "12px", opacity: 0.85 },
|
|
5523
5562
|
title: "Multiple sessions selected. Move to group or cancel selected sessions; click Clear to exit.",
|
|
5524
5563
|
}, `${selectedCount} selected`)
|
|
5525
5564
|
: null,
|
|
5526
|
-
isBulkSelection
|
|
5565
|
+
!actionsOnly && (isBulkSelection
|
|
5527
5566
|
? React.createElement(IconButton, {
|
|
5528
5567
|
className: "ps-mini-button",
|
|
5529
5568
|
icon: "✕",
|
|
@@ -5539,8 +5578,8 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
5539
5578
|
label: canPinActiveSession
|
|
5540
5579
|
? (isActivePinned ? "Unpin this session" : "Pin this session to the top of the list")
|
|
5541
5580
|
: "Only top-level non-system sessions can be pinned",
|
|
5542
|
-
}),
|
|
5543
|
-
React.createElement(IconButton, {
|
|
5581
|
+
})),
|
|
5582
|
+
!actionsOnly && React.createElement(IconButton, {
|
|
5544
5583
|
className: "ps-mini-button",
|
|
5545
5584
|
icon: groupableIds.length > 1
|
|
5546
5585
|
? React.createElement("span", { className: "ps-icon-badge" },
|
|
@@ -5581,9 +5620,9 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
5581
5620
|
? "Rename folder"
|
|
5582
5621
|
: !canModifyActiveSession
|
|
5583
5622
|
? "Only this session's owner or an admin can manage it"
|
|
5584
|
-
: "Manage session — rename, switch model, and sharing",
|
|
5623
|
+
: actionsOnly ? "Manage session — rename and switch model" : "Manage session — rename, switch model, and sharing",
|
|
5585
5624
|
}),
|
|
5586
|
-
React.createElement(IconButton, {
|
|
5625
|
+
!actionsOnly && React.createElement(IconButton, {
|
|
5587
5626
|
className: "ps-mini-button",
|
|
5588
5627
|
icon: React.createElement(LinkGlyph),
|
|
5589
5628
|
onClick: () => {
|
|
@@ -5643,16 +5682,31 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
5643
5682
|
|
|
5644
5683
|
return React.createElement(React.Fragment, null,
|
|
5645
5684
|
dragGhost,
|
|
5646
|
-
React.createElement(Panel, {
|
|
5685
|
+
actionsOnly ? (actionsHost ? createPortal(React.createElement("div", { className: "ps-moa-control-actions", onClick: onAction }, panelActions), actionsHost) : null) : React.createElement(Panel, {
|
|
5647
5686
|
title: [{ text: "Sessions", color: "yellow", bold: true }],
|
|
5648
5687
|
color: "yellow",
|
|
5649
5688
|
focused: viewState.focused,
|
|
5650
5689
|
theme,
|
|
5651
|
-
actions:
|
|
5690
|
+
actions: selection ? React.createElement(React.Fragment, null,
|
|
5691
|
+
selection.onCreate ? React.createElement(IconButton, {
|
|
5692
|
+
className: "ps-mini-button", icon: React.createElement(PlusGlyph),
|
|
5693
|
+
label: "Create New Session", onClick: selection.onCreate,
|
|
5694
|
+
}) : null, actions) : panelActions,
|
|
5652
5695
|
className: combinedPanelClassName,
|
|
5653
5696
|
},
|
|
5697
|
+
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
5698
|
React.createElement("div", {
|
|
5655
5699
|
ref: sessionListRef,
|
|
5700
|
+
onKeyDown: selection ? event => {
|
|
5701
|
+
if (!["ArrowUp", "ArrowDown", "Home", "End"].includes(event.key) || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
5702
|
+
const buttons = [...sessionListRef.current.querySelectorAll(".ps-session-list-button")];
|
|
5703
|
+
const current = buttons.indexOf(event.target.closest(".ps-session-list-button"));
|
|
5704
|
+
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)));
|
|
5705
|
+
const button = buttons[next]; if (!button) return;
|
|
5706
|
+
event.preventDefault(); event.stopPropagation();
|
|
5707
|
+
if (button.dataset.sessionId) selection.onSelect(button.dataset.sessionId);
|
|
5708
|
+
button.focus();
|
|
5709
|
+
} : undefined,
|
|
5656
5710
|
className: `ps-action-list ps-session-list${dragState.dragging ? " is-dragging" : ""}`,
|
|
5657
5711
|
// Clicking empty space below the rows clears the list selection while
|
|
5658
5712
|
// the other panes keep showing the session. With nothing selected the
|
|
@@ -5661,9 +5715,11 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
5661
5715
|
// Empty space only: a click that lands on a row bubbles here too.
|
|
5662
5716
|
const onRow = event.target instanceof Element && event.target.closest(".ps-session-list-button");
|
|
5663
5717
|
if (onRow) return;
|
|
5664
|
-
|
|
5718
|
+
if (selection) selection.onSelect(null);
|
|
5719
|
+
else controller.dispatch({ type: "sessions/listDeselect" });
|
|
5665
5720
|
},
|
|
5666
5721
|
},
|
|
5722
|
+
|
|
5667
5723
|
rows.length === 0
|
|
5668
5724
|
? React.createElement("div", { className: "ps-empty-state" }, viewState.filterQuery
|
|
5669
5725
|
? `No sessions matched "@@${viewState.filterQuery}".`
|
|
@@ -5678,7 +5734,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
5678
5734
|
setRef: setSessionButtonRef,
|
|
5679
5735
|
// Drag-to-folder is a fine-pointer gesture: arming it on
|
|
5680
5736
|
// touch hijacks the finger that should be scrolling the list.
|
|
5681
|
-
drag: touchInput ? null : dragHandlers,
|
|
5737
|
+
drag: selection || touchInput ? null : dragHandlers,
|
|
5682
5738
|
}))),
|
|
5683
5739
|
(showDetailBox === null ? !isMobilePane : showDetailBox)
|
|
5684
5740
|
? React.createElement(SessionDetailBox, {
|
|
@@ -5700,6 +5756,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
5700
5756
|
controller,
|
|
5701
5757
|
sessionId: activeSession.sessionId,
|
|
5702
5758
|
initialTitle: activeSession.title || "",
|
|
5759
|
+
allowSharing: !actionsOnly,
|
|
5703
5760
|
currentModel: activeSession.model || "",
|
|
5704
5761
|
currentReasoningEffort: activeSession.reasoningEffort || "",
|
|
5705
5762
|
principal: viewState.auth?.principal || null,
|
|
@@ -5898,6 +5955,13 @@ function SidebarGlyph() {
|
|
|
5898
5955
|
// icon size without a label.
|
|
5899
5956
|
// The Main pane, whatever layout it is in: a frame with a list rail and a
|
|
5900
5957
|
// conversation beside it. Constant across the three-way cycle.
|
|
5958
|
+
// A tiled command surface: one tall pane beside two stacked panes.
|
|
5959
|
+
function MoaGlyph() {
|
|
5960
|
+
return React.createElement(Glyph, null,
|
|
5961
|
+
React.createElement("rect", { x: 3, y: 4, width: 7, height: 16, rx: 1 }),
|
|
5962
|
+
React.createElement("rect", { x: 13, y: 4, width: 8, height: 6.5, rx: 1 }),
|
|
5963
|
+
React.createElement("rect", { x: 13, y: 13.5, width: 8, height: 6.5, rx: 1 }));
|
|
5964
|
+
}
|
|
5901
5965
|
function MainLayoutGlyph() {
|
|
5902
5966
|
return React.createElement(Glyph, null,
|
|
5903
5967
|
React.createElement("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }),
|
|
@@ -6082,7 +6146,7 @@ function useActiveSessionAccess(controller, activeSessionId, isGroup) {
|
|
|
6082
6146
|
// rename, copy link, plus (for the owner/admin) sharing — visibility +
|
|
6083
6147
|
// per-person grants. Fetches its own access snapshot so callers only pass the
|
|
6084
6148
|
// session id + current title.
|
|
6085
|
-
function SessionModifyModal({ controller, sessionId, initialTitle, currentModel, currentReasoningEffort, principal, onClose, onSwitchModel, onChanged }) {
|
|
6149
|
+
function SessionModifyModal({ controller, sessionId, initialTitle, currentModel, currentReasoningEffort, principal, onClose, onSwitchModel, onChanged, allowSharing = true }) {
|
|
6086
6150
|
const [tab, setTab] = React.useState("general");
|
|
6087
6151
|
const [access, setAccess] = React.useState(null);
|
|
6088
6152
|
const [title, setTitle] = React.useState(initialTitle || "");
|
|
@@ -6101,20 +6165,20 @@ function SessionModifyModal({ controller, sessionId, initialTitle, currentModel,
|
|
|
6101
6165
|
.then((a) => { if (!cancelled && a) { setAccess(a); setVisibility(a.visibility || "private"); } })
|
|
6102
6166
|
.catch(() => {});
|
|
6103
6167
|
// Member directory for name autocomplete (excludes synthetic principals).
|
|
6104
|
-
if (typeof controller.transport.listKnownUsers === "function") {
|
|
6168
|
+
if (allowSharing && typeof controller.transport.listKnownUsers === "function") {
|
|
6105
6169
|
controller.transport.listKnownUsers({ limit: 500 })
|
|
6106
6170
|
.then((users) => { if (!cancelled) setDirectory(Array.isArray(users) ? users : []); })
|
|
6107
6171
|
.catch(() => {});
|
|
6108
6172
|
}
|
|
6109
6173
|
return () => { cancelled = true; };
|
|
6110
|
-
}, [controller, sessionId]);
|
|
6174
|
+
}, [controller, sessionId, allowSharing]);
|
|
6111
6175
|
|
|
6112
6176
|
const loadShares = React.useCallback(() => {
|
|
6113
6177
|
controller.transport.listSessionShares(sessionId)
|
|
6114
6178
|
.then((rows) => { const list = Array.isArray(rows) ? rows : []; setShares(list); setDraftShares(list); })
|
|
6115
6179
|
.catch(() => { setShares([]); setDraftShares([]); });
|
|
6116
6180
|
}, [controller, sessionId]);
|
|
6117
|
-
React.useEffect(() => { loadShares(); }, [loadShares]);
|
|
6181
|
+
React.useEffect(() => { if (allowSharing) loadShares(); }, [loadShares, allowSharing]);
|
|
6118
6182
|
|
|
6119
6183
|
const run = async (fn) => {
|
|
6120
6184
|
setBusy(true); setError(null);
|
|
@@ -6220,7 +6284,7 @@ function SessionModifyModal({ controller, sessionId, initialTitle, currentModel,
|
|
|
6220
6284
|
// only rename/switch model on their own session.
|
|
6221
6285
|
const tabs = [
|
|
6222
6286
|
{ id: "general", label: "General" },
|
|
6223
|
-
...(canManage ? [{ id: "access", label: "Access" }] : []),
|
|
6287
|
+
...(allowSharing && canManage ? [{ id: "access", label: "Access" }] : []),
|
|
6224
6288
|
];
|
|
6225
6289
|
const activeTab = tabs.some((t) => t.id === tab) ? tab : "general";
|
|
6226
6290
|
// Regenerate is a change-this-session action, so it belongs here rather
|
|
@@ -6344,7 +6408,41 @@ function SessionModifyModal({ controller, sessionId, initialTitle, currentModel,
|
|
|
6344
6408
|
error ? React.createElement("div", { className: "ps-share-error" }, error) : null));
|
|
6345
6409
|
}
|
|
6346
6410
|
|
|
6347
|
-
function
|
|
6411
|
+
function SessionComposer({ controller, onReadOnlyFocus = null, mobile = false, compact = false }) {
|
|
6412
|
+
const modal = useControllerSelector(controller, state => state.ui.modal);
|
|
6413
|
+
const session = useControllerSelector(controller, state => state.sessions.byId[state.sessions.activeSessionId]);
|
|
6414
|
+
const { access } = useActiveSessionAccess(controller, session?.sessionId, session?.isGroup);
|
|
6415
|
+
const readOnly = session?.serviceKind || access?.canWrite === false;
|
|
6416
|
+
React.useLayoutEffect(() => { if (readOnly && !modal) onReadOnlyFocus?.(); }, [readOnly, modal, onReadOnlyFocus]);
|
|
6417
|
+
if (!session || session.isGroup || modal) return null;
|
|
6418
|
+
return React.createElement("div", { className: "ps-chat-composer" }, readOnly
|
|
6419
|
+
? React.createElement("div", { className: "ps-composer-readonly" }, session.serviceKind
|
|
6420
|
+
? "⚗ Service session — runtime machinery. Its transcript is a read-only trace; it does not accept messages."
|
|
6421
|
+
: `You have view access to this session. Ask ${access.owner?.displayName || access.owner?.email || "the owner"} for write access to participate.`)
|
|
6422
|
+
: React.createElement(PromptComposer, { controller, mobile, compact, autoFocus: !mobile, active: true }));
|
|
6423
|
+
}
|
|
6424
|
+
|
|
6425
|
+
// The compact focus views use their existing header for activity. The same
|
|
6426
|
+
// selector powers the desktop footer, so stale-status and elapsed-time rules
|
|
6427
|
+
// remain identical; queued prompt bodies stay readable in the transcript.
|
|
6428
|
+
function SessionHeaderStatus({ controller }) {
|
|
6429
|
+
const state = useControllerSelector(controller, s => s);
|
|
6430
|
+
const session = state.sessions.byId[state.sessions.activeSessionId];
|
|
6431
|
+
const spinnerFrame = useSpinnerFrame(session?.status === "running");
|
|
6432
|
+
const activity = selectLiveActivityLines(state, { spinnerFrame, maxWidth: 120 });
|
|
6433
|
+
const status = activity[0]?.map(run => run.text || "").join("")
|
|
6434
|
+
|| (session ? String(session.status || "idle").replace(/^./, c => c.toUpperCase()) : "");
|
|
6435
|
+
const messages = selectActiveOutboxMessages(state);
|
|
6436
|
+
const queue = ["pending", "queued", "cancelling", "rejected"].map(phase => {
|
|
6437
|
+
const count = messages.filter(message => message.pendingPhase === phase).length;
|
|
6438
|
+
return count ? `${count} ${phase}` : "";
|
|
6439
|
+
}).filter(Boolean).join(" · ");
|
|
6440
|
+
return React.createElement("div", { className: "ps-mobile-session-status", "aria-label": "Session status" },
|
|
6441
|
+
React.createElement("span", { className: "ps-mobile-activity", title: status }, status),
|
|
6442
|
+
queue ? React.createElement("span", { className: "ps-mobile-queue", title: queue }, queue) : null);
|
|
6443
|
+
}
|
|
6444
|
+
|
|
6445
|
+
function ChatPane({ controller, mobile = false, fullWidth = false, showComposer = true, onEnterZen = null, activityInHeader = false }) {
|
|
6348
6446
|
const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
|
|
6349
6447
|
const theme = getTheme(themeId);
|
|
6350
6448
|
const viewState = useControllerSelector(controller, (state) => {
|
|
@@ -6455,6 +6553,10 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
6455
6553
|
() => (pinnedActivityLines.length > 0 ? [...outboxLines, ...pinnedActivityLines] : outboxLines),
|
|
6456
6554
|
[outboxLines, pinnedActivityLines],
|
|
6457
6555
|
);
|
|
6556
|
+
const transcriptLines = React.useMemo(
|
|
6557
|
+
() => activityInHeader && outboxLines.length ? [...lines, ...outboxLines] : lines,
|
|
6558
|
+
[activityInHeader, lines, outboxLines],
|
|
6559
|
+
);
|
|
6458
6560
|
// Read-only gating: a view-only viewer (shared_read / read grant, no write)
|
|
6459
6561
|
// gets an explanatory notice instead of the composer. The visibility chip
|
|
6460
6562
|
// and Share affordance now live in the session list "Modify" modal and the
|
|
@@ -6510,11 +6612,12 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
6510
6612
|
titleRight: mobile && titleRight ? compactTitleRuns(titleRight, 18) : titleRight,
|
|
6511
6613
|
// Chat/Summary toggling lives on the top toolbar so the pane chrome
|
|
6512
6614
|
// stays clean. The toolbar button is disabled for group sessions.
|
|
6513
|
-
actions: null,
|
|
6615
|
+
actions: mobile && onEnterZen ? React.createElement(IconButton, { className: "ps-mini-button", icon: React.createElement(ExpandGlyph), label: "Enter mobile zen", onClick: onEnterZen }) : null,
|
|
6514
6616
|
color: chrome.color,
|
|
6515
6617
|
focused: viewState.focused,
|
|
6516
|
-
lines,
|
|
6517
|
-
bottomStickyLines: stickyBottom,
|
|
6618
|
+
lines: transcriptLines,
|
|
6619
|
+
bottomStickyLines: activityInHeader ? EMPTY_ARRAY : stickyBottom,
|
|
6620
|
+
reserveBottomSticky: !activityInHeader,
|
|
6518
6621
|
scrollOffset: viewState.scroll,
|
|
6519
6622
|
scrollMode: "bottom",
|
|
6520
6623
|
paneKey: "chat",
|
|
@@ -6864,7 +6967,7 @@ function useKeyboardTakeover(enabled) {
|
|
|
6864
6967
|
* detail sub-panel). This replaced chat-focus mode, which was a second way to
|
|
6865
6968
|
* say "chat only" with its own chrome and its own exit.
|
|
6866
6969
|
*/
|
|
6867
|
-
function MobileWorkspace({ controller, layout = "split" }) {
|
|
6970
|
+
function MobileWorkspace({ controller, layout = "split", onEnterZen }) {
|
|
6868
6971
|
const sessionPane = React.createElement(SessionPane, {
|
|
6869
6972
|
controller,
|
|
6870
6973
|
panelClassName: "ps-mobile-session-pane",
|
|
@@ -6872,7 +6975,7 @@ function MobileWorkspace({ controller, layout = "split" }) {
|
|
|
6872
6975
|
if (layout === "chat") {
|
|
6873
6976
|
return React.createElement("div", { className: "ps-mobile-workspace is-chat-only" },
|
|
6874
6977
|
React.createElement("div", { className: "ps-mobile-chat-pane" },
|
|
6875
|
-
React.createElement(ChatPane, { controller, mobile: true, fullWidth: true })));
|
|
6978
|
+
React.createElement(ChatPane, { controller, mobile: true, fullWidth: true, onEnterZen })));
|
|
6876
6979
|
}
|
|
6877
6980
|
if (layout === "sessions") {
|
|
6878
6981
|
return React.createElement("div", { className: "ps-mobile-workspace is-sessions-only" },
|
|
@@ -6885,7 +6988,7 @@ function MobileWorkspace({ controller, layout = "split" }) {
|
|
|
6885
6988
|
return React.createElement("div", { className: "ps-mobile-workspace" },
|
|
6886
6989
|
sessionPane,
|
|
6887
6990
|
React.createElement("div", { className: "ps-mobile-chat-pane" },
|
|
6888
|
-
React.createElement(ChatPane, { controller, mobile: true, fullWidth: true })));
|
|
6991
|
+
React.createElement(ChatPane, { controller, mobile: true, fullWidth: true, onEnterZen })));
|
|
6889
6992
|
}
|
|
6890
6993
|
|
|
6891
6994
|
/** Frame-and-easel glyph for the Canvas toggle. */
|
|
@@ -7233,7 +7336,9 @@ function RestoreGlyph() {
|
|
|
7233
7336
|
* loads hidden-but-painted behind the live frame and is promoted on load, so
|
|
7234
7337
|
* a live redraw is a composite swap of something already drawn.
|
|
7235
7338
|
*/
|
|
7236
|
-
function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible = true, focusOnPromote = false, dataRev = 0, dataPayload = null, dataPatch = null }) {
|
|
7339
|
+
function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible = true, focusOnPromote = false, dataRev = 0, dataPayload = null, dataPatch = null, onPanelKey = null }) {
|
|
7340
|
+
const panelKeyRef = React.useRef(onPanelKey); panelKeyRef.current = onPanelKey;
|
|
7341
|
+
const panelKeys = Boolean(onPanelKey);
|
|
7237
7342
|
const [live, setLive] = React.useState(null); // { url, rev }
|
|
7238
7343
|
const [staging, setStaging] = React.useState(null); // { url, rev }
|
|
7239
7344
|
const [loadError, setLoadError] = React.useState(null); // { rev }
|
|
@@ -7285,9 +7390,13 @@ function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible
|
|
|
7285
7390
|
// window that asked). Writes and actions stay live-frame only.
|
|
7286
7391
|
const fromStaging = Boolean(stagingIframeElRef.current && event.source === stagingIframeElRef.current.contentWindow);
|
|
7287
7392
|
if (!fromLive && !fromStaging) return;
|
|
7393
|
+
if (payload.type === "moa-panel-key") {
|
|
7394
|
+
if (fromLive && document.activeElement === frameEl && ["Tab", "Escape"].includes(payload.key)) panelKeyRef.current?.(payload.key, payload.backwards === true);
|
|
7395
|
+
return;
|
|
7396
|
+
}
|
|
7288
7397
|
if (payload.type === "canvas-action") {
|
|
7289
7398
|
if (!fromLive) return;
|
|
7290
|
-
controller.submitCanvasAction(sessionId, payload).then((result) => {
|
|
7399
|
+
controller.submitCanvasAction(sessionId, payload, slot).then((result) => {
|
|
7291
7400
|
if (result && result.ok === false && typeof console !== "undefined") {
|
|
7292
7401
|
console.warn(`canvas action rejected: ${result.reason}`);
|
|
7293
7402
|
}
|
|
@@ -7372,7 +7481,7 @@ function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible
|
|
|
7372
7481
|
if (!controller || !sessionId || !latestRev) return undefined;
|
|
7373
7482
|
let cancelled = false;
|
|
7374
7483
|
let retryTimer = null;
|
|
7375
|
-
fetchArtifactHtmlObjectUrl(controller, sessionId, slot <= 1 ? "canvas.html" : `canvas${slot}.html
|
|
7484
|
+
fetchArtifactHtmlObjectUrl(controller, sessionId, slot <= 1 ? "canvas.html" : `canvas${slot}.html`, panelKeys)
|
|
7376
7485
|
.then((url) => {
|
|
7377
7486
|
if (cancelled) { URL.revokeObjectURL(url); return; }
|
|
7378
7487
|
// A staging revision that never loaded is superseded here —
|
|
@@ -7400,7 +7509,7 @@ function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible
|
|
|
7400
7509
|
cancelled = true;
|
|
7401
7510
|
if (retryTimer) clearTimeout(retryTimer);
|
|
7402
7511
|
};
|
|
7403
|
-
}, [controller, sessionId, slot, latestRev, retryTick]);
|
|
7512
|
+
}, [controller, sessionId, slot, latestRev, retryTick, panelKeys]);
|
|
7404
7513
|
|
|
7405
7514
|
// Session switch / unmount: drop both frames — a different session is a
|
|
7406
7515
|
// different document, and showing the old canvas under a new session is a
|
|
@@ -8605,7 +8714,7 @@ function formatAttachmentSize(sizeBytes) {
|
|
|
8605
8714
|
return `${bytes} B`;
|
|
8606
8715
|
}
|
|
8607
8716
|
|
|
8608
|
-
function PromptComposer({ controller, mobile, active = true, onAfterSend = null }) {
|
|
8717
|
+
function PromptComposer({ controller, mobile, compact = false, active = true, onAfterSend = null, autoFocus = true }) {
|
|
8609
8718
|
const promptState = useControllerSelector(controller, (state) => {
|
|
8610
8719
|
const activeSessionId = state.sessions.activeSessionId;
|
|
8611
8720
|
const activeSession = activeSessionId ? state.sessions.byId[activeSessionId] || null : null;
|
|
@@ -8634,6 +8743,20 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
8634
8743
|
const inputRef = React.useRef(null);
|
|
8635
8744
|
const attachInputRef = React.useRef(null);
|
|
8636
8745
|
const [dragOver, setDragOver] = React.useState(false);
|
|
8746
|
+
const selectedQueued = promptState.selectedOutboxPhase === "queued";
|
|
8747
|
+
const selectedCancelling = promptState.selectedOutboxPhase === "cancelling";
|
|
8748
|
+
const selectedReadOnly = selectedQueued || selectedCancelling;
|
|
8749
|
+
const placeholder = mobile && !promptState.editingPending ? (promptState.answerMode ? "Answer…" : "Message…") : promptState.answerMode
|
|
8750
|
+
? "Type an answer and press Enter"
|
|
8751
|
+
: promptState.editingPending
|
|
8752
|
+
? selectedCancelling
|
|
8753
|
+
? "Cancellation requested"
|
|
8754
|
+
: selectedQueued
|
|
8755
|
+
? "Queued message selected"
|
|
8756
|
+
: "Edit the pending message, then send or cancel it"
|
|
8757
|
+
: promptState.hasOutbox
|
|
8758
|
+
? "Type a message and press Enter to queue it behind the pending batch"
|
|
8759
|
+
: "Type a message and press Enter";
|
|
8637
8760
|
|
|
8638
8761
|
// Auto-grow: one line idle, sized to the RENDERED content (scrollHeight
|
|
8639
8762
|
// sees soft wrap; counting "\n" does not). CSS max-height provides the
|
|
@@ -8661,7 +8784,10 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
8661
8784
|
const mayHaveShrunk = length <= lastPromptLengthRef.current;
|
|
8662
8785
|
lastPromptLengthRef.current = length;
|
|
8663
8786
|
growInput(mayHaveShrunk || length === 0);
|
|
8664
|
-
|
|
8787
|
+
// Empty textareas measure their placeholder too. Send briefly shows
|
|
8788
|
+
// the long outbox hint; acknowledgement shortens it without changing
|
|
8789
|
+
// the value or width, so neither input nor ResizeObserver will fire.
|
|
8790
|
+
}, [growInput, promptState.value, placeholder]);
|
|
8665
8791
|
React.useEffect(() => {
|
|
8666
8792
|
// Width changes re-wrap the content; re-measure on viewport resizes
|
|
8667
8793
|
// (covers rotation and the on-screen keyboard shrinking the pane).
|
|
@@ -8724,6 +8850,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
8724
8850
|
const inputNode = inputRef.current;
|
|
8725
8851
|
if (!active || promptState.modalOpen || !promptState.focused || !inputNode) return;
|
|
8726
8852
|
if (document.activeElement !== inputNode) {
|
|
8853
|
+
if (!autoFocus) return;
|
|
8727
8854
|
// Programmatic focus pops the on-screen keyboard on touch devices;
|
|
8728
8855
|
// there, focus only ever comes from the user's own tap.
|
|
8729
8856
|
if (mobile) return;
|
|
@@ -8743,7 +8870,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
8743
8870
|
if (inputNode.value === promptState.value && inputNode.selectionStart !== promptState.cursor) {
|
|
8744
8871
|
inputNode.setSelectionRange(promptState.cursor, promptState.cursor);
|
|
8745
8872
|
}
|
|
8746
|
-
}, [active, mobile, promptState.cursor, promptState.value, promptState.focused, promptState.modalOpen]);
|
|
8873
|
+
}, [active, mobile, autoFocus, promptState.cursor, promptState.value, promptState.focused, promptState.modalOpen]);
|
|
8747
8874
|
|
|
8748
8875
|
const sendPrompt = React.useCallback(() => {
|
|
8749
8876
|
controller.handleCommand(UI_COMMANDS.SEND_PROMPT)
|
|
@@ -8780,12 +8907,9 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
8780
8907
|
: promptState.hasOutbox
|
|
8781
8908
|
? "+"
|
|
8782
8909
|
: "❯";
|
|
8783
|
-
const selectedQueued = promptState.selectedOutboxPhase === "queued";
|
|
8784
|
-
const selectedCancelling = promptState.selectedOutboxPhase === "cancelling";
|
|
8785
|
-
const selectedReadOnly = selectedQueued || selectedCancelling;
|
|
8786
8910
|
|
|
8787
8911
|
return React.createElement("div", {
|
|
8788
|
-
className: `ps-prompt-shell${mobile ? " is-mobile" : ""}${dragOver ? " is-drag-over" : ""}`,
|
|
8912
|
+
className: `ps-prompt-shell${compact ? " is-compact" : ""}${mobile ? " is-mobile" : ""}${dragOver ? " is-drag-over" : ""}`,
|
|
8789
8913
|
...(canAttachImages ? {
|
|
8790
8914
|
onDragOver: (event) => {
|
|
8791
8915
|
if (event.dataTransfer?.types?.includes?.("Files")) {
|
|
@@ -8852,17 +8976,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
8852
8976
|
rows: 1,
|
|
8853
8977
|
value: promptState.value,
|
|
8854
8978
|
readOnly: selectedReadOnly,
|
|
8855
|
-
placeholder
|
|
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",
|
|
8979
|
+
placeholder,
|
|
8866
8980
|
// On touch, Enter inserts a newline (the chevron sends) — the
|
|
8867
8981
|
// keyboard must not advertise a send that will not happen.
|
|
8868
8982
|
enterKeyHint: mobile ? "enter" : "send",
|
|
@@ -8913,7 +9027,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
8913
9027
|
// pointerdown preventDefault on every action keeps focus in the
|
|
8914
9028
|
// textarea — on a phone, tapping Send must not collapse the
|
|
8915
9029
|
// keyboard between messages.
|
|
8916
|
-
canAttachImages
|
|
9030
|
+
canAttachImages && !compact
|
|
8917
9031
|
? React.createElement(React.Fragment, null,
|
|
8918
9032
|
React.createElement("input", {
|
|
8919
9033
|
ref: attachInputRef,
|
|
@@ -8950,7 +9064,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
8950
9064
|
onClick: cancelPending,
|
|
8951
9065
|
}, selectedQueued ? "Delete" : "Cancel")
|
|
8952
9066
|
: null,
|
|
8953
|
-
promptState.canStopTurn || stoppingTurn
|
|
9067
|
+
!compact && (promptState.canStopTurn || stoppingTurn)
|
|
8954
9068
|
? React.createElement("button", {
|
|
8955
9069
|
type: "button",
|
|
8956
9070
|
className: `ps-stop-button${stoppingTurn ? " is-stopping" : ""}`,
|
|
@@ -9006,14 +9120,29 @@ function IconButton({ icon, label, onClick, disabled = false, active = false, cl
|
|
|
9006
9120
|
const timerRef = React.useRef(null);
|
|
9007
9121
|
const longPressRef = React.useRef(false);
|
|
9008
9122
|
|
|
9009
|
-
//
|
|
9010
|
-
// buttons would otherwise clip off the edge (the tooltip is center-anchored).
|
|
9123
|
+
// Measure wrapped text against the visual viewport, including the keyboard.
|
|
9011
9124
|
React.useLayoutEffect(() => {
|
|
9012
|
-
if (!tip || !tipRef.current ||
|
|
9013
|
-
const
|
|
9014
|
-
const
|
|
9015
|
-
const
|
|
9016
|
-
|
|
9125
|
+
if (!tip || !tipRef.current || !btnRef.current) return;
|
|
9126
|
+
const el = tipRef.current, viewport = window.visualViewport;
|
|
9127
|
+
const update = () => {
|
|
9128
|
+
const left = viewport?.offsetLeft || 0, top = viewport?.offsetTop || 0;
|
|
9129
|
+
const width = viewport?.width || window.innerWidth, height = viewport?.height || window.innerHeight;
|
|
9130
|
+
const margin = 6, r = btnRef.current.getBoundingClientRect();
|
|
9131
|
+
el.style.width = "max-content";
|
|
9132
|
+
el.style.maxWidth = `${Math.min(220, width - 2 * margin)}px`;
|
|
9133
|
+
el.style.maxHeight = `${height - 2 * margin}px`;
|
|
9134
|
+
const box = el.getBoundingClientRect();
|
|
9135
|
+
let below = tip.placement === "below";
|
|
9136
|
+
if (!below && r.top - box.height - margin < top) below = true;
|
|
9137
|
+
if (below && r.bottom + box.height + margin > top + height && r.top - box.height - margin >= top) below = false;
|
|
9138
|
+
const x = Math.max(left + margin, Math.min(r.left + r.width / 2 - box.width / 2, left + width - box.width - margin));
|
|
9139
|
+
const y = Math.max(top + margin, Math.min(below ? r.bottom + margin : r.top - box.height - margin, top + height - box.height - margin));
|
|
9140
|
+
el.style.transform = "none";
|
|
9141
|
+
el.style.left = `${x}px`; el.style.top = `${y}px`;
|
|
9142
|
+
};
|
|
9143
|
+
update(); viewport?.addEventListener("resize", update); viewport?.addEventListener("scroll", update);
|
|
9144
|
+
window.addEventListener("resize", update);
|
|
9145
|
+
return () => { viewport?.removeEventListener("resize", update); viewport?.removeEventListener("scroll", update); window.removeEventListener("resize", update); };
|
|
9017
9146
|
}, [tip]);
|
|
9018
9147
|
|
|
9019
9148
|
const reveal = (preferAbove = false) => {
|
|
@@ -9135,7 +9264,7 @@ function IconButton({ icon, label, onClick, disabled = false, active = false, cl
|
|
|
9135
9264
|
tooltipNode);
|
|
9136
9265
|
}
|
|
9137
9266
|
|
|
9138
|
-
function Toolbar({ controller, mobile, canvasPaneOpen = false, onToggleCanvasPane = null, mobilePane = "workspace", onSelectMobilePane = null, mobileMainLayout = "split" }) {
|
|
9267
|
+
function Toolbar({ controller, mobile, moa = null, canvasPaneOpen = false, onToggleCanvasPane = null, mobilePane = "workspace", onSelectMobilePane = null, mobileMainLayout = "split" }) {
|
|
9139
9268
|
const [headerSlot, setHeaderSlot] = React.useState(null);
|
|
9140
9269
|
React.useEffect(() => {
|
|
9141
9270
|
if (typeof document === "undefined") return;
|
|
@@ -9274,10 +9403,21 @@ function Toolbar({ controller, mobile, canvasPaneOpen = false, onToggleCanvasPan
|
|
|
9274
9403
|
icon: React.createElement(MainLayoutGlyph),
|
|
9275
9404
|
label: "Workspace — sessions, chat and panels",
|
|
9276
9405
|
onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_WORKSPACE).catch(() => {}),
|
|
9277
|
-
active: !adminVisible && !budgetOpen,
|
|
9406
|
+
active: !moa?.active && !adminVisible && !budgetOpen,
|
|
9278
9407
|
}]),
|
|
9279
9408
|
];
|
|
9280
9409
|
|
|
9410
|
+
if (moa) buttonDefs.push({
|
|
9411
|
+
key: "moa", icon: React.createElement(MoaGlyph),
|
|
9412
|
+
label: moa.returnTo ? "Back to MoA — Master of Agents" : "Master of Agents",
|
|
9413
|
+
active: moa.active, disabled: !moa.loaded,
|
|
9414
|
+
onClick: () => {
|
|
9415
|
+
controller.dispatch({ type: "ui/canvasMaximized", on: false });
|
|
9416
|
+
controller.handleCommand(UI_COMMANDS.OPEN_WORKSPACE).catch(() => {});
|
|
9417
|
+
moa.open();
|
|
9418
|
+
},
|
|
9419
|
+
});
|
|
9420
|
+
|
|
9281
9421
|
const renderButton = (def) => {
|
|
9282
9422
|
const button = React.createElement(IconButton, {
|
|
9283
9423
|
key: def.badge ? undefined : def.key,
|
|
@@ -9331,8 +9471,9 @@ function Toolbar({ controller, mobile, canvasPaneOpen = false, onToggleCanvasPan
|
|
|
9331
9471
|
];
|
|
9332
9472
|
return React.createElement("div", { className: "ps-toolbar is-mobile" },
|
|
9333
9473
|
React.createElement("div", { className: "ps-toolbar-row ps-toolbar-row-primary" },
|
|
9334
|
-
React.createElement("div", { className: "ps-toolbar-row-actions" }, buttonDefs.map(renderButton)),
|
|
9474
|
+
React.createElement("div", { className: "ps-toolbar-row-actions" }, buttonDefs.filter(def => def.key !== "moa").map(renderButton)),
|
|
9335
9475
|
React.createElement("div", { className: "ps-toolbar-row-actions is-panes" },
|
|
9476
|
+
...buttonDefs.filter(def => def.key === "moa").map(renderButton),
|
|
9336
9477
|
paneDefs.map((def) => React.createElement(IconButton, {
|
|
9337
9478
|
key: def.id === "workspace" ? "workspace" : "diagnostics",
|
|
9338
9479
|
icon: React.createElement(def.icon),
|
|
@@ -9355,10 +9496,10 @@ function Toolbar({ controller, mobile, canvasPaneOpen = false, onToggleCanvasPan
|
|
|
9355
9496
|
// mode. Budget and the Admin Console replace the workspace; nothing on
|
|
9356
9497
|
// the left applies there. Modes are exclusive (the controller closes one
|
|
9357
9498
|
// when the other opens) and the Workspace button is the way back.
|
|
9358
|
-
const mode = adminVisible ? "admin" : (budgetOpen ? "budget" : "workspace");
|
|
9499
|
+
const mode = moa?.active ? "moa" : adminVisible ? "admin" : (budgetOpen ? "budget" : "workspace");
|
|
9359
9500
|
const ACTIONS = ["new", "filter"];
|
|
9360
9501
|
const PANELS = ["canvas", "diagnostics"];
|
|
9361
|
-
const MODES = ["workspace", "budget", "admin"];
|
|
9502
|
+
const MODES = ["workspace", "moa", "budget", "admin"];
|
|
9362
9503
|
|
|
9363
9504
|
// While the canvas is full screen, ANY other button first drops full
|
|
9364
9505
|
// screen and then does its own job. Pressing Filter and watching nothing
|
|
@@ -9366,10 +9507,11 @@ function Toolbar({ controller, mobile, canvasPaneOpen = false, onToggleCanvasPan
|
|
|
9366
9507
|
// it — is the confusing half of every full-screen mode. The Canvas toggle
|
|
9367
9508
|
// is exempt: it already means "put the canvas away", and closing drops
|
|
9368
9509
|
// the flag in the reducer.
|
|
9369
|
-
const withRestore = (def) => (!canvasMaximized || def.key === "canvas" ? def : {
|
|
9510
|
+
const withRestore = (def) => ((!canvasMaximized || def.key === "canvas") && !(moa?.active && ["workspace", "budget", "admin"].includes(def.key)) ? def : {
|
|
9370
9511
|
...def,
|
|
9371
9512
|
onClick: (...args) => {
|
|
9372
9513
|
controller.dispatch({ type: "ui/canvasMaximized", on: false });
|
|
9514
|
+
if (moa?.active && ["workspace", "budget", "admin"].includes(def.key)) moa.leave();
|
|
9373
9515
|
return def.onClick?.(...args);
|
|
9374
9516
|
},
|
|
9375
9517
|
});
|
|
@@ -9390,14 +9532,14 @@ function Toolbar({ controller, mobile, canvasPaneOpen = false, onToggleCanvasPan
|
|
|
9390
9532
|
...pick(["theme"]),
|
|
9391
9533
|
];
|
|
9392
9534
|
|
|
9393
|
-
const toolbar = React.createElement("div", { className:
|
|
9535
|
+
const toolbar = React.createElement("div", { className: `ps-toolbar${moa?.active ? " is-moa" : ""}` },
|
|
9394
9536
|
// Three columns: left rail | main controls | right rail. Full-screen
|
|
9395
9537
|
// canvas uses the otherwise-empty left rail for the normal actions so
|
|
9396
9538
|
// they do not crowd the canvas metadata and controls across the top.
|
|
9397
9539
|
React.createElement("div", { className: "ps-toolbar-side is-left" },
|
|
9398
|
-
canvasMaximized ? leftCluster : null),
|
|
9540
|
+
moa?.active ? React.createElement("div", { id: "ps-moa-status-slot" }) : canvasMaximized ? leftCluster : null),
|
|
9399
9541
|
React.createElement("div", { className: "ps-toolbar-actions" },
|
|
9400
|
-
canvasMaximized ? null : leftCluster),
|
|
9542
|
+
moa?.active ? React.createElement("div", { id: "ps-moa-header-slot" }) : canvasMaximized ? null : leftCluster),
|
|
9401
9543
|
React.createElement("div", { className: "ps-toolbar-side is-right" },
|
|
9402
9544
|
// The portal header parks its version/status meta here, LEFT of
|
|
9403
9545
|
// the tool buttons, and its sign-out glyph in the slot after
|
|
@@ -10129,32 +10271,30 @@ function budgetPlural(n, one, many) {
|
|
|
10129
10271
|
return `${n} ${n === 1 ? one : many}`;
|
|
10130
10272
|
}
|
|
10131
10273
|
|
|
10132
|
-
/**
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
* instance of that type with a name of its own, and both a model-scoped limit
|
|
10137
|
-
* and a default tuple are refused by the database unless the model half names
|
|
10138
|
-
* the provider it belongs to.
|
|
10139
|
-
*/
|
|
10140
|
-
function budgetModelsForProvider(models, provider) {
|
|
10141
|
-
if (!provider?.typeId) return [];
|
|
10142
|
-
return models
|
|
10143
|
-
.filter((model) => model.providerId === provider.typeId)
|
|
10144
|
-
.map((model) => ({
|
|
10145
|
-
qualifiedName: `${provider.name}:${model.modelName}`,
|
|
10146
|
-
modelName: model.modelName,
|
|
10147
|
-
}));
|
|
10148
|
-
}
|
|
10149
|
-
|
|
10150
|
-
/** The provider TYPES this deployment can instantiate, from its model catalog. */
|
|
10151
|
-
function budgetProviderTypes(models) {
|
|
10152
|
-
const seen = new Map();
|
|
10274
|
+
/** Runtime catalogs name a pool; type catalogs name a provider template. */
|
|
10275
|
+
function budgetModelsForProvider(models, provider, rawRows = []) {
|
|
10276
|
+
if (!provider?.name) return [];
|
|
10277
|
+
const choices = new Map();
|
|
10153
10278
|
for (const model of models) {
|
|
10154
|
-
|
|
10155
|
-
|
|
10279
|
+
const matches = model.catalogKind === "runtime_provider"
|
|
10280
|
+
? model.providerId === provider.name
|
|
10281
|
+
: model.catalogKind === "provider_type"
|
|
10282
|
+
? model.providerId === provider.typeId
|
|
10283
|
+
: model.providerId === provider.name || model.providerId === provider.typeId;
|
|
10284
|
+
if (!matches) continue;
|
|
10285
|
+
const qualifiedName = `${provider.name}:${model.modelName}`;
|
|
10286
|
+
choices.set(qualifiedName, { qualifiedName, modelName: model.modelName });
|
|
10287
|
+
}
|
|
10288
|
+
// Existing caps remain editable even when the catalog is unavailable or a
|
|
10289
|
+
// model was retired. Never borrow a model reference from another pool.
|
|
10290
|
+
for (const row of rawRows) {
|
|
10291
|
+
if (row.providerName !== provider.name || row.rowKind !== "model"
|
|
10292
|
+
|| !String(row.scope || "").startsWith(`${provider.name}:`)) continue;
|
|
10293
|
+
choices.set(row.scope, {
|
|
10294
|
+
qualifiedName: row.scope, modelName: row.scope.slice(provider.name.length + 1),
|
|
10295
|
+
});
|
|
10156
10296
|
}
|
|
10157
|
-
return [...
|
|
10297
|
+
return [...choices.values()];
|
|
10158
10298
|
}
|
|
10159
10299
|
|
|
10160
10300
|
/** listModels() is flat on the web transport and grouped on the direct one. */
|
|
@@ -10164,15 +10304,18 @@ function budgetNormalizeModels(raw) {
|
|
|
10164
10304
|
: (Array.isArray(raw?.providers) ? raw.providers : (Array.isArray(raw?.models) ? raw.models : []));
|
|
10165
10305
|
const flat = top.flatMap((entry) => (
|
|
10166
10306
|
Array.isArray(entry?.models)
|
|
10167
|
-
? entry.models.map((model) => ({
|
|
10307
|
+
? entry.models.map((model) => ({
|
|
10308
|
+
...model, providerId: model.providerId || entry.providerId,
|
|
10309
|
+
catalogKind: model.catalogKind || entry.catalogKind,
|
|
10310
|
+
}))
|
|
10168
10311
|
: [entry]
|
|
10169
10312
|
));
|
|
10170
10313
|
return flat
|
|
10171
10314
|
.filter((model) => model && (model.modelName || model.qualifiedName))
|
|
10172
10315
|
.map((model) => ({
|
|
10173
|
-
modelName: model.modelName || String(model.qualifiedName).split(":").
|
|
10316
|
+
modelName: model.modelName || String(model.qualifiedName).split(":").slice(1).join(":"),
|
|
10174
10317
|
providerId: model.providerId || "",
|
|
10175
|
-
|
|
10318
|
+
catalogKind: model.catalogKind || null,
|
|
10176
10319
|
}));
|
|
10177
10320
|
}
|
|
10178
10321
|
|
|
@@ -10710,7 +10853,7 @@ function ProviderSystemSpend({ view }) {
|
|
|
10710
10853
|
* reachable.
|
|
10711
10854
|
*/
|
|
10712
10855
|
function EditProviderSheet({
|
|
10713
|
-
row, rawRows, models, isAdmin, busy, error, onCancel, onRun,
|
|
10856
|
+
row, rawRows, models, modelsLoading, modelsError, onRetryModels, isAdmin, busy, error, onCancel, onRun,
|
|
10714
10857
|
initialPeriod = "day", initialScope = "*",
|
|
10715
10858
|
}) {
|
|
10716
10859
|
const shared = row.class === "shared";
|
|
@@ -10854,10 +10997,13 @@ function EditProviderSheet({
|
|
|
10854
10997
|
className: "ps-budget-input", "aria-label": "Model this limit applies to",
|
|
10855
10998
|
value: model, onChange: (event) => setModel(event.target.value),
|
|
10856
10999
|
}, models.length === 0
|
|
10857
|
-
? React.createElement("option", { value: "" }, "No models
|
|
11000
|
+
? React.createElement("option", { value: "" }, modelsLoading ? "Loading models…" : modelsError ? "Model catalog unavailable" : "No models listed for this provider")
|
|
10858
11001
|
: models.map((option) => React.createElement("option", {
|
|
10859
11002
|
key: option.qualifiedName, value: option.qualifiedName,
|
|
10860
|
-
}, option.qualifiedName))) : null
|
|
11003
|
+
}, option.qualifiedName))) : null,
|
|
11004
|
+
modelsError ? React.createElement("div", { className: "ps-budget-note is-warn", role: "status" },
|
|
11005
|
+
"Could not load the model catalog. Existing model limits remain editable. ",
|
|
11006
|
+
React.createElement("button", { type: "button", onClick: onRetryModels, disabled: modelsLoading }, "Retry")) : null),
|
|
10861
11007
|
React.createElement(SheetField, {
|
|
10862
11008
|
label: "Tokens",
|
|
10863
11009
|
hint: parsed
|
|
@@ -11643,6 +11789,9 @@ function ProviderBudgetView({ controller }) {
|
|
|
11643
11789
|
// a failure is not fatal — the controls that need it say they have nothing
|
|
11644
11790
|
// to offer.
|
|
11645
11791
|
const [models, setModels] = React.useState([]);
|
|
11792
|
+
const [modelsLoading, setModelsLoading] = React.useState(true);
|
|
11793
|
+
const [modelsError, setModelsError] = React.useState(false);
|
|
11794
|
+
const [modelsRequest, setModelsRequest] = React.useState(0);
|
|
11646
11795
|
const [providerTypes, setProviderTypes] = React.useState(new Map());
|
|
11647
11796
|
const [defaults, setDefaults] = React.useState(null);
|
|
11648
11797
|
const readDefaults = React.useCallback(() => {
|
|
@@ -11653,11 +11802,14 @@ function ProviderBudgetView({ controller }) {
|
|
|
11653
11802
|
}, [controller]);
|
|
11654
11803
|
React.useEffect(() => {
|
|
11655
11804
|
let live = true;
|
|
11656
|
-
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
|
|
11660
|
-
}
|
|
11805
|
+
setModelsLoading(true);
|
|
11806
|
+
Promise.resolve().then(() => {
|
|
11807
|
+
if (typeof controller.transport.listModels !== "function") throw new Error("Model catalog unavailable");
|
|
11808
|
+
return controller.transport.listModels();
|
|
11809
|
+
}).then((rows) => {
|
|
11810
|
+
if (live) { setModels(budgetNormalizeModels(rows)); setModelsError(false); }
|
|
11811
|
+
}).catch(() => { if (live) setModelsError(true); })
|
|
11812
|
+
.finally(() => { if (live) setModelsLoading(false); });
|
|
11661
11813
|
if (typeof controller.transport.listProviders === "function") {
|
|
11662
11814
|
Promise.resolve(controller.transport.listProviders())
|
|
11663
11815
|
.then((rows) => {
|
|
@@ -11670,7 +11822,7 @@ function ProviderBudgetView({ controller }) {
|
|
|
11670
11822
|
}
|
|
11671
11823
|
readDefaults();
|
|
11672
11824
|
return () => { live = false; };
|
|
11673
|
-
}, [controller, readDefaults]);
|
|
11825
|
+
}, [controller, readDefaults, modelsRequest]);
|
|
11674
11826
|
|
|
11675
11827
|
// Opened by the toolbar, which loads it. Mounting with nothing behind it —
|
|
11676
11828
|
// a restored state, a second mount — reads once rather than showing zeros.
|
|
@@ -11755,13 +11907,14 @@ function ProviderBudgetView({ controller }) {
|
|
|
11755
11907
|
const modelsForSelected = selected
|
|
11756
11908
|
? budgetModelsForProvider(models, {
|
|
11757
11909
|
name: selected.providerName, typeId: providerTypes.get(selected.providerName) || "",
|
|
11758
|
-
})
|
|
11910
|
+
}, rawRows)
|
|
11759
11911
|
: [];
|
|
11760
11912
|
|
|
11761
11913
|
let sheetEl = null;
|
|
11762
11914
|
if (sheet?.kind === "edit" && selectedProvider) {
|
|
11763
11915
|
sheetEl = React.createElement(EditProviderSheet, {
|
|
11764
11916
|
row: selectedProvider, rawRows, models: modelsForSelected, isAdmin: view.isAdmin,
|
|
11917
|
+
modelsLoading, modelsError, onRetryModels: () => setModelsRequest((value) => value + 1),
|
|
11765
11918
|
// MAJOR 17: the sheet opens on the limit the reader was standing
|
|
11766
11919
|
// on, rather than always on Daily / all models.
|
|
11767
11920
|
initialPeriod: sheet.period || "day",
|
|
@@ -12596,8 +12749,17 @@ function DistillerModelPickers({ controller, extras }) {
|
|
|
12596
12749
|
);
|
|
12597
12750
|
}
|
|
12598
12751
|
|
|
12599
|
-
function
|
|
12752
|
+
function ScopedModalLayer({ controller }) {
|
|
12753
|
+
const modal = useControllerSelector(controller, state => state.ui.modal);
|
|
12754
|
+
// Isolated MoA controllers own keyboard commands only while their dialog
|
|
12755
|
+
// is open. They must never install normal-workspace shortcuts globally.
|
|
12756
|
+
useKeyboardShortcuts(controller, false, !modal);
|
|
12757
|
+
return React.createElement(ModalLayer, { controller });
|
|
12758
|
+
}
|
|
12759
|
+
|
|
12760
|
+
function useKeyboardShortcuts(controller, mobile, suspended = false) {
|
|
12600
12761
|
React.useEffect(() => {
|
|
12762
|
+
if (suspended) return undefined;
|
|
12601
12763
|
const handler = (event) => {
|
|
12602
12764
|
// A focused control that handled the key owns it. The diagnostics
|
|
12603
12765
|
// seam answers ArrowUp/ArrowDown itself; letting the same keydown
|
|
@@ -12942,7 +13104,7 @@ function useKeyboardShortcuts(controller, mobile) {
|
|
|
12942
13104
|
|
|
12943
13105
|
window.addEventListener("keydown", handler);
|
|
12944
13106
|
return () => window.removeEventListener("keydown", handler);
|
|
12945
|
-
}, [controller, mobile]);
|
|
13107
|
+
}, [controller, mobile, suspended]);
|
|
12946
13108
|
}
|
|
12947
13109
|
|
|
12948
13110
|
function formatAdminPrincipalLabel(principal) {
|
|
@@ -14143,11 +14305,11 @@ export function createWebPilotSwarmController({ transport, mode = "remote", bran
|
|
|
14143
14305
|
return new PilotSwarmUiController({ store, transport });
|
|
14144
14306
|
}
|
|
14145
14307
|
|
|
14146
|
-
export function PilotSwarmWebApp({ controller }) {
|
|
14308
|
+
export function PilotSwarmWebApp({ controller, suspended = false, moa = null }) {
|
|
14147
14309
|
const viewportRef = React.useRef(null);
|
|
14148
14310
|
const mainGridRef = React.useRef(null);
|
|
14149
|
-
const viewport = useMeasuredViewport(viewportRef);
|
|
14150
|
-
const mainGridViewport = useMeasuredViewport(mainGridRef);
|
|
14311
|
+
const viewport = useMeasuredViewport(viewportRef, suspended);
|
|
14312
|
+
const mainGridViewport = useMeasuredViewport(mainGridRef, suspended);
|
|
14151
14313
|
const gridViewport = computeGridViewport(viewport);
|
|
14152
14314
|
|
|
14153
14315
|
// Publish the real viewport in character cells. Without this the ui-core
|
|
@@ -14209,6 +14371,7 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
14209
14371
|
// toolbar button). Desktop has real columns and needs no such cycle.
|
|
14210
14372
|
const [mobileMainLayout, setMobileMainLayout] = React.useState("split");
|
|
14211
14373
|
const state = useControllerSelector(controller, (rootState) => ({
|
|
14374
|
+
moa: rootState.ui.moa,
|
|
14212
14375
|
themeId: rootState.ui.themeId,
|
|
14213
14376
|
touchScale: Boolean(rootState.ui.touchScale),
|
|
14214
14377
|
sessionDetailCollapsed: Boolean(rootState.ui.sessionDetailCollapsed),
|
|
@@ -14224,6 +14387,7 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
14224
14387
|
// Conversion to a sorted array happens inside `normalizeProfileSettings`.
|
|
14225
14388
|
collapsedSessionIds: rootState.sessions.collapsedIds,
|
|
14226
14389
|
activeSessionId: rootState.sessions.activeSessionId || null,
|
|
14390
|
+
revealedCreatedSessionId: rootState.ui.revealedCreatedSessionId || null,
|
|
14227
14391
|
promptRows: getStatePromptRows(rootState),
|
|
14228
14392
|
rightPaneMode: rootState.ui.rightPaneMode || "panes",
|
|
14229
14393
|
// The two optional desktop columns, independent of each other.
|
|
@@ -14278,7 +14442,21 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
14278
14442
|
const readOnlyChatPane = state.activeSessionIsGroup;
|
|
14279
14443
|
const effectivePromptRows = readOnlyChatPane ? 0 : state.promptRows;
|
|
14280
14444
|
|
|
14281
|
-
useKeyboardShortcuts(controller, mobile);
|
|
14445
|
+
useKeyboardShortcuts(controller, mobile, suspended);
|
|
14446
|
+
|
|
14447
|
+
const lastCreatedSessionRef = React.useRef(state.revealedCreatedSessionId);
|
|
14448
|
+
React.useEffect(() => {
|
|
14449
|
+
if (lastCreatedSessionRef.current === state.revealedCreatedSessionId) return;
|
|
14450
|
+
lastCreatedSessionRef.current = state.revealedCreatedSessionId;
|
|
14451
|
+
if (!state.revealedCreatedSessionId || state.activeSessionId !== state.revealedCreatedSessionId) return;
|
|
14452
|
+
if (mobile) {
|
|
14453
|
+
setMobilePane("workspace");
|
|
14454
|
+
setMobileMainLayout("chat");
|
|
14455
|
+
}
|
|
14456
|
+
// Creation opens the ordinary session workspace, without modifying
|
|
14457
|
+
// any of the user's saved Master of Agents panels.
|
|
14458
|
+
if (moa?.active) moa.leave();
|
|
14459
|
+
}, [state.revealedCreatedSessionId, state.activeSessionId, mobile, moa]);
|
|
14282
14460
|
|
|
14283
14461
|
// Tell the reducer which device slot of the per-session views this tab
|
|
14284
14462
|
// owns. Only a desktop applies and records them; a phone keeps its
|
|
@@ -14440,7 +14618,7 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
14440
14618
|
);
|
|
14441
14619
|
const settingsJson = JSON.stringify(settings);
|
|
14442
14620
|
const currentSettingsBeforeApply = profileSettingsFromViewState(
|
|
14443
|
-
controller.getState(), otherTouchScaleRef.current, desktopRightPaneModeRef.current, desktopPanesRef.current,
|
|
14621
|
+
profileViewState(controller.getState()), otherTouchScaleRef.current, desktopRightPaneModeRef.current, desktopPanesRef.current,
|
|
14444
14622
|
);
|
|
14445
14623
|
const currentSettingsBeforeApplyJson = JSON.stringify(currentSettingsBeforeApply);
|
|
14446
14624
|
const hasUnpersistedLocalChange = profileSettingsHydratedRef.current
|
|
@@ -14448,7 +14626,8 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
14448
14626
|
&& currentSettingsBeforeApplyJson !== lastProfileSettingsJsonRef.current;
|
|
14449
14627
|
const hasPendingLocalWrite = Boolean(profileSettingsSaveTimerRef.current)
|
|
14450
14628
|
|| profileSettingsSaveInFlightRef.current
|
|
14451
|
-
|| hasUnpersistedLocalChange
|
|
14629
|
+
|| hasUnpersistedLocalChange
|
|
14630
|
+
|| controller.getState().ui.moaDirty === true;
|
|
14452
14631
|
if (!hasPendingLocalWrite && appliedProfileSettingsJsonRef.current !== settingsJson) {
|
|
14453
14632
|
controller.dispatch({ type: "profileSettings/apply", settings });
|
|
14454
14633
|
appliedProfileSettingsJsonRef.current = settingsJson;
|
|
@@ -14483,7 +14662,7 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
14483
14662
|
// remote state by definition and there is nothing to lose).
|
|
14484
14663
|
if (!profileSettingsHydratedRef.current || !hasPendingLocalWrite) {
|
|
14485
14664
|
const currentSettings = profileSettingsFromViewState(
|
|
14486
|
-
controller.getState(), otherTouchScaleRef.current, desktopRightPaneModeRef.current, desktopPanesRef.current,
|
|
14665
|
+
profileViewState(controller.getState()), otherTouchScaleRef.current, desktopRightPaneModeRef.current, desktopPanesRef.current,
|
|
14487
14666
|
);
|
|
14488
14667
|
lastProfileSettingsJsonRef.current = JSON.stringify(currentSettings);
|
|
14489
14668
|
}
|
|
@@ -14527,14 +14706,20 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
14527
14706
|
profileSettingsSaveTimerRef.current = setTimeout(() => {
|
|
14528
14707
|
profileSettingsSaveTimerRef.current = null;
|
|
14529
14708
|
profileSettingsSaveInFlightRef.current = true;
|
|
14709
|
+
const moaRevision = controller.getState().ui.moaRevision;
|
|
14710
|
+
controller.dispatch({ type: "ui/moaSaveStatus", status: "saving" });
|
|
14530
14711
|
saveProfileSettings(controller, settings)
|
|
14531
|
-
.
|
|
14712
|
+
.then(() => controller.dispatch({ type: "ui/moaSaveStatus", status: "saved", revision: moaRevision }))
|
|
14713
|
+
.catch(() => {
|
|
14714
|
+
lastProfileSettingsJsonRef.current = null;
|
|
14715
|
+
controller.dispatch({ type: "ui/moaSaveStatus", status: "error" });
|
|
14716
|
+
})
|
|
14532
14717
|
.finally(() => {
|
|
14533
14718
|
profileSettingsSaveInFlightRef.current = false;
|
|
14534
14719
|
});
|
|
14535
14720
|
}, 400);
|
|
14536
14721
|
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]);
|
|
14722
|
+
}, [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
14723
|
|
|
14539
14724
|
React.useEffect(() => {
|
|
14540
14725
|
applyDocumentTheme(state.themeId);
|
|
@@ -14645,7 +14830,7 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
14645
14830
|
Number(state.canvasPrefs?.[state.activeSessionId]?.zenRailPx) || 380));
|
|
14646
14831
|
// Escape steps down one rung: full screen -> zen -> normal workspace.
|
|
14647
14832
|
React.useEffect(() => {
|
|
14648
|
-
if (!canvasMaximized && !zenActive) return undefined;
|
|
14833
|
+
if (suspended || (!canvasMaximized && !zenActive)) return undefined;
|
|
14649
14834
|
const onKey = (e) => {
|
|
14650
14835
|
if (e.key !== "Escape") return;
|
|
14651
14836
|
controller.dispatch(canvasMaximized
|
|
@@ -14654,7 +14839,7 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
14654
14839
|
};
|
|
14655
14840
|
window.addEventListener("keydown", onKey);
|
|
14656
14841
|
return () => window.removeEventListener("keydown", onKey);
|
|
14657
|
-
}, [controller, canvasMaximized, zenActive]);
|
|
14842
|
+
}, [controller, canvasMaximized, zenActive, suspended]);
|
|
14658
14843
|
|
|
14659
14844
|
// The right block is exactly as wide as the columns in it. Pixels, not a
|
|
14660
14845
|
// share of the window: when a column closes, its pixels go to chat and
|
|
@@ -14862,7 +15047,7 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
14862
15047
|
// it — the layer below is a SIBLING of this content and covers it. Drawing
|
|
14863
15048
|
// a pane here would only be invisible work.
|
|
14864
15049
|
else if (mobilePane === "canvas") mobileContent = null;
|
|
14865
|
-
else mobileContent = React.createElement(MobileWorkspace, { controller, layout: mobileMainLayout });
|
|
15050
|
+
else mobileContent = React.createElement(MobileWorkspace, { controller, layout: mobileMainLayout, onEnterZen: moa?.openMobileZen });
|
|
14866
15051
|
|
|
14867
15052
|
// The phone's canvas layer: a sibling of the content region's pane, NOT a
|
|
14868
15053
|
// child of any pane. That is deliberate — panes mount and unmount as the
|
|
@@ -14877,13 +15062,16 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
14877
15062
|
})
|
|
14878
15063
|
: null;
|
|
14879
15064
|
|
|
15065
|
+
if (suspended) return React.createElement(ControllerContext.Provider, { value: controller },
|
|
15066
|
+
React.createElement(Toolbar, { controller, mobile: false, moa }),
|
|
15067
|
+
React.createElement(ModalLayer, { controller }));
|
|
14880
15068
|
return React.createElement(ControllerContext.Provider, { value: controller },
|
|
14881
15069
|
React.createElement("div", { ref: viewportRef, className: "ps-web-shell" },
|
|
14882
15070
|
// The phone's toolbar carries its NAVIGATION (the three view modes,
|
|
14883
15071
|
// plus the Main layout cycle), so it renders on every pane — hiding it
|
|
14884
15072
|
// anywhere would trap the user with no way back.
|
|
14885
15073
|
React.createElement(Toolbar, {
|
|
14886
|
-
controller,
|
|
15074
|
+
controller, moa,
|
|
14887
15075
|
mobile,
|
|
14888
15076
|
canvasPaneOpen: mobileCanvasOpen,
|
|
14889
15077
|
onToggleCanvasPane: toggleMobileCanvas,
|
|
@@ -14921,3 +15109,6 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
14921
15109
|
: null,
|
|
14922
15110
|
React.createElement(ModalLayer, { controller })));
|
|
14923
15111
|
}
|
|
15112
|
+
|
|
15113
|
+
// Browser hosts may compose these existing surfaces with isolated controllers.
|
|
15114
|
+
export { SessionHeaderStatus, ChatPane, CanvasFrame, SessionPane, SessionRowContent, SessionDetailBox, SessionComposer, ScopedModalLayer, ControllerContext };
|