pilotswarm 0.5.39 → 0.5.40
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 +369 -34
- 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-C6FaYMB7.js} +1 -1
- package/web/dist/assets/{arc-DiQLL9vM.js → arc-iZBUHcPs.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-B8ry8WNZ.js → architectureDiagram-ZJ3FMSHR-D4-wnpAG.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-Z4PcukIo.js → blockDiagram-677ZJIJ3-Dz-s103f.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-Cahkc0gc.js → c4Diagram-LMCZKHZV-DE7sGdHE.js} +1 -1
- package/web/dist/assets/channel-Bk68hLPV.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-CtZuWH4J.js → chunk-2Q5K7J3B-DSFs_jRM.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-C0M390-k.js → chunk-32BRIVSS-8sBVtZ4f.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-ClvX9u5j.js → chunk-5VM5RSS4-CbIX7k0G.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-BVAet5KG.js → chunk-EX3LRPZG-ZfmPtWF8.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-BccmZmrr.js → chunk-JWPE2WC7-DifZrqsQ.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-BN5MKe94.js → chunk-MOJQB5TN-CEvg_lZT.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-DfWw-ACP.js → chunk-RYQCIY6F-DmYOwk6-.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-BgUEoLnN.js → chunk-V7JOEXUC-DOxG9Vxu.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-k1eKfFNA.js → chunk-VR4S4FIN-ic-KB2tF.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-DPyV10yR.js → chunk-XXDRQBXY-BqeYnem3.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-arTJL-2y.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-arTJL-2y.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-Bt_rpukK.js → cose-bilkent-JH36ORCC-uaIgs2-N.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-Z8eMzFMv.js → cynefin-VYW2F7L2-DzFeugVQ.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-BJSNawFi.js → cynefinDiagram-TSTJHNR4-NrAChuCW.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-D6dNl8jc.js → dagre-VKFMJZFB-Djf9LD2t.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-Cy1nbGsU.js → diagram-FQU43EPY-BpIP2iD9.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-CmrbuA_d.js → diagram-G47NLZAW-D_DCw8Oy.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-PoLNQE2j.js → diagram-NH7WQ7WH-BztvRvrM.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-BZwNFcSV.js → diagram-OA4YK3LP-BGIwSnbM.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-pKw0b_fN.js → diagram-WEI45ONY-Czj2ttCt.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-UNctuQCV.js → ebnfDiagram-CCIWWBDH-0nqAhMvJ.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-DxIETMif.js → erDiagram-Q63AITRT-uLoGb9TG.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-C06IdNKV.js → flowDiagram-23GEKE2U-Bgxy7CWO.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-fU7aGyP1.js → ganttDiagram-NO4QXBWP-5q8zYzbu.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-9UKkPJm6.js → gitGraphDiagram-IHSO6WYX-D6-i59JZ.js} +1 -1
- package/web/dist/assets/index-B3WlYEvi.css +1 -0
- package/web/dist/assets/index-BFa_e8zL.js +24 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-C6F06SQJ.js → infoDiagram-FWYZ7A6U-bdx9GrWW.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-CB-O9NfO.js → ishikawaDiagram-FXEZZL3T-xzL6Bssj.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-B-8MOf-H.js → journeyDiagram-5HDEW3XC-C1-18xOk.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-DX0XA3hI.js → kanban-definition-HUTT4EX6-BS4KDL4_.js} +1 -1
- package/web/dist/assets/{linear-D60PrjTW.js → linear-Dmv3ton3.js} +1 -1
- package/web/dist/assets/{mermaid.core-bonzwapf.js → mermaid.core-Nhzw7Fl2.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-Y-6Dum33.js → mindmap-definition-LN4V7U3C-2DSMdlsp.js} +1 -1
- package/web/dist/assets/{papaparse.min-Cas1JfRG.js → papaparse.min-H4g6q6aH.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-_paO0yDw.js → pegDiagram-2B236MQR-CM_rgJ2s.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-xT1_Gpn4.js → pieDiagram-ENE6RG2P-Cbo8FtuT.js} +1 -1
- package/web/dist/assets/pilotswarm-ClLipXts.js +97 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-1usdLFOF.js → quadrantDiagram-ABIIQ3AL-CVsF_pZA.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-R3lwHOz9.js → railroadDiagram-RFXS5EU6-CauKYIfy.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-BL8J1__Z.js → requirementDiagram-TGXJPOKE-DoXh8EL8.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-Dz6--0pl.js → sankeyDiagram-HTMAVEWB-gA_ej2MB.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CG2Xpkct.js → sequenceDiagram-DBY2YBRQ-keYBa83Q.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-BPbYfjxj.js → sizeCapture-X5ZJPWSS-Y6NCdqw8.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-46x_y_aA.js → stateDiagram-2N3HPSRC-MvJfZXqW.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-xTZh36yB.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-SXdMo4zJ.js → swimlanes-5IMT3BWC-D41i7zcr.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BjlBuuiD.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-CGgT-b38.js → timeline-definition-FHXFAJF6-CUA2ENV8.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-BGS3xBa_.js → vennDiagram-L72KCM5P-CBTp-2d6.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-m2U3wEZJ.js → wardleyDiagram-EHGQE667-BIFjtpCg.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-DI6tQysg.js → xychartDiagram-FW5EYKEG-DiamOwOI.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; }
|
|
@@ -6683,6 +6930,15 @@ function CanvasPane({ controller, mobile = false, visible = true, focusOnPromote
|
|
|
6683
6930
|
React.createElement(CanvasSlotControls, { controller, view })),
|
|
6684
6931
|
React.createElement("span", { className: "ps-artifact-pane-actions" },
|
|
6685
6932
|
view.exists ? React.createElement(HtmlZoomControl, { zoom, setZoom }) : null,
|
|
6933
|
+
view.exists
|
|
6934
|
+
? React.createElement("button", {
|
|
6935
|
+
type: "button",
|
|
6936
|
+
className: "ps-artifact-pane-btn",
|
|
6937
|
+
onClick: () => setShareOpen(true),
|
|
6938
|
+
title: "Share this canvas — copy a view link",
|
|
6939
|
+
"aria-label": "Share canvas",
|
|
6940
|
+
}, React.createElement(CanvasLinkGlyph))
|
|
6941
|
+
: null,
|
|
6686
6942
|
// Zen: sessions step away, chat becomes a rail, the canvas is
|
|
6687
6943
|
// the workbench. The mode for "the work happens on the canvas,
|
|
6688
6944
|
// chat is for steering".
|
|
@@ -6727,11 +6983,20 @@ function CanvasPane({ controller, mobile = false, visible = true, focusOnPromote
|
|
|
6727
6983
|
focusOnPromote,
|
|
6728
6984
|
dataRev: view.latestDataRev,
|
|
6729
6985
|
dataPayload: view.dataPayload,
|
|
6986
|
+
dataPatch: view.dataPatch,
|
|
6730
6987
|
})
|
|
6731
6988
|
: React.createElement("div", { className: "ps-canvas-blank" },
|
|
6732
6989
|
React.createElement("p", { className: "ps-canvas-blank-title" }, "Nothing on the canvas yet."),
|
|
6733
6990
|
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.")))
|
|
6991
|
+
"Ask the agent to draw — a dashboard, a chart, a diagram — or it will draw when it has something worth showing."))),
|
|
6992
|
+
shareOpen && view.exists
|
|
6993
|
+
? React.createElement(CanvasShareDialog, {
|
|
6994
|
+
controller,
|
|
6995
|
+
sessionId: view.sessionId,
|
|
6996
|
+
slot: view.slot,
|
|
6997
|
+
onClose: () => setShareOpen(false),
|
|
6998
|
+
})
|
|
6999
|
+
: null);
|
|
6735
7000
|
}
|
|
6736
7001
|
|
|
6737
7002
|
function InspectorTabs({ activeTab, controller }) {
|
|
@@ -7239,9 +7504,16 @@ function InspectorPane({ controller, mobile = false, panelClassName = "", extraA
|
|
|
7239
7504
|
// could pan it — in practice it rendered clipped on BOTH edges (the
|
|
7240
7505
|
// timestamps and the STATS header sliced off) and read as broken. Fitting
|
|
7241
7506
|
// to width collapses surplus lanes into "…" instead, which is legible.
|
|
7507
|
+
//
|
|
7508
|
+
// The width itself is the pane's MEASURED column count wherever a
|
|
7509
|
+
// measurement exists — the legacy-model rightWidth imagines a split the
|
|
7510
|
+
// portal's draggable grid does not honor, which is how the sequence grid
|
|
7511
|
+
// used to spill past the pane edge and the stats boxes arrived pre-sliced.
|
|
7512
|
+
const [measuredCols, panelRef] = useMeasuredPaneColumns();
|
|
7513
|
+
const contentWidth = measuredCols ?? viewState.contentWidth;
|
|
7242
7514
|
const inspector = React.useMemo(() => selectInspector(selectorState, {
|
|
7243
|
-
width:
|
|
7244
|
-
}), [selectorState,
|
|
7515
|
+
width: contentWidth,
|
|
7516
|
+
}), [selectorState, contentWidth]);
|
|
7245
7517
|
const completionByTurn = React.useMemo(() => {
|
|
7246
7518
|
const history = viewState.activeSessionId
|
|
7247
7519
|
? viewState.historyBySessionId?.get(viewState.activeSessionId) || null
|
|
@@ -7391,6 +7663,7 @@ function InspectorPane({ controller, mobile = false, panelClassName = "", extraA
|
|
|
7391
7663
|
: "top",
|
|
7392
7664
|
stickyBottom: inspector.activeTab === "logs",
|
|
7393
7665
|
paneKey: "inspector",
|
|
7666
|
+
panelRef,
|
|
7394
7667
|
className: inspector.activeTab === "history" || inspector.activeTab === "logs" ? "is-wrapped" : "is-preserve",
|
|
7395
7668
|
panelClassName: `ps-inspector-pane${inspector.activeTab === "sequence" ? " has-preserved-sticky" : ""}${panelClassName ? ` ${panelClassName}` : ""}`.trim(),
|
|
7396
7669
|
});
|
|
@@ -7564,6 +7837,33 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7564
7837
|
const inputRef = React.useRef(null);
|
|
7565
7838
|
const attachInputRef = React.useRef(null);
|
|
7566
7839
|
const [dragOver, setDragOver] = React.useState(false);
|
|
7840
|
+
// Expand mode: the same buffer, editor-sized. Web-local view state — the
|
|
7841
|
+
// shared core never needs to know the box got taller.
|
|
7842
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
7843
|
+
|
|
7844
|
+
// Auto-grow: one line idle, sized to the RENDERED content (scrollHeight
|
|
7845
|
+
// sees soft wrap; counting "\n" does not). CSS max-height provides the
|
|
7846
|
+
// cap, after which the textarea scrolls internally. In expand mode the
|
|
7847
|
+
// height belongs to CSS entirely.
|
|
7848
|
+
const growInput = React.useCallback(() => {
|
|
7849
|
+
const node = inputRef.current;
|
|
7850
|
+
if (!node) return;
|
|
7851
|
+
if (expanded) {
|
|
7852
|
+
node.style.height = "";
|
|
7853
|
+
return;
|
|
7854
|
+
}
|
|
7855
|
+
node.style.height = "auto";
|
|
7856
|
+
node.style.height = `${node.scrollHeight + 2}px`;
|
|
7857
|
+
}, [expanded]);
|
|
7858
|
+
React.useLayoutEffect(() => {
|
|
7859
|
+
growInput();
|
|
7860
|
+
}, [growInput, promptState.value]);
|
|
7861
|
+
React.useEffect(() => {
|
|
7862
|
+
// Width changes re-wrap the content; re-measure on viewport resizes
|
|
7863
|
+
// (covers rotation and the on-screen keyboard shrinking the pane).
|
|
7864
|
+
window.addEventListener("resize", growInput);
|
|
7865
|
+
return () => window.removeEventListener("resize", growInput);
|
|
7866
|
+
}, [growInput]);
|
|
7567
7867
|
|
|
7568
7868
|
const canAttachImages = typeof controller.transport?.supportsPromptImageAttachments === "function"
|
|
7569
7869
|
&& controller.transport.supportsPromptImageAttachments()
|
|
@@ -7609,16 +7909,31 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7609
7909
|
const inputNode = inputRef.current;
|
|
7610
7910
|
if (!active || promptState.modalOpen || !promptState.focused || !inputNode) return;
|
|
7611
7911
|
if (document.activeElement !== inputNode) {
|
|
7912
|
+
// Programmatic focus pops the on-screen keyboard on touch devices;
|
|
7913
|
+
// there, focus only ever comes from the user's own tap.
|
|
7914
|
+
if (mobile) return;
|
|
7612
7915
|
try {
|
|
7613
7916
|
inputNode.focus({ preventScroll: true });
|
|
7614
7917
|
} catch {
|
|
7615
7918
|
inputNode.focus();
|
|
7616
7919
|
}
|
|
7617
7920
|
}
|
|
7618
|
-
|
|
7619
|
-
|
|
7921
|
+
// Apply the model's caret ONLY when it disagrees with the DOM — that
|
|
7922
|
+
// is a programmatic intervention (pending-message recall, reference
|
|
7923
|
+
// autocomplete insert, draft restore). While the user edits, the DOM
|
|
7924
|
+
// is the source of truth and onSelect/onChange mirror it into state;
|
|
7925
|
+
// unconditionally echoing state back used to collapse in-progress
|
|
7926
|
+
// backward selections (mouse drag right-to-left, Shift+Left, and the
|
|
7927
|
+
// start handle of a touch selection).
|
|
7928
|
+
if (inputNode.value === promptState.value && inputNode.selectionStart !== promptState.cursor) {
|
|
7929
|
+
inputNode.setSelectionRange(promptState.cursor, promptState.cursor);
|
|
7930
|
+
}
|
|
7931
|
+
}, [active, mobile, promptState.cursor, promptState.value, promptState.focused, promptState.modalOpen]);
|
|
7620
7932
|
|
|
7621
7933
|
const sendPrompt = React.useCallback(() => {
|
|
7934
|
+
// Sending collapses expand mode — the tall editor was for composing
|
|
7935
|
+
// THIS message; the reply deserves the transcript space back.
|
|
7936
|
+
setExpanded(false);
|
|
7622
7937
|
controller.handleCommand(UI_COMMANDS.SEND_PROMPT)
|
|
7623
7938
|
.catch(() => {})
|
|
7624
7939
|
.finally(() => {
|
|
@@ -7658,7 +7973,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7658
7973
|
const selectedReadOnly = selectedQueued || selectedCancelling;
|
|
7659
7974
|
|
|
7660
7975
|
return React.createElement("div", {
|
|
7661
|
-
className: `ps-prompt-shell${mobile ? " is-mobile" : ""}${dragOver ? " is-drag-over" : ""}`,
|
|
7976
|
+
className: `ps-prompt-shell${mobile ? " is-mobile" : ""}${dragOver ? " is-drag-over" : ""}${expanded ? " is-expanded" : ""}`,
|
|
7662
7977
|
...(canAttachImages ? {
|
|
7663
7978
|
onDragOver: (event) => {
|
|
7664
7979
|
if (event.dataTransfer?.types?.includes?.("Files")) {
|
|
@@ -7720,7 +8035,9 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7720
8035
|
React.createElement("textarea", {
|
|
7721
8036
|
ref: inputRef,
|
|
7722
8037
|
className: "ps-prompt-input",
|
|
7723
|
-
|
|
8038
|
+
// One line at rest; a layout effect grows it to the rendered
|
|
8039
|
+
// content and CSS max-height caps it. Never a manual resize grip.
|
|
8040
|
+
rows: 1,
|
|
7724
8041
|
value: promptState.value,
|
|
7725
8042
|
readOnly: selectedReadOnly,
|
|
7726
8043
|
placeholder: promptState.answerMode
|
|
@@ -7734,7 +8051,9 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7734
8051
|
: promptState.hasOutbox
|
|
7735
8052
|
? "Type a message and press Enter to queue it behind the pending batch"
|
|
7736
8053
|
: "Type a message and press Enter",
|
|
7737
|
-
|
|
8054
|
+
// On touch, Enter inserts a newline (the chevron sends) — the
|
|
8055
|
+
// keyboard must not advertise a send that will not happen.
|
|
8056
|
+
enterKeyHint: mobile ? "enter" : "send",
|
|
7738
8057
|
onFocus: () => controller.setFocus("prompt"),
|
|
7739
8058
|
onSelect: (event) => controller.setPromptCursor(event.currentTarget.selectionStart || 0),
|
|
7740
8059
|
onChange: (event) => controller.setPrompt(event.currentTarget.value, event.currentTarget.selectionStart || event.currentTarget.value.length),
|
|
@@ -7758,14 +8077,14 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7758
8077
|
event.preventDefault();
|
|
7759
8078
|
return;
|
|
7760
8079
|
}
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
if (event.key === "
|
|
8080
|
+
// Arrow keys are NOT intercepted: the textarea's native cursor
|
|
8081
|
+
// movement understands soft-wrapped lines and goal columns;
|
|
8082
|
+
// onSelect mirrors every move into the shared model. (The old
|
|
8083
|
+
// hijack routed through the TUI's logical-line cursor and made
|
|
8084
|
+
// Down jump whole paragraphs inside wrapped text.)
|
|
8085
|
+
if (event.key === "Escape" && expanded && !promptState.editingPending) {
|
|
7767
8086
|
event.preventDefault();
|
|
7768
|
-
|
|
8087
|
+
setExpanded(false);
|
|
7769
8088
|
return;
|
|
7770
8089
|
}
|
|
7771
8090
|
if (event.key === "Escape" && promptState.editingPending) {
|
|
@@ -7784,6 +8103,18 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7784
8103
|
},
|
|
7785
8104
|
}),
|
|
7786
8105
|
React.createElement("div", { className: "ps-prompt-actions" },
|
|
8106
|
+
// pointerdown preventDefault on every action keeps focus in the
|
|
8107
|
+
// textarea — on a phone, tapping Send must not collapse the
|
|
8108
|
+
// keyboard between messages.
|
|
8109
|
+
React.createElement("button", {
|
|
8110
|
+
type: "button",
|
|
8111
|
+
className: `ps-mini-button ps-expand-button${expanded ? " is-active" : ""}`,
|
|
8112
|
+
title: expanded ? "Collapse the editor (Esc)" : "Expand into a larger editor",
|
|
8113
|
+
"aria-label": expanded ? "Collapse the editor" : "Expand the editor",
|
|
8114
|
+
"aria-pressed": expanded,
|
|
8115
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
8116
|
+
onClick: () => setExpanded((current) => !current),
|
|
8117
|
+
}, expanded ? "⤡" : "⤢"),
|
|
7787
8118
|
canAttachImages
|
|
7788
8119
|
? React.createElement(React.Fragment, null,
|
|
7789
8120
|
React.createElement("input", {
|
|
@@ -7807,6 +8138,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7807
8138
|
className: "ps-mini-button ps-attach-button",
|
|
7808
8139
|
title: "Attach images (or paste / drop them into the composer)",
|
|
7809
8140
|
"aria-label": "Attach images",
|
|
8141
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
7810
8142
|
onClick: () => attachInputRef.current?.click(),
|
|
7811
8143
|
}, "📎"))
|
|
7812
8144
|
: null,
|
|
@@ -7816,6 +8148,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7816
8148
|
className: "ps-mini-button",
|
|
7817
8149
|
title: selectedQueued ? "Delete selected queued prompt" : "Cancel selected pending prompt",
|
|
7818
8150
|
"aria-label": selectedQueued ? "Delete selected queued prompt" : "Cancel selected pending prompt",
|
|
8151
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
7819
8152
|
onClick: cancelPending,
|
|
7820
8153
|
}, selectedQueued ? "Delete" : "Cancel")
|
|
7821
8154
|
: null,
|
|
@@ -7826,6 +8159,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7826
8159
|
title: "Stop the current turn (the session stays alive and returns to idle)",
|
|
7827
8160
|
"aria-label": "Stop the current turn",
|
|
7828
8161
|
disabled: stoppingTurn,
|
|
8162
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
7829
8163
|
onClick: stopTurn,
|
|
7830
8164
|
}, "■")
|
|
7831
8165
|
: null,
|
|
@@ -7842,6 +8176,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7842
8176
|
: promptState.hasOutbox
|
|
7843
8177
|
? "Queue prompt"
|
|
7844
8178
|
: "Send prompt",
|
|
8179
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
7845
8180
|
onClick: sendPrompt,
|
|
7846
8181
|
}, sendLabel),
|
|
7847
8182
|
),
|