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.
- package/README.md +6 -0
- package/package.json +2 -2
- package/ui/core/src/controller.js +39 -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 +19 -2
- 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 +215 -77
- package/web/dist/assets/{abnfDiagram-VRR7QNED-D9Rv2VpV.js → abnfDiagram-VRR7QNED-CXEHJkTQ.js} +1 -1
- package/web/dist/assets/{arc-BATiucnq.js → arc-lInHlEVm.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-CSTMt0si.js → architectureDiagram-ZJ3FMSHR-vrocx6re.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-C_RGqfeY.js → blockDiagram-677ZJIJ3-B1KA5QIB.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-DH__s__0.js → c4Diagram-LMCZKHZV-Du9xJMjz.js} +1 -1
- package/web/dist/assets/channel-6wPrR6sC.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-C197fD3B.js → chunk-2Q5K7J3B-BuHucQOQ.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-BjpuBroU.js → chunk-32BRIVSS-T-AE5Yz0.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-W_jL8euj.js → chunk-5VM5RSS4-B_kTsNzt.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-a9h0dtlp.js → chunk-EX3LRPZG-CtqgceBQ.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-CQN0qE7F.js → chunk-JWPE2WC7-C_Fv9QUd.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-D-Y6KQ7t.js → chunk-MOJQB5TN-D5GeF8ZZ.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-jPwseC5f.js → chunk-RYQCIY6F-Cg63wDst.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-DRNCEcQh.js → chunk-V7JOEXUC-Bz6G3lev.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-DzhUcoZ7.js → chunk-VR4S4FIN-DKs_hvUD.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-ldJfyPXK.js → chunk-XXDRQBXY-B9AtUlbn.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-vvXyIfbC.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-vvXyIfbC.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-BBVo21wR.js → cose-bilkent-JH36ORCC-rAG6oS0i.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-CgRQfdW1.js → cynefin-VYW2F7L2-CUy2C8UT.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-Ckz2X0t3.js → cynefinDiagram-TSTJHNR4-C1W8blXH.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-C1SanJiY.js → dagre-VKFMJZFB-DC0XqwcC.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-dBdEifVy.js → diagram-FQU43EPY-COAY9zCP.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-Byqc1VCB.js → diagram-G47NLZAW-DLPr8eWG.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-9aeTaCsX.js → diagram-NH7WQ7WH-DoNgwoDz.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-CL0a0o0k.js → diagram-OA4YK3LP-D3-5TINE.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-YlMQOJfY.js → diagram-WEI45ONY-DplCkBJC.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-qMZQEnws.js → ebnfDiagram-CCIWWBDH-dvcVh7yr.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-C3CWBCtq.js → erDiagram-Q63AITRT-Cde8cTY6.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-BN79sybL.js → flowDiagram-23GEKE2U-MGMNsh2E.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-BOWhiAgB.js → ganttDiagram-NO4QXBWP-BiKw7AfS.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-CKX9OACp.js → gitGraphDiagram-IHSO6WYX-vC7fsylq.js} +1 -1
- package/web/dist/assets/index-D9Ab1VTY.css +1 -0
- package/web/dist/assets/index-DDAyy127.js +24 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-QQEunPNs.js → infoDiagram-FWYZ7A6U-Du1uirB0.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-CvMwrnLQ.js → ishikawaDiagram-FXEZZL3T-Dk--Qshr.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-giNpEVpj.js → journeyDiagram-5HDEW3XC-iDE4zf_N.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-CUPPpo27.js → kanban-definition-HUTT4EX6-D7WI8iRR.js} +1 -1
- package/web/dist/assets/{linear-AXqadfOR.js → linear-B1_kxCBK.js} +1 -1
- package/web/dist/assets/{mermaid.core-C78wdk8Y.js → mermaid.core-DNmC5O4Y.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-BGUmh9bK.js → mindmap-definition-LN4V7U3C-DIbLKLMA.js} +1 -1
- package/web/dist/assets/{papaparse.min-Bh8RLCk_.js → papaparse.min-BdWinjY-.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-BHsKXKhK.js → pegDiagram-2B236MQR-CkxxR07G.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-D3EvWTdH.js → pieDiagram-ENE6RG2P-Dxvp1mn7.js} +1 -1
- package/web/dist/assets/pilotswarm-493nqGmy.js +98 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-CbkxfLkV.js → quadrantDiagram-ABIIQ3AL-CrL0hjZW.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-H4M6JSDW.js → railroadDiagram-RFXS5EU6-CmVmFBPa.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-BJo7JW1v.js → requirementDiagram-TGXJPOKE-DJc3NRXb.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-DL19MeOP.js → sankeyDiagram-HTMAVEWB-BJNqWqBb.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-DyVFc5pQ.js → sequenceDiagram-DBY2YBRQ-C0wddECV.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-CKbcKIEQ.js → sizeCapture-X5ZJPWSS-Cw0dqPWS.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-BSukSXzS.js → stateDiagram-2N3HPSRC-C5793RHZ.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-qGaRayHj.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-BUd7UhsF.js → swimlanes-5IMT3BWC-Bhpm77Zu.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BXjNM_Zw.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-Dt1gludn.js → timeline-definition-FHXFAJF6-CwbE4NsH.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-BHxt00GV.js → vennDiagram-L72KCM5P-DZnJEL_w.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-BT3fIDbe.js → wardleyDiagram-EHGQE667-DwTgBDev.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-ChoYDS2b.js → xychartDiagram-FW5EYKEG-BiPjN3Qc.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,
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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" },
|
|
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 :
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
.
|
|
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 };
|