pilotswarm 0.5.38 → 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/mcp/dist/src/tools/agent-packages.d.ts.map +1 -1
- package/mcp/dist/src/tools/agent-packages.js +34 -14
- package/mcp/dist/src/tools/agent-packages.js.map +1 -1
- package/package.json +2 -2
- package/tui/src/node-sdk-transport.js +138 -18
- package/ui/core/src/controller.js +133 -37
- package/ui/core/src/history.js +5 -1
- package/ui/core/src/index.js +1 -0
- package/ui/core/src/layout.js +10 -0
- package/ui/core/src/portal-links.js +46 -0
- package/ui/core/src/reducer.js +140 -7
- package/ui/core/src/selectors.js +51 -7
- package/ui/react/src/web-app.js +445 -53
- 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-DrJhXhSQ.js → abnfDiagram-VRR7QNED-C6FaYMB7.js} +1 -1
- package/web/dist/assets/{arc-BEaG9DWd.js → arc-iZBUHcPs.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-D9fc1Imx.js → architectureDiagram-ZJ3FMSHR-D4-wnpAG.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-rS5Eikvt.js → blockDiagram-677ZJIJ3-Dz-s103f.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-HKLlX5cR.js → c4Diagram-LMCZKHZV-DE7sGdHE.js} +1 -1
- package/web/dist/assets/channel-Bk68hLPV.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-kq0TyLSB.js → chunk-2Q5K7J3B-DSFs_jRM.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-O-LMarT5.js → chunk-32BRIVSS-8sBVtZ4f.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-Di4h0CAu.js → chunk-5VM5RSS4-CbIX7k0G.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-C8rgqUOt.js → chunk-EX3LRPZG-ZfmPtWF8.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-BSH41q4T.js → chunk-JWPE2WC7-DifZrqsQ.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-BAFuQHAC.js → chunk-MOJQB5TN-CEvg_lZT.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-DAg6JOs7.js → chunk-RYQCIY6F-DmYOwk6-.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-DbVrT8BG.js → chunk-V7JOEXUC-DOxG9Vxu.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-C6f5_Grp.js → chunk-VR4S4FIN-ic-KB2tF.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-CqcCYpqA.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-DJw04eFh.js → cose-bilkent-JH36ORCC-uaIgs2-N.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-CpjwdEIU.js → cynefin-VYW2F7L2-DzFeugVQ.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-RqEIWoT0.js → cynefinDiagram-TSTJHNR4-NrAChuCW.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-C1eGQNK2.js → dagre-VKFMJZFB-Djf9LD2t.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-BODLQ7dz.js → diagram-FQU43EPY-BpIP2iD9.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-DlhmukMC.js → diagram-G47NLZAW-D_DCw8Oy.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-Ba_jZoCN.js → diagram-NH7WQ7WH-BztvRvrM.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-DnSiAJBG.js → diagram-OA4YK3LP-BGIwSnbM.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-B3KoQ7_W.js → diagram-WEI45ONY-Czj2ttCt.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-ChnFc5gf.js → ebnfDiagram-CCIWWBDH-0nqAhMvJ.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-D2KkJXLZ.js → erDiagram-Q63AITRT-uLoGb9TG.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-CmybZufj.js → flowDiagram-23GEKE2U-Bgxy7CWO.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-BzPurW_v.js → ganttDiagram-NO4QXBWP-5q8zYzbu.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-DAbXl4gO.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-C6qGjNA1.js → infoDiagram-FWYZ7A6U-bdx9GrWW.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-D2d1tO-9.js → ishikawaDiagram-FXEZZL3T-xzL6Bssj.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-fP13w1b7.js → journeyDiagram-5HDEW3XC-C1-18xOk.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-BJfWAXZ0.js → kanban-definition-HUTT4EX6-BS4KDL4_.js} +1 -1
- package/web/dist/assets/{linear-CltApkHf.js → linear-Dmv3ton3.js} +1 -1
- package/web/dist/assets/{mermaid.core-B1Dwetk3.js → mermaid.core-Nhzw7Fl2.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-CdRUD7LF.js → mindmap-definition-LN4V7U3C-2DSMdlsp.js} +1 -1
- package/web/dist/assets/{papaparse.min-C1y3wBH1.js → papaparse.min-H4g6q6aH.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-CeQkqN6Q.js → pegDiagram-2B236MQR-CM_rgJ2s.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-Dqgc87jA.js → pieDiagram-ENE6RG2P-Cbo8FtuT.js} +1 -1
- package/web/dist/assets/pilotswarm-ClLipXts.js +97 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-CD7f-w6m.js → quadrantDiagram-ABIIQ3AL-CVsF_pZA.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-CcsAijSC.js → railroadDiagram-RFXS5EU6-CauKYIfy.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-BkzF80pL.js → requirementDiagram-TGXJPOKE-DoXh8EL8.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-D9XOFw7Y.js → sankeyDiagram-HTMAVEWB-gA_ej2MB.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-BFfaS_Sz.js → sequenceDiagram-DBY2YBRQ-keYBa83Q.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-CDJV16tP.js → sizeCapture-X5ZJPWSS-Y6NCdqw8.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-C0E-MPXO.js → stateDiagram-2N3HPSRC-MvJfZXqW.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-xTZh36yB.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-n5Vsv7n9.js → swimlanes-5IMT3BWC-D41i7zcr.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BjlBuuiD.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-DW2gV8-n.js → timeline-definition-FHXFAJF6-CUA2ENV8.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-D9dbDm08.js → vennDiagram-L72KCM5P-CBTp-2d6.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-CIcG7fiC.js → wardleyDiagram-EHGQE667-BIFjtpCg.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-elEIFsc9.js → xychartDiagram-FW5EYKEG-DiamOwOI.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/runtime.js +101 -7
- package/web/server.js +51 -2
- package/web/dist/assets/channel-DRy7FvyF.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-C35RhB8X.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-C35RhB8X.js +0 -1
- package/web/dist/assets/index-CRY94nFp.js +0 -24
- package/web/dist/assets/index-DtL0lbis.css +0 -1
- package/web/dist/assets/pilotswarm-DayYdi39.js +0 -96
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-BMqXeMJu.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-CGl4gRnH.js +0 -8
package/ui/react/src/web-app.js
CHANGED
|
@@ -12,11 +12,14 @@ 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,
|
|
18
20
|
formatCronTimestampForClient,
|
|
19
21
|
formatHumanDurationSeconds,
|
|
22
|
+
getDiagnosticsSplitAdjustBounds,
|
|
20
23
|
getPromptInputRows,
|
|
21
24
|
getTheme,
|
|
22
25
|
isThemeLight,
|
|
@@ -173,8 +176,21 @@ function buildArtifactLinkUrl(sessionId, filename, { fullscreen = true } = {}) {
|
|
|
173
176
|
return `${base}&artifact=${encodeURIComponent(filename)}${fullscreen ? "&view=full" : ""}`;
|
|
174
177
|
}
|
|
175
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
|
+
|
|
176
191
|
function copySessionLinkText(url) {
|
|
177
|
-
|
|
192
|
+
const text = formatPortalLinksForCopy(buildPortalLinks(url, portalLinkOrigins));
|
|
193
|
+
if (navigator?.clipboard?.writeText) navigator.clipboard.writeText(text).catch(() => {});
|
|
178
194
|
}
|
|
179
195
|
|
|
180
196
|
// Best-effort probe: is this session's deep link openable only by the owner/
|
|
@@ -633,6 +649,77 @@ function getPortalInspectorContentWidth(paneWidth, inspectorTab, mobile = false)
|
|
|
633
649
|
return Math.max(20, paneWidth - (mobile ? 6 : 4));
|
|
634
650
|
}
|
|
635
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
|
+
|
|
636
723
|
function normalizeLines(lines) {
|
|
637
724
|
const normalized = [];
|
|
638
725
|
|
|
@@ -3734,10 +3821,17 @@ function focusRegionForPaneKey(paneKey, override = null) {
|
|
|
3734
3821
|
return PANE_KEY_FOCUS_REGIONS[String(paneKey || "")] || null;
|
|
3735
3822
|
}
|
|
3736
3823
|
|
|
3737
|
-
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 }) {
|
|
3738
3825
|
const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
|
|
3739
3826
|
const theme = getTheme(themeId);
|
|
3740
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]);
|
|
3741
3835
|
const stickyRef = React.useRef(null);
|
|
3742
3836
|
const syncingHorizontalRef = React.useRef(false);
|
|
3743
3837
|
const { normalizedLines, onScroll, onWheel, onTouchStart, onTouchMove, onTouchEnd } = useScrollSync(ref, lines, scrollOffset, scrollMode, paneKey, controller, { stickyBottom });
|
|
@@ -3827,7 +3921,7 @@ function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, l
|
|
|
3827
3921
|
normalizedSticky.map((line, index) => React.createElement(Line, { key: `sticky:${index}`, line, theme })),
|
|
3828
3922
|
)
|
|
3829
3923
|
: null,
|
|
3830
|
-
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 },
|
|
3831
3925
|
typeof renderBody === "function"
|
|
3832
3926
|
? renderBody(normalizedLines, theme)
|
|
3833
3927
|
: structuredBlocks
|
|
@@ -3925,6 +4019,7 @@ function SessionDetailBox({ session, childCount = 0 }) {
|
|
|
3925
4019
|
// used, so the box height still cannot move with the selection.
|
|
3926
4020
|
field("Title", session?.title, "is-title"),
|
|
3927
4021
|
field("ID", session?.sessionId, "is-id"),
|
|
4022
|
+
field("Owner", session?.owner ? formatAdminPrincipalLabel(session.owner) : null),
|
|
3928
4023
|
field("Model", model),
|
|
3929
4024
|
field("Context", context, percent != null && percent >= 85 ? "is-hot" : percent != null && percent >= 70 ? "is-warm" : ""),
|
|
3930
4025
|
field("Cron", cron, session?.cronActive === true ? "is-armed" : ""),
|
|
@@ -5136,15 +5231,24 @@ function SessionLinkModal({ url, warn, onCopyAgain, onClose }) {
|
|
|
5136
5231
|
return React.createElement("div", { className: "ps-share-overlay", onClick: onClose },
|
|
5137
5232
|
React.createElement("div", { className: "ps-link-modal", onClick: stop },
|
|
5138
5233
|
React.createElement("div", { className: "ps-share-modal-head" },
|
|
5139
|
-
React.createElement("span", null, "Link copied"),
|
|
5234
|
+
React.createElement("span", null, portalLinkOrigins.length >= 2 ? "Session links copied" : "Link copied"),
|
|
5140
5235
|
React.createElement("button", { className: "ps-modal-close", onClick: onClose, "aria-label": "Close", title: "Close" }, "✕")),
|
|
5141
5236
|
React.createElement("div", { className: "ps-share-section-sub" },
|
|
5142
|
-
|
|
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."),
|
|
5143
5240
|
React.createElement("div", { className: "ps-link-row" },
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
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
|
+
}),
|
|
5148
5252
|
React.createElement("button", { className: "ps-mini-button", onClick: onCopyAgain }, "Copy")),
|
|
5149
5253
|
warn
|
|
5150
5254
|
? React.createElement("div", { className: "ps-link-warn" }, SESSION_LINK_PRIVATE_WARNING)
|
|
@@ -5788,9 +5892,14 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
5788
5892
|
viewState.sessionsById,
|
|
5789
5893
|
viewState.sessionsFlat,
|
|
5790
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;
|
|
5791
5900
|
const chrome = React.useMemo(
|
|
5792
|
-
() => selectChatPaneChrome(selectorState, { width:
|
|
5793
|
-
[selectorState,
|
|
5901
|
+
() => selectChatPaneChrome(selectorState, { width: contentWidth }),
|
|
5902
|
+
[selectorState, contentWidth],
|
|
5794
5903
|
);
|
|
5795
5904
|
const animatedDots = useAnimatedDots(Boolean(chrome.animateTitleRight));
|
|
5796
5905
|
const titleRight = React.useMemo(
|
|
@@ -5807,21 +5916,21 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
5807
5916
|
&& Number(viewState.activeHistory?.loadedEventCount || 0) >= AUTO_HISTORY_EVENT_SOFT_CAP,
|
|
5808
5917
|
);
|
|
5809
5918
|
const lines = React.useMemo(
|
|
5810
|
-
() => selectChatLines(selectorState,
|
|
5811
|
-
[selectorState,
|
|
5919
|
+
() => selectChatLines(selectorState, contentWidth, { tableMode: "sentinel" }),
|
|
5920
|
+
[selectorState, contentWidth],
|
|
5812
5921
|
);
|
|
5813
5922
|
const spinnerFrame = useSpinnerFrame(viewState.activeSessionStatus === "running");
|
|
5814
5923
|
const liveActivityLines = React.useMemo(
|
|
5815
|
-
() => selectLiveActivityLines(selectorState, { spinnerFrame, maxWidth:
|
|
5816
|
-
[selectorState, spinnerFrame,
|
|
5924
|
+
() => selectLiveActivityLines(selectorState, { spinnerFrame, maxWidth: contentWidth }),
|
|
5925
|
+
[selectorState, spinnerFrame, contentWidth],
|
|
5817
5926
|
);
|
|
5818
5927
|
// The live-activity line is pinned in the bottom-sticky strip (with the
|
|
5819
5928
|
// outbox overlay), NOT appended to the transcript — it must stay put while
|
|
5820
5929
|
// chat content scrolls, and it drops the instant the turn ends.
|
|
5821
5930
|
const pinnedActivityLines = liveActivityLines;
|
|
5822
5931
|
const outboxLines = React.useMemo(
|
|
5823
|
-
() => selectOutboxOverlayLines(selectorState,
|
|
5824
|
-
[selectorState,
|
|
5932
|
+
() => selectOutboxOverlayLines(selectorState, contentWidth, { tableMode: "sentinel" }),
|
|
5933
|
+
[selectorState, contentWidth],
|
|
5825
5934
|
);
|
|
5826
5935
|
const stickyBottom = React.useMemo(
|
|
5827
5936
|
() => (pinnedActivityLines.length > 0 ? [...outboxLines, ...pinnedActivityLines] : outboxLines),
|
|
@@ -5892,6 +6001,7 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
5892
6001
|
paneKey: "chat",
|
|
5893
6002
|
className: "is-wrapped",
|
|
5894
6003
|
panelClassName: "ps-chat-panel",
|
|
6004
|
+
panelRef,
|
|
5895
6005
|
bottomContent: composer,
|
|
5896
6006
|
topContent: showLoadOlder
|
|
5897
6007
|
? React.createElement("div", { className: "ps-load-older-bar" },
|
|
@@ -6254,6 +6364,135 @@ function SessionCanvasMark({ mark }) {
|
|
|
6254
6364
|
: null));
|
|
6255
6365
|
}
|
|
6256
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
|
+
|
|
6257
6496
|
/**
|
|
6258
6497
|
* The canvas slot controls: a dropdown when the session has more than one
|
|
6259
6498
|
* drawn canvas, plus prev/next arrows. Hidden entirely with one canvas —
|
|
@@ -6320,7 +6559,7 @@ function RestoreGlyph() {
|
|
|
6320
6559
|
* loads hidden-but-painted behind the live frame and is promoted on load, so
|
|
6321
6560
|
* a live redraw is a composite swap of something already drawn.
|
|
6322
6561
|
*/
|
|
6323
|
-
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 }) {
|
|
6324
6563
|
const [live, setLive] = React.useState(null); // { url, rev }
|
|
6325
6564
|
const [staging, setStaging] = React.useState(null); // { url, rev }
|
|
6326
6565
|
const [loadError, setLoadError] = React.useState(null); // { rev }
|
|
@@ -6469,13 +6708,22 @@ function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible
|
|
|
6469
6708
|
liveIframeElRef.current?.contentWindow?.postMessage(
|
|
6470
6709
|
{ type: "canvas-data", data: dataPayload, dataRev }, "*",
|
|
6471
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
|
+
}
|
|
6472
6720
|
} catch { /* frame mid-teardown */ }
|
|
6473
6721
|
// On-screen injection = seen; hidden injection leaves the badge lit
|
|
6474
6722
|
// until the user actually looks (the catch-up below files it then).
|
|
6475
6723
|
if (controller && sessionId && visibleRef.current) {
|
|
6476
6724
|
controller.dispatch({ type: "canvas/viewed", sessionId, slot, dataRev });
|
|
6477
6725
|
}
|
|
6478
|
-
}, [live, dataRev, dataPayload, controller, sessionId]);
|
|
6726
|
+
}, [live, dataRev, dataPayload, dataPatch, controller, sessionId]);
|
|
6479
6727
|
|
|
6480
6728
|
// Catch-up receipt: becoming visible with a live revision on screen IS
|
|
6481
6729
|
// viewing it — this is what clears a badge earned while hidden.
|
|
@@ -6599,6 +6847,7 @@ function CanvasPane({ controller, mobile = false, visible = true, focusOnPromote
|
|
|
6599
6847
|
}, [controller, view.sessionId]);
|
|
6600
6848
|
|
|
6601
6849
|
const zenOn = useControllerSelector(controller, (s) => s?.ui?.canvasZen === true);
|
|
6850
|
+
const [shareOpen, setShareOpen] = React.useState(false);
|
|
6602
6851
|
const [toolbarSlot, setToolbarSlot] = React.useState(null);
|
|
6603
6852
|
React.useEffect(() => {
|
|
6604
6853
|
if (!maximized) { setToolbarSlot(null); return undefined; }
|
|
@@ -6681,6 +6930,15 @@ function CanvasPane({ controller, mobile = false, visible = true, focusOnPromote
|
|
|
6681
6930
|
React.createElement(CanvasSlotControls, { controller, view })),
|
|
6682
6931
|
React.createElement("span", { className: "ps-artifact-pane-actions" },
|
|
6683
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,
|
|
6684
6942
|
// Zen: sessions step away, chat becomes a rail, the canvas is
|
|
6685
6943
|
// the workbench. The mode for "the work happens on the canvas,
|
|
6686
6944
|
// chat is for steering".
|
|
@@ -6725,11 +6983,20 @@ function CanvasPane({ controller, mobile = false, visible = true, focusOnPromote
|
|
|
6725
6983
|
focusOnPromote,
|
|
6726
6984
|
dataRev: view.latestDataRev,
|
|
6727
6985
|
dataPayload: view.dataPayload,
|
|
6986
|
+
dataPatch: view.dataPatch,
|
|
6728
6987
|
})
|
|
6729
6988
|
: React.createElement("div", { className: "ps-canvas-blank" },
|
|
6730
6989
|
React.createElement("p", { className: "ps-canvas-blank-title" }, "Nothing on the canvas yet."),
|
|
6731
6990
|
React.createElement("p", null,
|
|
6732
|
-
"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);
|
|
6733
7000
|
}
|
|
6734
7001
|
|
|
6735
7002
|
function InspectorTabs({ activeTab, controller }) {
|
|
@@ -7237,9 +7504,16 @@ function InspectorPane({ controller, mobile = false, panelClassName = "", extraA
|
|
|
7237
7504
|
// could pan it — in practice it rendered clipped on BOTH edges (the
|
|
7238
7505
|
// timestamps and the STATS header sliced off) and read as broken. Fitting
|
|
7239
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;
|
|
7240
7514
|
const inspector = React.useMemo(() => selectInspector(selectorState, {
|
|
7241
|
-
width:
|
|
7242
|
-
}), [selectorState,
|
|
7515
|
+
width: contentWidth,
|
|
7516
|
+
}), [selectorState, contentWidth]);
|
|
7243
7517
|
const completionByTurn = React.useMemo(() => {
|
|
7244
7518
|
const history = viewState.activeSessionId
|
|
7245
7519
|
? viewState.historyBySessionId?.get(viewState.activeSessionId) || null
|
|
@@ -7389,6 +7663,7 @@ function InspectorPane({ controller, mobile = false, panelClassName = "", extraA
|
|
|
7389
7663
|
: "top",
|
|
7390
7664
|
stickyBottom: inspector.activeTab === "logs",
|
|
7391
7665
|
paneKey: "inspector",
|
|
7666
|
+
panelRef,
|
|
7392
7667
|
className: inspector.activeTab === "history" || inspector.activeTab === "logs" ? "is-wrapped" : "is-preserve",
|
|
7393
7668
|
panelClassName: `ps-inspector-pane${inspector.activeTab === "sequence" ? " has-preserved-sticky" : ""}${panelClassName ? ` ${panelClassName}` : ""}`.trim(),
|
|
7394
7669
|
});
|
|
@@ -7562,6 +7837,33 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7562
7837
|
const inputRef = React.useRef(null);
|
|
7563
7838
|
const attachInputRef = React.useRef(null);
|
|
7564
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]);
|
|
7565
7867
|
|
|
7566
7868
|
const canAttachImages = typeof controller.transport?.supportsPromptImageAttachments === "function"
|
|
7567
7869
|
&& controller.transport.supportsPromptImageAttachments()
|
|
@@ -7607,16 +7909,31 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7607
7909
|
const inputNode = inputRef.current;
|
|
7608
7910
|
if (!active || promptState.modalOpen || !promptState.focused || !inputNode) return;
|
|
7609
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;
|
|
7610
7915
|
try {
|
|
7611
7916
|
inputNode.focus({ preventScroll: true });
|
|
7612
7917
|
} catch {
|
|
7613
7918
|
inputNode.focus();
|
|
7614
7919
|
}
|
|
7615
7920
|
}
|
|
7616
|
-
|
|
7617
|
-
|
|
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]);
|
|
7618
7932
|
|
|
7619
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);
|
|
7620
7937
|
controller.handleCommand(UI_COMMANDS.SEND_PROMPT)
|
|
7621
7938
|
.catch(() => {})
|
|
7622
7939
|
.finally(() => {
|
|
@@ -7656,7 +7973,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7656
7973
|
const selectedReadOnly = selectedQueued || selectedCancelling;
|
|
7657
7974
|
|
|
7658
7975
|
return React.createElement("div", {
|
|
7659
|
-
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" : ""}`,
|
|
7660
7977
|
...(canAttachImages ? {
|
|
7661
7978
|
onDragOver: (event) => {
|
|
7662
7979
|
if (event.dataTransfer?.types?.includes?.("Files")) {
|
|
@@ -7718,7 +8035,9 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7718
8035
|
React.createElement("textarea", {
|
|
7719
8036
|
ref: inputRef,
|
|
7720
8037
|
className: "ps-prompt-input",
|
|
7721
|
-
|
|
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,
|
|
7722
8041
|
value: promptState.value,
|
|
7723
8042
|
readOnly: selectedReadOnly,
|
|
7724
8043
|
placeholder: promptState.answerMode
|
|
@@ -7732,7 +8051,9 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7732
8051
|
: promptState.hasOutbox
|
|
7733
8052
|
? "Type a message and press Enter to queue it behind the pending batch"
|
|
7734
8053
|
: "Type a message and press Enter",
|
|
7735
|
-
|
|
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",
|
|
7736
8057
|
onFocus: () => controller.setFocus("prompt"),
|
|
7737
8058
|
onSelect: (event) => controller.setPromptCursor(event.currentTarget.selectionStart || 0),
|
|
7738
8059
|
onChange: (event) => controller.setPrompt(event.currentTarget.value, event.currentTarget.selectionStart || event.currentTarget.value.length),
|
|
@@ -7756,14 +8077,14 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7756
8077
|
event.preventDefault();
|
|
7757
8078
|
return;
|
|
7758
8079
|
}
|
|
7759
|
-
|
|
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) {
|
|
7760
8086
|
event.preventDefault();
|
|
7761
|
-
|
|
7762
|
-
return;
|
|
7763
|
-
}
|
|
7764
|
-
if (event.key === "ArrowDown" && !event.shiftKey && !event.metaKey && !event.altKey) {
|
|
7765
|
-
event.preventDefault();
|
|
7766
|
-
controller.movePromptCursorVertical(1);
|
|
8087
|
+
setExpanded(false);
|
|
7767
8088
|
return;
|
|
7768
8089
|
}
|
|
7769
8090
|
if (event.key === "Escape" && promptState.editingPending) {
|
|
@@ -7782,6 +8103,18 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7782
8103
|
},
|
|
7783
8104
|
}),
|
|
7784
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 ? "⤡" : "⤢"),
|
|
7785
8118
|
canAttachImages
|
|
7786
8119
|
? React.createElement(React.Fragment, null,
|
|
7787
8120
|
React.createElement("input", {
|
|
@@ -7805,6 +8138,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7805
8138
|
className: "ps-mini-button ps-attach-button",
|
|
7806
8139
|
title: "Attach images (or paste / drop them into the composer)",
|
|
7807
8140
|
"aria-label": "Attach images",
|
|
8141
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
7808
8142
|
onClick: () => attachInputRef.current?.click(),
|
|
7809
8143
|
}, "📎"))
|
|
7810
8144
|
: null,
|
|
@@ -7814,6 +8148,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7814
8148
|
className: "ps-mini-button",
|
|
7815
8149
|
title: selectedQueued ? "Delete selected queued prompt" : "Cancel selected pending prompt",
|
|
7816
8150
|
"aria-label": selectedQueued ? "Delete selected queued prompt" : "Cancel selected pending prompt",
|
|
8151
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
7817
8152
|
onClick: cancelPending,
|
|
7818
8153
|
}, selectedQueued ? "Delete" : "Cancel")
|
|
7819
8154
|
: null,
|
|
@@ -7824,6 +8159,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7824
8159
|
title: "Stop the current turn (the session stays alive and returns to idle)",
|
|
7825
8160
|
"aria-label": "Stop the current turn",
|
|
7826
8161
|
disabled: stoppingTurn,
|
|
8162
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
7827
8163
|
onClick: stopTurn,
|
|
7828
8164
|
}, "■")
|
|
7829
8165
|
: null,
|
|
@@ -7840,6 +8176,7 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
|
|
|
7840
8176
|
: promptState.hasOutbox
|
|
7841
8177
|
? "Queue prompt"
|
|
7842
8178
|
: "Send prompt",
|
|
8179
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
7843
8180
|
onClick: sendPrompt,
|
|
7844
8181
|
}, sendLabel),
|
|
7845
8182
|
),
|
|
@@ -8194,12 +8531,13 @@ function Toolbar({ controller, mobile, canvasPaneOpen = false, onToggleCanvasPan
|
|
|
8194
8531
|
const leftButtons = buttonDefs.filter((d) => !RIGHT_GROUP.has(d.key)).map(withRestore);
|
|
8195
8532
|
const rightButtons = buttonDefs.filter((d) => RIGHT_GROUP.has(d.key)).map(withRestore);
|
|
8196
8533
|
const toolbar = React.createElement("div", { className: "ps-toolbar" },
|
|
8197
|
-
// Three columns:
|
|
8198
|
-
//
|
|
8199
|
-
//
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
React.createElement("div", { className: "ps-toolbar-actions" },
|
|
8534
|
+
// Three columns: left rail | main controls | right rail. Full-screen
|
|
8535
|
+
// canvas uses the otherwise-empty left rail for the normal actions so
|
|
8536
|
+
// they do not crowd the canvas metadata and controls across the top.
|
|
8537
|
+
React.createElement("div", { className: "ps-toolbar-side is-left" },
|
|
8538
|
+
canvasMaximized ? leftButtons.map(renderButton) : null),
|
|
8539
|
+
React.createElement("div", { className: "ps-toolbar-actions" },
|
|
8540
|
+
canvasMaximized ? null : leftButtons.map(renderButton)),
|
|
8203
8541
|
React.createElement("div", { className: "ps-toolbar-side is-right" },
|
|
8204
8542
|
// The portal header parks its version/status meta here, LEFT of
|
|
8205
8543
|
// the tool buttons, and its sign-out glyph in the slot after
|
|
@@ -8749,11 +9087,11 @@ function DiagnosticsSplitResizeHandle({ controller, splitAdjust = 0 }) {
|
|
|
8749
9087
|
const onMove = (event) => {
|
|
8750
9088
|
const st = dragStateRef.current;
|
|
8751
9089
|
if (!st) return;
|
|
8752
|
-
//
|
|
8753
|
-
//
|
|
8754
|
-
//
|
|
8755
|
-
//
|
|
8756
|
-
const next =
|
|
9090
|
+
// The first grid track is `50% + adjust`, while the divider takes
|
|
9091
|
+
// real space between the two panes. Its bounds are asymmetric:
|
|
9092
|
+
// -column/2 makes Inspector 0px; column/2-divider makes Activity
|
|
9093
|
+
// 0px. Using one symmetric half-range left an 8-9px pane remnant.
|
|
9094
|
+
const next = clamp(st.start + (event.clientY - st.startY), st.minAdjust, st.maxAdjust);
|
|
8757
9095
|
controller.dispatch({ type: "ui/diagnosticsSplitAdjust", diagnosticsSplitAdjust: next });
|
|
8758
9096
|
};
|
|
8759
9097
|
const onUp = () => {
|
|
@@ -8781,8 +9119,15 @@ function DiagnosticsSplitResizeHandle({ controller, splitAdjust = 0 }) {
|
|
|
8781
9119
|
event.preventDefault();
|
|
8782
9120
|
capturePointerForDrag(event);
|
|
8783
9121
|
const col = event.currentTarget.closest(".ps-workspace-column");
|
|
8784
|
-
const
|
|
8785
|
-
|
|
9122
|
+
const columnHeight = Math.max(0, col?.getBoundingClientRect?.().height || 0);
|
|
9123
|
+
const dividerHeight = Math.max(0, event.currentTarget.getBoundingClientRect().height || 0);
|
|
9124
|
+
const { minAdjust, maxAdjust } = getDiagnosticsSplitAdjustBounds(columnHeight, dividerHeight);
|
|
9125
|
+
dragStateRef.current = {
|
|
9126
|
+
startY: event.clientY,
|
|
9127
|
+
start: clamp(Number(splitAdjust) || 0, minAdjust, maxAdjust),
|
|
9128
|
+
minAdjust,
|
|
9129
|
+
maxAdjust,
|
|
9130
|
+
};
|
|
8786
9131
|
setDragging(true);
|
|
8787
9132
|
document.body.classList.add("is-resizing-pane-y");
|
|
8788
9133
|
},
|
|
@@ -8790,9 +9135,17 @@ function DiagnosticsSplitResizeHandle({ controller, splitAdjust = 0 }) {
|
|
|
8790
9135
|
onKeyDown: (event) => {
|
|
8791
9136
|
if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
|
|
8792
9137
|
event.preventDefault();
|
|
9138
|
+
const col = event.currentTarget.closest(".ps-workspace-column");
|
|
9139
|
+
const columnHeight = Math.max(0, col?.getBoundingClientRect?.().height || 0);
|
|
9140
|
+
const dividerHeight = Math.max(0, event.currentTarget.getBoundingClientRect().height || 0);
|
|
9141
|
+
const { minAdjust, maxAdjust } = getDiagnosticsSplitAdjustBounds(columnHeight, dividerHeight);
|
|
8793
9142
|
controller.dispatch({
|
|
8794
9143
|
type: "ui/diagnosticsSplitAdjust",
|
|
8795
|
-
diagnosticsSplitAdjust: (
|
|
9144
|
+
diagnosticsSplitAdjust: clamp(
|
|
9145
|
+
(Number(splitAdjust) || 0) + (event.key === "ArrowDown" ? dividerHeight : -dividerHeight),
|
|
9146
|
+
minAdjust,
|
|
9147
|
+
maxAdjust,
|
|
9148
|
+
),
|
|
8796
9149
|
});
|
|
8797
9150
|
},
|
|
8798
9151
|
},
|
|
@@ -9984,7 +10337,14 @@ function AdminSettingsTree({ controller, view }) {
|
|
|
9984
10337
|
`${row.label}`, React.createElement("span", { className: "ps-admin-tree__count" }, String(row.count ?? 0)));
|
|
9985
10338
|
}
|
|
9986
10339
|
const onClick = row.kind === "package"
|
|
9987
|
-
|
|
10340
|
+
// The selector says WHICH copy this row is — one name can be a
|
|
10341
|
+
// shared package and a user copy at once (scope shadowing).
|
|
10342
|
+
? () => controller.selectAdminPackage(row.name, {
|
|
10343
|
+
scope: row.scope === "shared" ? "shared" : "user",
|
|
10344
|
+
...(row.scope !== "shared" && row.owner?.provider && row.owner?.subject
|
|
10345
|
+
? { owner: { provider: row.owner.provider, subject: row.owner.subject } }
|
|
10346
|
+
: {}),
|
|
10347
|
+
})
|
|
9988
10348
|
: () => controller.setAdminSection(row.id === "agents" ? "packages" : row.id === "workers" ? "workers" : "ghcp");
|
|
9989
10349
|
return React.createElement("button", {
|
|
9990
10350
|
key: row.id,
|
|
@@ -10100,8 +10460,29 @@ function AdminPackageDetailPane({ controller, view }) {
|
|
|
10100
10460
|
disabled: Boolean(pending),
|
|
10101
10461
|
title: `Publish a new version of ${detail.name} from a repo folder`,
|
|
10102
10462
|
}, "Update"),
|
|
10103
|
-
|
|
10104
|
-
|
|
10463
|
+
// Promote MOVES this row to shared scope, so it only works
|
|
10464
|
+
// while the shared name is free. Once a shared copy exists,
|
|
10465
|
+
// the update path is republish: this version's exact bytes
|
|
10466
|
+
// become a new version of the existing shared package.
|
|
10467
|
+
detail.scope === "user" && detail.hasSharedCounterpart
|
|
10468
|
+
? React.createElement("button", {
|
|
10469
|
+
type: "button",
|
|
10470
|
+
className: "ps-mini-button",
|
|
10471
|
+
onClick: act("republish", detail.activeSemver),
|
|
10472
|
+
disabled: Boolean(pending) || !detail.activeSemver,
|
|
10473
|
+
title: `Publish ${detail.name}@${detail.activeSemver ?? "?"} into the existing shared package`,
|
|
10474
|
+
}, pending === "republish" ? "Publishing…" : `Publish ${detail.activeSemver ?? ""} to shared`)
|
|
10475
|
+
: React.createElement("button", { type: "button", className: "ps-mini-button", onClick: act(detail.scope === "shared" ? "demote" : "promote"), disabled: Boolean(pending) },
|
|
10476
|
+
detail.scope === "shared" ? "Demote to user" : "Promote to shared"),
|
|
10477
|
+
detail.scope === "shared" && !detail.hasOwnUserCounterpart
|
|
10478
|
+
? React.createElement("button", {
|
|
10479
|
+
type: "button",
|
|
10480
|
+
className: "ps-mini-button",
|
|
10481
|
+
onClick: act("republish", detail.activeSemver),
|
|
10482
|
+
disabled: Boolean(pending) || !detail.activeSemver,
|
|
10483
|
+
title: "Copy the active version into your user scope as a private test bench",
|
|
10484
|
+
}, pending === "republish" ? "Copying…" : "Copy to my user scope")
|
|
10485
|
+
: null,
|
|
10105
10486
|
React.createElement("button", { type: "button", className: "ps-mini-button", onClick: act(detail.enabled ? "disable" : "enable"), disabled: Boolean(pending) },
|
|
10106
10487
|
detail.enabled ? "Disable" : "Enable"),
|
|
10107
10488
|
React.createElement("button", { type: "button", className: "ps-mini-button is-danger", onClick: confirmDelete, disabled: Boolean(pending) }, "Delete"))
|
|
@@ -10593,6 +10974,7 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
10593
10974
|
diagnosticsPaneAdjust: rootState.ui.layout?.diagnosticsPaneAdjust ?? 0,
|
|
10594
10975
|
diagnosticsSplitAdjust: rootState.ui.layout?.diagnosticsSplitAdjust ?? 0,
|
|
10595
10976
|
focusRegion: rootState.ui.focusRegion,
|
|
10977
|
+
requestedFocusRegion: rootState.ui.requestedFocusRegion,
|
|
10596
10978
|
inspectorTab: rootState.ui.inspectorTab,
|
|
10597
10979
|
filesFullscreen: Boolean(rootState.files.fullscreen),
|
|
10598
10980
|
selectedArtifactId: rootState.files.selectedArtifactId || null,
|
|
@@ -10881,10 +11263,20 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
10881
11263
|
// toolbar: the sessions-only layout's list reclaims focus, which
|
|
10882
11264
|
// bounced the user out of Inspector the instant they tapped it.
|
|
10883
11265
|
// Returning to Main is the Main button's job now.
|
|
10884
|
-
|
|
11266
|
+
//
|
|
11267
|
+
// Gate on the RAW request, not the normalized region: on a phone a
|
|
11268
|
+
// workspace region (sessions/chat) normalizes to inspector, so the
|
|
11269
|
+
// Main button's own setFocus("chat") would otherwise land here as an
|
|
11270
|
+
// "inspector" focus and yank the user into diagnostics the instant
|
|
11271
|
+
// they asked for the workspace. Only a focus the caller genuinely
|
|
11272
|
+
// aimed at inspector/activity switches the pane.
|
|
11273
|
+
const requested = state.requestedFocusRegion;
|
|
11274
|
+
if (mobile
|
|
11275
|
+
&& (state.focusRegion === "inspector" || state.focusRegion === "activity")
|
|
11276
|
+
&& (requested === "inspector" || requested === "activity")) {
|
|
10885
11277
|
setMobilePane(state.focusRegion);
|
|
10886
11278
|
}
|
|
10887
|
-
}, [mobile, state.focusRegion]);
|
|
11279
|
+
}, [mobile, state.focusRegion, state.requestedFocusRegion]);
|
|
10888
11280
|
|
|
10889
11281
|
React.useEffect(() => {
|
|
10890
11282
|
const visibleTabs = getVisibleInspectorTabs(controller);
|