pilotswarm 0.5.39 → 0.5.41
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/package.json +2 -2
- package/tui/src/node-sdk-transport.js +25 -0
- package/ui/core/src/controller.js +34 -8
- package/ui/core/src/history.js +5 -1
- package/ui/core/src/index.js +1 -0
- package/ui/core/src/portal-links.js +46 -0
- package/ui/core/src/reducer.js +88 -2
- package/ui/core/src/selectors.js +2 -0
- package/ui/react/src/web-app.js +358 -36
- package/web/README.md +28 -0
- package/web/api/canvas-plane.js +116 -0
- package/web/api/ws.js +120 -5
- package/web/config.js +60 -0
- package/web/dist/assets/{abnfDiagram-VRR7QNED-Hsly1fgf.js → abnfDiagram-VRR7QNED-kpxu_PEa.js} +1 -1
- package/web/dist/assets/{arc-DiQLL9vM.js → arc-BnVQmtB5.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-B8ry8WNZ.js → architectureDiagram-ZJ3FMSHR-CDynQ_6_.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-Z4PcukIo.js → blockDiagram-677ZJIJ3-BUpYaSDb.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-Cahkc0gc.js → c4Diagram-LMCZKHZV-ZL8eskDB.js} +1 -1
- package/web/dist/assets/channel-Bi5A6sHR.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-CtZuWH4J.js → chunk-2Q5K7J3B-o0Nsyp1T.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-C0M390-k.js → chunk-32BRIVSS-CH5CSNRP.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-ClvX9u5j.js → chunk-5VM5RSS4-C9wwtPWL.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-BVAet5KG.js → chunk-EX3LRPZG--F9yFbhA.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-BccmZmrr.js → chunk-JWPE2WC7-BPIstyWL.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-BN5MKe94.js → chunk-MOJQB5TN-5Dm2u-0k.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-DfWw-ACP.js → chunk-RYQCIY6F-Dbtf9iQK.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-BgUEoLnN.js → chunk-V7JOEXUC-DrZEBJ5l.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-k1eKfFNA.js → chunk-VR4S4FIN-AqAUynqK.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-DPyV10yR.js → chunk-XXDRQBXY-DQlFaqCy.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-D8dCxI54.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-D8dCxI54.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-Bt_rpukK.js → cose-bilkent-JH36ORCC-RAtPG1Ou.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-Z8eMzFMv.js → cynefin-VYW2F7L2-CbeHaUWZ.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-BJSNawFi.js → cynefinDiagram-TSTJHNR4-B6paqF5Y.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-D6dNl8jc.js → dagre-VKFMJZFB-DziTBO2K.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-Cy1nbGsU.js → diagram-FQU43EPY-CPRUiLcr.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-CmrbuA_d.js → diagram-G47NLZAW-CIMVdcL1.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-PoLNQE2j.js → diagram-NH7WQ7WH-CHPFllxR.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-BZwNFcSV.js → diagram-OA4YK3LP-DwS6unBJ.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-pKw0b_fN.js → diagram-WEI45ONY-DKWgkObI.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-UNctuQCV.js → ebnfDiagram-CCIWWBDH-zAVWV6-D.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-DxIETMif.js → erDiagram-Q63AITRT-D0tYX4h0.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-C06IdNKV.js → flowDiagram-23GEKE2U-Cfq4I7R0.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-fU7aGyP1.js → ganttDiagram-NO4QXBWP-wgBAHr5I.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-9UKkPJm6.js → gitGraphDiagram-IHSO6WYX-BTIPX5PR.js} +1 -1
- package/web/dist/assets/index-OHj-gGHp.js +24 -0
- package/web/dist/assets/index-eTH1FBtN.css +1 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-C6F06SQJ.js → infoDiagram-FWYZ7A6U-CRLiQ87D.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-CB-O9NfO.js → ishikawaDiagram-FXEZZL3T-CwzwUis1.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-B-8MOf-H.js → journeyDiagram-5HDEW3XC-B9pqMeFw.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-DX0XA3hI.js → kanban-definition-HUTT4EX6-CZ5Xky4p.js} +1 -1
- package/web/dist/assets/{linear-D60PrjTW.js → linear-BLZ2eH4a.js} +1 -1
- package/web/dist/assets/{mermaid.core-bonzwapf.js → mermaid.core-CPN1IVjz.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-Y-6Dum33.js → mindmap-definition-LN4V7U3C-MdC8d2tc.js} +1 -1
- package/web/dist/assets/{papaparse.min-Cas1JfRG.js → papaparse.min-CtrVJupf.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-_paO0yDw.js → pegDiagram-2B236MQR-B2DqAc-A.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-xT1_Gpn4.js → pieDiagram-ENE6RG2P-C3QAoXZh.js} +1 -1
- package/web/dist/assets/pilotswarm-CF1nmpMy.js +97 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-1usdLFOF.js → quadrantDiagram-ABIIQ3AL-BoUr5qFH.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-R3lwHOz9.js → railroadDiagram-RFXS5EU6-bZzE4KFN.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-BL8J1__Z.js → requirementDiagram-TGXJPOKE-B2ENAqLS.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-Dz6--0pl.js → sankeyDiagram-HTMAVEWB-AJZ3h46Z.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CG2Xpkct.js → sequenceDiagram-DBY2YBRQ-QUf0VVE7.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-BPbYfjxj.js → sizeCapture-X5ZJPWSS-Cv9RcUy6.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-46x_y_aA.js → stateDiagram-2N3HPSRC-hDiye45t.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-Bu4ufvaU.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-SXdMo4zJ.js → swimlanes-5IMT3BWC-hSk_OF85.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BTFePb1Y.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-CGgT-b38.js → timeline-definition-FHXFAJF6-ccOZEJ5e.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-BGS3xBa_.js → vennDiagram-L72KCM5P-Bk5DLH_V.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-m2U3wEZJ.js → wardleyDiagram-EHGQE667-C20AX8ot.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-DI6tQysg.js → xychartDiagram-FW5EYKEG-DaHqmi-R.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/runtime.js +71 -0
- package/web/server.js +51 -2
- package/web/dist/assets/channel-BHLBPE50.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-rj9D2TSL.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-rj9D2TSL.js +0 -1
- package/web/dist/assets/index-B8nGlHwb.js +0 -24
- package/web/dist/assets/index-DtL0lbis.css +0 -1
- package/web/dist/assets/pilotswarm-BT6tOknQ.js +0 -96
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-C2tnolSo.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-Dh6Sn__t.js +0 -8
package/ui/react/src/web-app.js
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
canStopSessionTurn,
|
|
13
13
|
computeLegacyLayout,
|
|
14
14
|
createInitialState,
|
|
15
|
+
buildPortalLinks,
|
|
16
|
+
formatPortalLinksForCopy,
|
|
15
17
|
createStore,
|
|
16
18
|
AUTO_HISTORY_EVENT_SOFT_CAP,
|
|
17
19
|
formatCompactNumber,
|
|
@@ -174,8 +176,21 @@ function buildArtifactLinkUrl(sessionId, filename, { fullscreen = true } = {}) {
|
|
|
174
176
|
return `${base}&artifact=${encodeURIComponent(filename)}${fullscreen ? "&view=full" : ""}`;
|
|
175
177
|
}
|
|
176
178
|
|
|
179
|
+
// Multi-origin link generation (PORTAL_LINK_ORIGINS): set once by the host
|
|
180
|
+
// app when the portal config lands. Empty = single-origin behavior. A module
|
|
181
|
+
// singleton, deliberately — links are produced from deep inside component
|
|
182
|
+
// trees and the origin list is global, immutable config.
|
|
183
|
+
let portalLinkOrigins = [];
|
|
184
|
+
export function setPortalLinkOrigins(list) {
|
|
185
|
+
portalLinkOrigins = Array.isArray(list) ? list : [];
|
|
186
|
+
}
|
|
187
|
+
export function getPortalLinkOrigins() {
|
|
188
|
+
return portalLinkOrigins;
|
|
189
|
+
}
|
|
190
|
+
|
|
177
191
|
function copySessionLinkText(url) {
|
|
178
|
-
|
|
192
|
+
const text = formatPortalLinksForCopy(buildPortalLinks(url, portalLinkOrigins));
|
|
193
|
+
if (navigator?.clipboard?.writeText) navigator.clipboard.writeText(text).catch(() => {});
|
|
179
194
|
}
|
|
180
195
|
|
|
181
196
|
// Best-effort probe: is this session's deep link openable only by the owner/
|
|
@@ -634,6 +649,77 @@ function getPortalInspectorContentWidth(paneWidth, inspectorTab, mobile = false)
|
|
|
634
649
|
return Math.max(20, paneWidth - (mobile ? 6 : 4));
|
|
635
650
|
}
|
|
636
651
|
|
|
652
|
+
/**
|
|
653
|
+
* The pane's own truth for content width, in character columns.
|
|
654
|
+
*
|
|
655
|
+
* Character-cell panes (chat code fences and tables, the sequence grid, the
|
|
656
|
+
* stats boxes) were sized from the legacy TUI layout model's split of a
|
|
657
|
+
* window-level probe — a model that knows nothing about the portal's
|
|
658
|
+
* draggable grid columns, so its imagined pane widths drift arbitrarily far
|
|
659
|
+
* from the real ones. Content rendered for the imagined width then spills
|
|
660
|
+
* out of the real pane (sequence), arrives pre-sliced behind a sideways
|
|
661
|
+
* scroll (stats), or wraps absurdly narrow (a JSON fence at ~19 columns).
|
|
662
|
+
*
|
|
663
|
+
* This hook measures the actual panel node: its content box, and a probe in
|
|
664
|
+
* its own rendered line font (the same technique as the mobile inspector
|
|
665
|
+
* clamp below, which proved the window-level probe "able to lie"). Returns
|
|
666
|
+
* null until the first measurement lands — callers fall back to the legacy
|
|
667
|
+
* width for that first paint. Re-measures on real size changes, trailing
|
|
668
|
+
* debounced so a splitter drag re-wraps once at the end, not per tick.
|
|
669
|
+
*/
|
|
670
|
+
function useMeasuredPaneColumns({ slack = 1, debounceMs = 120 } = {}) {
|
|
671
|
+
const [cols, setCols] = React.useState(null);
|
|
672
|
+
const stateRef = React.useRef({ node: null, observer: null, timer: null, frame: null });
|
|
673
|
+
const attachRef = React.useCallback((node) => {
|
|
674
|
+
const st = stateRef.current;
|
|
675
|
+
if (st.observer) { st.observer.disconnect(); st.observer = null; }
|
|
676
|
+
if (st.timer) { window.clearTimeout(st.timer); st.timer = null; }
|
|
677
|
+
if (st.frame) { cancelAnimationFrame(st.frame); st.frame = null; }
|
|
678
|
+
st.node = node;
|
|
679
|
+
if (!node || typeof window === "undefined" || typeof ResizeObserver === "undefined") return;
|
|
680
|
+
const measure = () => {
|
|
681
|
+
st.timer = null;
|
|
682
|
+
if (st.node !== node || !node.isConnected) return;
|
|
683
|
+
const target = node.querySelector(".ps-line") || node;
|
|
684
|
+
const cs = window.getComputedStyle(target);
|
|
685
|
+
const probe = document.createElement("span");
|
|
686
|
+
probe.style.cssText = "position:absolute;visibility:hidden;white-space:pre;";
|
|
687
|
+
probe.style.fontFamily = cs.fontFamily;
|
|
688
|
+
probe.style.fontSize = cs.fontSize;
|
|
689
|
+
probe.style.fontWeight = cs.fontWeight;
|
|
690
|
+
probe.style.letterSpacing = cs.letterSpacing;
|
|
691
|
+
probe.textContent = "0".repeat(100);
|
|
692
|
+
node.appendChild(probe);
|
|
693
|
+
const charWidth = (probe.getBoundingClientRect().width / 100) || 0;
|
|
694
|
+
probe.remove();
|
|
695
|
+
if (!(charWidth > 0)) return;
|
|
696
|
+
const panelStyle = window.getComputedStyle(node);
|
|
697
|
+
const contentPx = node.clientWidth
|
|
698
|
+
- (parseFloat(panelStyle.paddingLeft) || 0)
|
|
699
|
+
- (parseFloat(panelStyle.paddingRight) || 0);
|
|
700
|
+
// A collapsed/hidden pane measures ~0; keep the last good value.
|
|
701
|
+
if (!(contentPx > 50)) return;
|
|
702
|
+
const next = Math.max(20, Math.floor(contentPx / charWidth) - slack);
|
|
703
|
+
setCols((current) => (current === next ? current : next));
|
|
704
|
+
};
|
|
705
|
+
const schedule = () => {
|
|
706
|
+
if (st.timer) window.clearTimeout(st.timer);
|
|
707
|
+
st.timer = window.setTimeout(measure, debounceMs);
|
|
708
|
+
};
|
|
709
|
+
st.frame = requestAnimationFrame(measure);
|
|
710
|
+
st.observer = new ResizeObserver(schedule);
|
|
711
|
+
// content-box: a classic (non-overlay) vertical scrollbar appearing
|
|
712
|
+
// shrinks the content box without touching the border box — observe
|
|
713
|
+
// the box that actually bounds the text.
|
|
714
|
+
try {
|
|
715
|
+
st.observer.observe(node, { box: "content-box" });
|
|
716
|
+
} catch {
|
|
717
|
+
st.observer.observe(node);
|
|
718
|
+
}
|
|
719
|
+
}, [slack, debounceMs]);
|
|
720
|
+
return [cols, attachRef];
|
|
721
|
+
}
|
|
722
|
+
|
|
637
723
|
function normalizeLines(lines) {
|
|
638
724
|
const normalized = [];
|
|
639
725
|
|
|
@@ -3735,10 +3821,17 @@ function focusRegionForPaneKey(paneKey, override = null) {
|
|
|
3735
3821
|
return PANE_KEY_FOCUS_REGIONS[String(paneKey || "")] || null;
|
|
3736
3822
|
}
|
|
3737
3823
|
|
|
3738
|
-
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 }) {
|
|
3824
|
+
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 }) {
|
|
3739
3825
|
const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
|
|
3740
3826
|
const theme = getTheme(themeId);
|
|
3741
3827
|
const ref = React.useRef(null);
|
|
3828
|
+
// Mirror the scroll-panel node into the caller's ref too (panes measure
|
|
3829
|
+
// their own content width from it — see useMeasuredPaneColumns).
|
|
3830
|
+
const setPanelNode = React.useCallback((node) => {
|
|
3831
|
+
ref.current = node;
|
|
3832
|
+
if (typeof panelRef === "function") panelRef(node);
|
|
3833
|
+
else if (panelRef) panelRef.current = node;
|
|
3834
|
+
}, [panelRef]);
|
|
3742
3835
|
const stickyRef = React.useRef(null);
|
|
3743
3836
|
const syncingHorizontalRef = React.useRef(false);
|
|
3744
3837
|
const { normalizedLines, onScroll, onWheel, onTouchStart, onTouchMove, onTouchEnd } = useScrollSync(ref, lines, scrollOffset, scrollMode, paneKey, controller, { stickyBottom });
|
|
@@ -3828,7 +3921,7 @@ function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, l
|
|
|
3828
3921
|
normalizedSticky.map((line, index) => React.createElement(Line, { key: `sticky:${index}`, line, theme })),
|
|
3829
3922
|
)
|
|
3830
3923
|
: null,
|
|
3831
|
-
React.createElement("div", { ref, className: `ps-scroll-panel ${className}${scrollShadow.down ? " is-scrolled-down" : ""}${scrollShadow.up ? " is-scrolled-up" : ""}`.trim(), "data-session-scroll": focusRegion === "sessions" ? "1" : undefined, onScroll: handleBodyScroll, onMouseDown: claimFocus, onTouchStart, onWheel, onTouchMove, onTouchEnd, onTouchCancel: onTouchEnd },
|
|
3924
|
+
React.createElement("div", { ref: setPanelNode, className: `ps-scroll-panel ${className}${scrollShadow.down ? " is-scrolled-down" : ""}${scrollShadow.up ? " is-scrolled-up" : ""}`.trim(), "data-session-scroll": focusRegion === "sessions" ? "1" : undefined, onScroll: handleBodyScroll, onMouseDown: claimFocus, onTouchStart, onWheel, onTouchMove, onTouchEnd, onTouchCancel: onTouchEnd },
|
|
3832
3925
|
typeof renderBody === "function"
|
|
3833
3926
|
? renderBody(normalizedLines, theme)
|
|
3834
3927
|
: structuredBlocks
|
|
@@ -5138,15 +5231,24 @@ function SessionLinkModal({ url, warn, onCopyAgain, onClose }) {
|
|
|
5138
5231
|
return React.createElement("div", { className: "ps-share-overlay", onClick: onClose },
|
|
5139
5232
|
React.createElement("div", { className: "ps-link-modal", onClick: stop },
|
|
5140
5233
|
React.createElement("div", { className: "ps-share-modal-head" },
|
|
5141
|
-
React.createElement("span", null, "Link copied"),
|
|
5234
|
+
React.createElement("span", null, portalLinkOrigins.length >= 2 ? "Session links copied" : "Link copied"),
|
|
5142
5235
|
React.createElement("button", { className: "ps-modal-close", onClick: onClose, "aria-label": "Close", title: "Close" }, "✕")),
|
|
5143
5236
|
React.createElement("div", { className: "ps-share-section-sub" },
|
|
5144
|
-
|
|
5237
|
+
portalLinkOrigins.length >= 2
|
|
5238
|
+
? "All entry-point links were copied — use the one reachable from the recipient's network."
|
|
5239
|
+
: "Copied to your clipboard — anyone with access can open the session from this link."),
|
|
5145
5240
|
React.createElement("div", { className: "ps-link-row" },
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5241
|
+
portalLinkOrigins.length >= 2
|
|
5242
|
+
? React.createElement("textarea", {
|
|
5243
|
+
ref: inputRef, className: "ps-link-input", readOnly: true,
|
|
5244
|
+
rows: buildPortalLinks(url, portalLinkOrigins).length,
|
|
5245
|
+
value: formatPortalLinksForCopy(buildPortalLinks(url, portalLinkOrigins)),
|
|
5246
|
+
onFocus: () => inputRef.current?.select(),
|
|
5247
|
+
})
|
|
5248
|
+
: React.createElement("input", {
|
|
5249
|
+
ref: inputRef, className: "ps-link-input", readOnly: true, value: url,
|
|
5250
|
+
onFocus: () => inputRef.current?.select(),
|
|
5251
|
+
}),
|
|
5150
5252
|
React.createElement("button", { className: "ps-mini-button", onClick: onCopyAgain }, "Copy")),
|
|
5151
5253
|
warn
|
|
5152
5254
|
? React.createElement("div", { className: "ps-link-warn" }, SESSION_LINK_PRIVATE_WARNING)
|
|
@@ -5790,9 +5892,14 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
5790
5892
|
viewState.sessionsById,
|
|
5791
5893
|
viewState.sessionsFlat,
|
|
5792
5894
|
]);
|
|
5895
|
+
// Character-cell content (code fences, tables, box art) wraps at the
|
|
5896
|
+
// pane's MEASURED width; the legacy-model width only covers the first
|
|
5897
|
+
// paint, before the panel node exists to measure.
|
|
5898
|
+
const [measuredCols, panelRef] = useMeasuredPaneColumns();
|
|
5899
|
+
const contentWidth = measuredCols ?? viewState.contentWidth;
|
|
5793
5900
|
const chrome = React.useMemo(
|
|
5794
|
-
() => selectChatPaneChrome(selectorState, { width:
|
|
5795
|
-
[selectorState,
|
|
5901
|
+
() => selectChatPaneChrome(selectorState, { width: contentWidth }),
|
|
5902
|
+
[selectorState, contentWidth],
|
|
5796
5903
|
);
|
|
5797
5904
|
const animatedDots = useAnimatedDots(Boolean(chrome.animateTitleRight));
|
|
5798
5905
|
const titleRight = React.useMemo(
|
|
@@ -5809,21 +5916,21 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
5809
5916
|
&& Number(viewState.activeHistory?.loadedEventCount || 0) >= AUTO_HISTORY_EVENT_SOFT_CAP,
|
|
5810
5917
|
);
|
|
5811
5918
|
const lines = React.useMemo(
|
|
5812
|
-
() => selectChatLines(selectorState,
|
|
5813
|
-
[selectorState,
|
|
5919
|
+
() => selectChatLines(selectorState, contentWidth, { tableMode: "sentinel" }),
|
|
5920
|
+
[selectorState, contentWidth],
|
|
5814
5921
|
);
|
|
5815
5922
|
const spinnerFrame = useSpinnerFrame(viewState.activeSessionStatus === "running");
|
|
5816
5923
|
const liveActivityLines = React.useMemo(
|
|
5817
|
-
() => selectLiveActivityLines(selectorState, { spinnerFrame, maxWidth:
|
|
5818
|
-
[selectorState, spinnerFrame,
|
|
5924
|
+
() => selectLiveActivityLines(selectorState, { spinnerFrame, maxWidth: contentWidth }),
|
|
5925
|
+
[selectorState, spinnerFrame, contentWidth],
|
|
5819
5926
|
);
|
|
5820
5927
|
// The live-activity line is pinned in the bottom-sticky strip (with the
|
|
5821
5928
|
// outbox overlay), NOT appended to the transcript — it must stay put while
|
|
5822
5929
|
// chat content scrolls, and it drops the instant the turn ends.
|
|
5823
5930
|
const pinnedActivityLines = liveActivityLines;
|
|
5824
5931
|
const outboxLines = React.useMemo(
|
|
5825
|
-
() => selectOutboxOverlayLines(selectorState,
|
|
5826
|
-
[selectorState,
|
|
5932
|
+
() => selectOutboxOverlayLines(selectorState, contentWidth, { tableMode: "sentinel" }),
|
|
5933
|
+
[selectorState, contentWidth],
|
|
5827
5934
|
);
|
|
5828
5935
|
const stickyBottom = React.useMemo(
|
|
5829
5936
|
() => (pinnedActivityLines.length > 0 ? [...outboxLines, ...pinnedActivityLines] : outboxLines),
|
|
@@ -5894,6 +6001,7 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
5894
6001
|
paneKey: "chat",
|
|
5895
6002
|
className: "is-wrapped",
|
|
5896
6003
|
panelClassName: "ps-chat-panel",
|
|
6004
|
+
panelRef,
|
|
5897
6005
|
bottomContent: composer,
|
|
5898
6006
|
topContent: showLoadOlder
|
|
5899
6007
|
? React.createElement("div", { className: "ps-load-older-bar" },
|
|
@@ -6256,6 +6364,135 @@ function SessionCanvasMark({ mark }) {
|
|
|
6256
6364
|
: null));
|
|
6257
6365
|
}
|
|
6258
6366
|
|
|
6367
|
+
/** Chain-link glyph for the canvas share button. */
|
|
6368
|
+
function CanvasLinkGlyph() {
|
|
6369
|
+
const stroke = { fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round" };
|
|
6370
|
+
return React.createElement("svg", { viewBox: "0 0 20 20", width: 14, height: 14, "aria-hidden": "true" },
|
|
6371
|
+
React.createElement("path", { d: "M8.5 11.5l3-3", ...stroke }),
|
|
6372
|
+
React.createElement("path", { d: "M7 13l-1.8 1.8a2.8 2.8 0 01-4-4l3-3a2.8 2.8 0 014 0", ...stroke }),
|
|
6373
|
+
React.createElement("path", { d: "M13 7l1.8-1.8a2.8 2.8 0 014 4l-3 3a2.8 2.8 0 01-4 0", ...stroke }));
|
|
6374
|
+
}
|
|
6375
|
+
|
|
6376
|
+
/**
|
|
6377
|
+
* The canvas share dialog: two audiences, one live public token.
|
|
6378
|
+
*
|
|
6379
|
+
* - Session access: a plain deep link; portal sign-in + the existing
|
|
6380
|
+
* visibility check do the authz. Always available.
|
|
6381
|
+
* - Anyone with link: the ONE public view token for this canvas. Created
|
|
6382
|
+
* on demand, RESET rotates it (the old link dies instantly), Remove
|
|
6383
|
+
* returns to unlinked. The raw token appears exactly once, right after
|
|
6384
|
+
* mint — the server stores only its hash.
|
|
6385
|
+
*
|
|
6386
|
+
* Link management is a session:manage operation server-side; anyone else
|
|
6387
|
+
* sees the server's refusal inline.
|
|
6388
|
+
*/
|
|
6389
|
+
function CanvasShareDialog({ controller, sessionId, slot, onClose }) {
|
|
6390
|
+
const [info, setInfo] = React.useState(null); // { exists, createdAt } | null while loading
|
|
6391
|
+
const [freshLink, setFreshLink] = React.useState(null); // the just-minted URL, shown once
|
|
6392
|
+
const [busy, setBusy] = React.useState(false);
|
|
6393
|
+
const [error, setError] = React.useState("");
|
|
6394
|
+
const [copied, setCopied] = React.useState("");
|
|
6395
|
+
|
|
6396
|
+
const transport = controller.transport;
|
|
6397
|
+
const sessionLink = `${window.location.origin}/?session=${encodeURIComponent(sessionId)}&view=canvas&slot=${slot}&max=1`;
|
|
6398
|
+
// Multi-entry-point deployments: ONE text box holding every labeled
|
|
6399
|
+
// variant, ONE copy that takes all of them — the sender does not know
|
|
6400
|
+
// which entry point the recipient can reach, so they paste both. The
|
|
6401
|
+
// token variants all resolve the same hashed row: one token, N doors,
|
|
6402
|
+
// single-switch revocation whichever door the bearer uses.
|
|
6403
|
+
const linkRows = (url, keyPrefix) => {
|
|
6404
|
+
const links = buildPortalLinks(url, portalLinkOrigins);
|
|
6405
|
+
if (links.length <= 1) {
|
|
6406
|
+
return React.createElement("div", { className: "ps-canvas-share-row" },
|
|
6407
|
+
React.createElement("code", { className: "ps-canvas-share-url" }, url),
|
|
6408
|
+
React.createElement("button", {
|
|
6409
|
+
type: "button", className: "ps-mini-button",
|
|
6410
|
+
onClick: () => copy(keyPrefix, url),
|
|
6411
|
+
}, copied === keyPrefix ? "Copied" : "Copy"));
|
|
6412
|
+
}
|
|
6413
|
+
const text = formatPortalLinksForCopy(links);
|
|
6414
|
+
return React.createElement("div", { className: "ps-canvas-share-row" },
|
|
6415
|
+
React.createElement("textarea", {
|
|
6416
|
+
className: "ps-link-input",
|
|
6417
|
+
readOnly: true,
|
|
6418
|
+
rows: links.length,
|
|
6419
|
+
value: text,
|
|
6420
|
+
onFocus: (event) => event.currentTarget.select(),
|
|
6421
|
+
}),
|
|
6422
|
+
React.createElement("button", {
|
|
6423
|
+
type: "button", className: "ps-mini-button",
|
|
6424
|
+
onClick: () => copy(keyPrefix, text),
|
|
6425
|
+
}, copied === keyPrefix ? "Copied" : "Copy"));
|
|
6426
|
+
};
|
|
6427
|
+
|
|
6428
|
+
const refresh = React.useCallback(() => {
|
|
6429
|
+
transport.getCanvasShareLink?.(sessionId, slot)
|
|
6430
|
+
.then((state) => setInfo(state || { exists: false }))
|
|
6431
|
+
.catch((err) => { setInfo({ exists: false }); setError(err?.message || String(err)); });
|
|
6432
|
+
}, [transport, sessionId, slot]);
|
|
6433
|
+
React.useEffect(() => { refresh(); }, [refresh]);
|
|
6434
|
+
|
|
6435
|
+
const copy = (label, text) => {
|
|
6436
|
+
navigator.clipboard?.writeText(text)
|
|
6437
|
+
.then(() => { setCopied(label); window.setTimeout(() => setCopied(""), 1500); })
|
|
6438
|
+
.catch(() => setError("Could not reach the clipboard — copy the link manually."));
|
|
6439
|
+
};
|
|
6440
|
+
|
|
6441
|
+
const mint = (isReset) => {
|
|
6442
|
+
setBusy(true); setError("");
|
|
6443
|
+
transport.resetCanvasShareLink(sessionId, slot)
|
|
6444
|
+
.then(({ token }) => {
|
|
6445
|
+
setFreshLink(`${window.location.origin}/?canvasShare=${encodeURIComponent(token)}`);
|
|
6446
|
+
setInfo({ exists: true });
|
|
6447
|
+
})
|
|
6448
|
+
.catch((err) => setError(err?.message || String(err)))
|
|
6449
|
+
.finally(() => setBusy(false));
|
|
6450
|
+
void isReset;
|
|
6451
|
+
};
|
|
6452
|
+
|
|
6453
|
+
const remove = () => {
|
|
6454
|
+
setBusy(true); setError("");
|
|
6455
|
+
transport.removeCanvasShareLink(sessionId, slot)
|
|
6456
|
+
.then(() => { setFreshLink(null); setInfo({ exists: false }); })
|
|
6457
|
+
.catch((err) => setError(err?.message || String(err)))
|
|
6458
|
+
.finally(() => setBusy(false));
|
|
6459
|
+
};
|
|
6460
|
+
|
|
6461
|
+
return React.createElement("div", { className: "ps-canvas-share-overlay", onClick: onClose },
|
|
6462
|
+
React.createElement("div", { className: "ps-canvas-share-dialog", onClick: (e) => e.stopPropagation(), role: "dialog", "aria-label": "Share canvas" },
|
|
6463
|
+
React.createElement("div", { className: "ps-canvas-share-title" },
|
|
6464
|
+
"Share this canvas",
|
|
6465
|
+
React.createElement("button", { type: "button", className: "ps-mini-button", onClick: onClose, "aria-label": "Close" }, "✕")),
|
|
6466
|
+
|
|
6467
|
+
React.createElement("div", { className: "ps-canvas-share-section" },
|
|
6468
|
+
React.createElement("div", { className: "ps-canvas-share-head" }, "Anyone with session access"),
|
|
6469
|
+
React.createElement("div", { className: "ps-canvas-share-sub" }, "Opens the portal signed in, canvas full screen. Session visibility rules apply."),
|
|
6470
|
+
linkRows(sessionLink, "session")),
|
|
6471
|
+
|
|
6472
|
+
React.createElement("div", { className: "ps-canvas-share-section" },
|
|
6473
|
+
React.createElement("div", { className: "ps-canvas-share-head" }, "Anyone with the link"),
|
|
6474
|
+
React.createElement("div", { className: "ps-canvas-share-sub" },
|
|
6475
|
+
"View only, live, no sign-in. One link at a time — resetting makes the previous link stop working immediately."),
|
|
6476
|
+
info === null
|
|
6477
|
+
? React.createElement("div", { className: "ps-canvas-share-sub" }, "Checking…")
|
|
6478
|
+
: freshLink
|
|
6479
|
+
? React.createElement(React.Fragment, null,
|
|
6480
|
+
linkRows(freshLink, "public"),
|
|
6481
|
+
React.createElement("div", { className: "ps-canvas-share-sub is-warn" },
|
|
6482
|
+
"Copy it now — the link is shown only this once."))
|
|
6483
|
+
: info.exists
|
|
6484
|
+
? React.createElement("div", { className: "ps-canvas-share-sub" },
|
|
6485
|
+
`A link exists${info.createdAt ? ` (created ${new Date(info.createdAt).toLocaleString()})` : ""}. For safety it cannot be re-shown — Reset to get a new one.`)
|
|
6486
|
+
: React.createElement("div", { className: "ps-canvas-share-sub" }, "No public link yet."),
|
|
6487
|
+
React.createElement("div", { className: "ps-canvas-share-actions" },
|
|
6488
|
+
React.createElement("button", { type: "button", className: "ps-mini-button", disabled: busy, onClick: () => mint(info?.exists) },
|
|
6489
|
+
info?.exists ? "Reset link" : "Create link"),
|
|
6490
|
+
info?.exists
|
|
6491
|
+
? React.createElement("button", { type: "button", className: "ps-mini-button", disabled: busy, onClick: remove }, "Remove link")
|
|
6492
|
+
: null)),
|
|
6493
|
+
error ? React.createElement("div", { className: "ps-canvas-share-error" }, error) : null));
|
|
6494
|
+
}
|
|
6495
|
+
|
|
6259
6496
|
/**
|
|
6260
6497
|
* The canvas slot controls: a dropdown when the session has more than one
|
|
6261
6498
|
* drawn canvas, plus prev/next arrows. Hidden entirely with one canvas —
|
|
@@ -6322,7 +6559,7 @@ function RestoreGlyph() {
|
|
|
6322
6559
|
* loads hidden-but-painted behind the live frame and is promoted on load, so
|
|
6323
6560
|
* a live redraw is a composite swap of something already drawn.
|
|
6324
6561
|
*/
|
|
6325
|
-
function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible = true, focusOnPromote = false, dataRev = 0, dataPayload = null }) {
|
|
6562
|
+
function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible = true, focusOnPromote = false, dataRev = 0, dataPayload = null, dataPatch = null }) {
|
|
6326
6563
|
const [live, setLive] = React.useState(null); // { url, rev }
|
|
6327
6564
|
const [staging, setStaging] = React.useState(null); // { url, rev }
|
|
6328
6565
|
const [loadError, setLoadError] = React.useState(null); // { rev }
|
|
@@ -6471,13 +6708,22 @@ function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible
|
|
|
6471
6708
|
liveIframeElRef.current?.contentWindow?.postMessage(
|
|
6472
6709
|
{ type: "canvas-data", data: dataPayload, dataRev }, "*",
|
|
6473
6710
|
);
|
|
6711
|
+
// The delta that produced this state, for pages that registered a
|
|
6712
|
+
// canvas-patch listener (targeted DOM updates, transitions). The
|
|
6713
|
+
// whole-state message above ALWAYS accompanies it — applyData
|
|
6714
|
+
// stays the mandatory floor; the patch is the opt-in fast path.
|
|
6715
|
+
if (dataPatch) {
|
|
6716
|
+
liveIframeElRef.current?.contentWindow?.postMessage(
|
|
6717
|
+
{ type: "canvas-patch", patch: dataPatch, dataRev }, "*",
|
|
6718
|
+
);
|
|
6719
|
+
}
|
|
6474
6720
|
} catch { /* frame mid-teardown */ }
|
|
6475
6721
|
// On-screen injection = seen; hidden injection leaves the badge lit
|
|
6476
6722
|
// until the user actually looks (the catch-up below files it then).
|
|
6477
6723
|
if (controller && sessionId && visibleRef.current) {
|
|
6478
6724
|
controller.dispatch({ type: "canvas/viewed", sessionId, slot, dataRev });
|
|
6479
6725
|
}
|
|
6480
|
-
}, [live, dataRev, dataPayload, controller, sessionId]);
|
|
6726
|
+
}, [live, dataRev, dataPayload, dataPatch, controller, sessionId]);
|
|
6481
6727
|
|
|
6482
6728
|
// Catch-up receipt: becoming visible with a live revision on screen IS
|
|
6483
6729
|
// viewing it — this is what clears a badge earned while hidden.
|
|
@@ -6601,6 +6847,7 @@ function CanvasPane({ controller, mobile = false, visible = true, focusOnPromote
|
|
|
6601
6847
|
}, [controller, view.sessionId]);
|
|
6602
6848
|
|
|
6603
6849
|
const zenOn = useControllerSelector(controller, (s) => s?.ui?.canvasZen === true);
|
|
6850
|
+
const [shareOpen, setShareOpen] = React.useState(false);
|
|
6604
6851
|
const [toolbarSlot, setToolbarSlot] = React.useState(null);
|
|
6605
6852
|
React.useEffect(() => {
|
|
6606
6853
|
if (!maximized) { setToolbarSlot(null); return undefined; }
|
|
@@ -6635,6 +6882,15 @@ function CanvasPane({ controller, mobile = false, visible = true, focusOnPromote
|
|
|
6635
6882
|
React.createElement("span", { className: "ps-artifact-pane-type", title: view.note || undefined },
|
|
6636
6883
|
view.exists ? `${view.name ? `${view.name} \u00b7 ` : ""}rev ${view.latestRev}` : ""),
|
|
6637
6884
|
React.createElement(CanvasSlotControls, { controller, view, compact: true }),
|
|
6885
|
+
view.exists
|
|
6886
|
+
? React.createElement("button", {
|
|
6887
|
+
type: "button",
|
|
6888
|
+
className: "ps-canvas-max-btn",
|
|
6889
|
+
onClick: () => setShareOpen(true),
|
|
6890
|
+
title: "Share this canvas — copy a view link",
|
|
6891
|
+
"aria-label": "Share canvas",
|
|
6892
|
+
}, React.createElement(CanvasLinkGlyph))
|
|
6893
|
+
: null,
|
|
6638
6894
|
onToggleMaximized
|
|
6639
6895
|
? React.createElement("button", {
|
|
6640
6896
|
type: "button",
|
|
@@ -6683,6 +6939,15 @@ function CanvasPane({ controller, mobile = false, visible = true, focusOnPromote
|
|
|
6683
6939
|
React.createElement(CanvasSlotControls, { controller, view })),
|
|
6684
6940
|
React.createElement("span", { className: "ps-artifact-pane-actions" },
|
|
6685
6941
|
view.exists ? React.createElement(HtmlZoomControl, { zoom, setZoom }) : null,
|
|
6942
|
+
view.exists
|
|
6943
|
+
? React.createElement("button", {
|
|
6944
|
+
type: "button",
|
|
6945
|
+
className: "ps-artifact-pane-btn",
|
|
6946
|
+
onClick: () => setShareOpen(true),
|
|
6947
|
+
title: "Share this canvas — copy a view link",
|
|
6948
|
+
"aria-label": "Share canvas",
|
|
6949
|
+
}, React.createElement(CanvasLinkGlyph))
|
|
6950
|
+
: null,
|
|
6686
6951
|
// Zen: sessions step away, chat becomes a rail, the canvas is
|
|
6687
6952
|
// the workbench. The mode for "the work happens on the canvas,
|
|
6688
6953
|
// chat is for steering".
|
|
@@ -6727,11 +6992,20 @@ function CanvasPane({ controller, mobile = false, visible = true, focusOnPromote
|
|
|
6727
6992
|
focusOnPromote,
|
|
6728
6993
|
dataRev: view.latestDataRev,
|
|
6729
6994
|
dataPayload: view.dataPayload,
|
|
6995
|
+
dataPatch: view.dataPatch,
|
|
6730
6996
|
})
|
|
6731
6997
|
: React.createElement("div", { className: "ps-canvas-blank" },
|
|
6732
6998
|
React.createElement("p", { className: "ps-canvas-blank-title" }, "Nothing on the canvas yet."),
|
|
6733
6999
|
React.createElement("p", null,
|
|
6734
|
-
"Ask the agent to draw — a dashboard, a chart, a diagram — or it will draw when it has something worth showing.")))
|
|
7000
|
+
"Ask the agent to draw — a dashboard, a chart, a diagram — or it will draw when it has something worth showing."))),
|
|
7001
|
+
shareOpen && view.exists
|
|
7002
|
+
? React.createElement(CanvasShareDialog, {
|
|
7003
|
+
controller,
|
|
7004
|
+
sessionId: view.sessionId,
|
|
7005
|
+
slot: view.slot,
|
|
7006
|
+
onClose: () => setShareOpen(false),
|
|
7007
|
+
})
|
|
7008
|
+
: null);
|
|
6735
7009
|
}
|
|
6736
7010
|
|
|
6737
7011
|
function InspectorTabs({ activeTab, controller }) {
|
|
@@ -7239,9 +7513,16 @@ function InspectorPane({ controller, mobile = false, panelClassName = "", extraA
|
|
|
7239
7513
|
// could pan it — in practice it rendered clipped on BOTH edges (the
|
|
7240
7514
|
// timestamps and the STATS header sliced off) and read as broken. Fitting
|
|
7241
7515
|
// to width collapses surplus lanes into "…" instead, which is legible.
|
|
7516
|
+
//
|
|
7517
|
+
// The width itself is the pane's MEASURED column count wherever a
|
|
7518
|
+
// measurement exists — the legacy-model rightWidth imagines a split the
|
|
7519
|
+
// portal's draggable grid does not honor, which is how the sequence grid
|
|
7520
|
+
// used to spill past the pane edge and the stats boxes arrived pre-sliced.
|
|
7521
|
+
const [measuredCols, panelRef] = useMeasuredPaneColumns();
|
|
7522
|
+
const contentWidth = measuredCols ?? viewState.contentWidth;
|
|
7242
7523
|
const inspector = React.useMemo(() => selectInspector(selectorState, {
|
|
7243
|
-
width:
|
|
7244
|
-
}), [selectorState,
|
|
7524
|
+
width: contentWidth,
|
|
7525
|
+
}), [selectorState, contentWidth]);
|
|
7245
7526
|
const completionByTurn = React.useMemo(() => {
|
|
7246
7527
|
const history = viewState.activeSessionId
|
|
7247
7528
|
? viewState.historyBySessionId?.get(viewState.activeSessionId) || null
|
|
@@ -7391,6 +7672,7 @@ function InspectorPane({ controller, mobile = false, panelClassName = "", extraA
|
|
|
7391
7672
|
: "top",
|
|
7392
7673
|
stickyBottom: inspector.activeTab === "logs",
|
|
7393
7674
|
paneKey: "inspector",
|
|
7675
|
+
panelRef,
|
|
7394
7676
|
className: inspector.activeTab === "history" || inspector.activeTab === "logs" ? "is-wrapped" : "is-preserve",
|
|
7395
7677
|
panelClassName: `ps-inspector-pane${inspector.activeTab === "sequence" ? " has-preserved-sticky" : ""}${panelClassName ? ` ${panelClassName}` : ""}`.trim(),
|
|
7396
7678
|
});
|
|
@@ -7565,6 +7847,28 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7565
7847
|
const attachInputRef = React.useRef(null);
|
|
7566
7848
|
const [dragOver, setDragOver] = React.useState(false);
|
|
7567
7849
|
|
|
7850
|
+
// Auto-grow: one line idle, sized to the RENDERED content (scrollHeight
|
|
7851
|
+
// sees soft wrap; counting "\n" does not). CSS max-height provides the
|
|
7852
|
+
// cap, after which the textarea scrolls internally. The measurement
|
|
7853
|
+
// zeroes the height first — NOT "auto": iOS Safari keeps a textarea's
|
|
7854
|
+
// scrollHeight at its high-water mark under "auto", so a box that grew
|
|
7855
|
+
// never shrank back after the text was sent or deleted.
|
|
7856
|
+
const growInput = React.useCallback(() => {
|
|
7857
|
+
const node = inputRef.current;
|
|
7858
|
+
if (!node) return;
|
|
7859
|
+
node.style.height = "0px";
|
|
7860
|
+
node.style.height = `${node.scrollHeight + 2}px`;
|
|
7861
|
+
}, []);
|
|
7862
|
+
React.useLayoutEffect(() => {
|
|
7863
|
+
growInput();
|
|
7864
|
+
}, [growInput, promptState.value]);
|
|
7865
|
+
React.useEffect(() => {
|
|
7866
|
+
// Width changes re-wrap the content; re-measure on viewport resizes
|
|
7867
|
+
// (covers rotation and the on-screen keyboard shrinking the pane).
|
|
7868
|
+
window.addEventListener("resize", growInput);
|
|
7869
|
+
return () => window.removeEventListener("resize", growInput);
|
|
7870
|
+
}, [growInput]);
|
|
7871
|
+
|
|
7568
7872
|
const canAttachImages = typeof controller.transport?.supportsPromptImageAttachments === "function"
|
|
7569
7873
|
&& controller.transport.supportsPromptImageAttachments()
|
|
7570
7874
|
&& typeof controller.transport?.uploadArtifactFromFile === "function";
|
|
@@ -7609,14 +7913,26 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7609
7913
|
const inputNode = inputRef.current;
|
|
7610
7914
|
if (!active || promptState.modalOpen || !promptState.focused || !inputNode) return;
|
|
7611
7915
|
if (document.activeElement !== inputNode) {
|
|
7916
|
+
// Programmatic focus pops the on-screen keyboard on touch devices;
|
|
7917
|
+
// there, focus only ever comes from the user's own tap.
|
|
7918
|
+
if (mobile) return;
|
|
7612
7919
|
try {
|
|
7613
7920
|
inputNode.focus({ preventScroll: true });
|
|
7614
7921
|
} catch {
|
|
7615
7922
|
inputNode.focus();
|
|
7616
7923
|
}
|
|
7617
7924
|
}
|
|
7618
|
-
|
|
7619
|
-
|
|
7925
|
+
// Apply the model's caret ONLY when it disagrees with the DOM — that
|
|
7926
|
+
// is a programmatic intervention (pending-message recall, reference
|
|
7927
|
+
// autocomplete insert, draft restore). While the user edits, the DOM
|
|
7928
|
+
// is the source of truth and onSelect/onChange mirror it into state;
|
|
7929
|
+
// unconditionally echoing state back used to collapse in-progress
|
|
7930
|
+
// backward selections (mouse drag right-to-left, Shift+Left, and the
|
|
7931
|
+
// start handle of a touch selection).
|
|
7932
|
+
if (inputNode.value === promptState.value && inputNode.selectionStart !== promptState.cursor) {
|
|
7933
|
+
inputNode.setSelectionRange(promptState.cursor, promptState.cursor);
|
|
7934
|
+
}
|
|
7935
|
+
}, [active, mobile, promptState.cursor, promptState.value, promptState.focused, promptState.modalOpen]);
|
|
7620
7936
|
|
|
7621
7937
|
const sendPrompt = React.useCallback(() => {
|
|
7622
7938
|
controller.handleCommand(UI_COMMANDS.SEND_PROMPT)
|
|
@@ -7720,7 +8036,9 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7720
8036
|
React.createElement("textarea", {
|
|
7721
8037
|
ref: inputRef,
|
|
7722
8038
|
className: "ps-prompt-input",
|
|
7723
|
-
|
|
8039
|
+
// One line at rest; a layout effect grows it to the rendered
|
|
8040
|
+
// content and CSS max-height caps it. Never a manual resize grip.
|
|
8041
|
+
rows: 1,
|
|
7724
8042
|
value: promptState.value,
|
|
7725
8043
|
readOnly: selectedReadOnly,
|
|
7726
8044
|
placeholder: promptState.answerMode
|
|
@@ -7734,7 +8052,9 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7734
8052
|
: promptState.hasOutbox
|
|
7735
8053
|
? "Type a message and press Enter to queue it behind the pending batch"
|
|
7736
8054
|
: "Type a message and press Enter",
|
|
7737
|
-
|
|
8055
|
+
// On touch, Enter inserts a newline (the chevron sends) — the
|
|
8056
|
+
// keyboard must not advertise a send that will not happen.
|
|
8057
|
+
enterKeyHint: mobile ? "enter" : "send",
|
|
7738
8058
|
onFocus: () => controller.setFocus("prompt"),
|
|
7739
8059
|
onSelect: (event) => controller.setPromptCursor(event.currentTarget.selectionStart || 0),
|
|
7740
8060
|
onChange: (event) => controller.setPrompt(event.currentTarget.value, event.currentTarget.selectionStart || event.currentTarget.value.length),
|
|
@@ -7758,16 +8078,11 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7758
8078
|
event.preventDefault();
|
|
7759
8079
|
return;
|
|
7760
8080
|
}
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
if (event.key === "ArrowDown" && !event.shiftKey && !event.metaKey && !event.altKey) {
|
|
7767
|
-
event.preventDefault();
|
|
7768
|
-
controller.movePromptCursorVertical(1);
|
|
7769
|
-
return;
|
|
7770
|
-
}
|
|
8081
|
+
// Arrow keys are NOT intercepted: the textarea's native cursor
|
|
8082
|
+
// movement understands soft-wrapped lines and goal columns;
|
|
8083
|
+
// onSelect mirrors every move into the shared model. (The old
|
|
8084
|
+
// hijack routed through the TUI's logical-line cursor and made
|
|
8085
|
+
// Down jump whole paragraphs inside wrapped text.)
|
|
7771
8086
|
if (event.key === "Escape" && promptState.editingPending) {
|
|
7772
8087
|
event.preventDefault();
|
|
7773
8088
|
if (selectedReadOnly) {
|
|
@@ -7784,6 +8099,9 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7784
8099
|
},
|
|
7785
8100
|
}),
|
|
7786
8101
|
React.createElement("div", { className: "ps-prompt-actions" },
|
|
8102
|
+
// pointerdown preventDefault on every action keeps focus in the
|
|
8103
|
+
// textarea — on a phone, tapping Send must not collapse the
|
|
8104
|
+
// keyboard between messages.
|
|
7787
8105
|
canAttachImages
|
|
7788
8106
|
? React.createElement(React.Fragment, null,
|
|
7789
8107
|
React.createElement("input", {
|
|
@@ -7807,6 +8125,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7807
8125
|
className: "ps-mini-button ps-attach-button",
|
|
7808
8126
|
title: "Attach images (or paste / drop them into the composer)",
|
|
7809
8127
|
"aria-label": "Attach images",
|
|
8128
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
7810
8129
|
onClick: () => attachInputRef.current?.click(),
|
|
7811
8130
|
}, "📎"))
|
|
7812
8131
|
: null,
|
|
@@ -7816,6 +8135,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7816
8135
|
className: "ps-mini-button",
|
|
7817
8136
|
title: selectedQueued ? "Delete selected queued prompt" : "Cancel selected pending prompt",
|
|
7818
8137
|
"aria-label": selectedQueued ? "Delete selected queued prompt" : "Cancel selected pending prompt",
|
|
8138
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
7819
8139
|
onClick: cancelPending,
|
|
7820
8140
|
}, selectedQueued ? "Delete" : "Cancel")
|
|
7821
8141
|
: null,
|
|
@@ -7826,6 +8146,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7826
8146
|
title: "Stop the current turn (the session stays alive and returns to idle)",
|
|
7827
8147
|
"aria-label": "Stop the current turn",
|
|
7828
8148
|
disabled: stoppingTurn,
|
|
8149
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
7829
8150
|
onClick: stopTurn,
|
|
7830
8151
|
}, "■")
|
|
7831
8152
|
: null,
|
|
@@ -7842,6 +8163,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7842
8163
|
: promptState.hasOutbox
|
|
7843
8164
|
? "Queue prompt"
|
|
7844
8165
|
: "Send prompt",
|
|
8166
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
7845
8167
|
onClick: sendPrompt,
|
|
7846
8168
|
}, sendLabel),
|
|
7847
8169
|
),
|
package/web/README.md
CHANGED
|
@@ -196,3 +196,31 @@ pilotswarm (the app package; this dir is its web/ tree)
|
|
|
196
196
|
`pilotswarm-cli` rather than importing monorepo-relative source files. That
|
|
197
197
|
keeps the publishable package graph explicit and lets the portal reuse the same
|
|
198
198
|
Node transport and plugin-config behavior as the TUI.
|
|
199
|
+
|
|
200
|
+
## Multi-origin links (PORTAL_LINK_ORIGINS)
|
|
201
|
+
|
|
202
|
+
A deployment reachable through several entry points (a public edge and a
|
|
203
|
+
private/VPN hostname, say) sets:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
PORTAL_LINK_ORIGINS="Corporate=https://corp.example.com,Private/VPN=https://vpn.example.com"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
With two or more entries, every link-producing surface — the session
|
|
210
|
+
copy-link, artifact links, and the canvas share dialog (both the
|
|
211
|
+
session-access and the public-token link) — offers one labeled variant per
|
|
212
|
+
origin, and copy puts all of them on the clipboard as labeled lines. Token
|
|
213
|
+
links all resolve the same stored hash, so a reset revokes every variant at
|
|
214
|
+
once. Unset (or empty) means single-origin behavior, unchanged.
|
|
215
|
+
|
|
216
|
+
Validation is fail-loud at portal startup: 2-6 entries, `Label=Origin`
|
|
217
|
+
pairs, unique labels and origins, bare https origins (http allowed only for
|
|
218
|
+
localhost). A single entry is refused as a probable mistake.
|
|
219
|
+
|
|
220
|
+
Note: configured origins are served on the pre-auth /api/portal-config
|
|
221
|
+
payload (the same visibility the waldemort patch had, which baked both
|
|
222
|
+
hostnames into the public JS bundle) — treat entry-point hostnames as
|
|
223
|
+
non-secret.
|
|
224
|
+
|
|
225
|
+
This replaces the waldemort `patch-pilotswarm-dual-session-links.mjs`
|
|
226
|
+
deploy patch: set the env var, delete the patch.
|