pilotswarm 0.5.37 → 0.5.39
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 +3 -3
- package/tui/src/node-sdk-transport.js +113 -18
- package/ui/core/src/controller.js +196 -49
- package/ui/core/src/layout.js +10 -0
- package/ui/core/src/reducer.js +265 -20
- package/ui/core/src/selectors.js +132 -10
- package/ui/core/src/state.js +90 -2
- package/ui/core/src/themes/index.js +1 -1
- package/ui/react/src/web-app.js +1097 -220
- package/web/auth/config.js +3 -0
- package/web/auth/index.js +8 -1
- package/web/auth/normalize/proxy.js +48 -0
- package/web/auth/providers/proxy.js +141 -0
- package/web/dist/assets/{abnfDiagram-VRR7QNED-DlvPH-8O.js → abnfDiagram-VRR7QNED-Hsly1fgf.js} +1 -1
- package/web/dist/assets/{arc-r1liz5e5.js → arc-DiQLL9vM.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-Bci5dNH3.js → architectureDiagram-ZJ3FMSHR-B8ry8WNZ.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-Bug32AXp.js → blockDiagram-677ZJIJ3-Z4PcukIo.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-DwxSFqgE.js → c4Diagram-LMCZKHZV-Cahkc0gc.js} +1 -1
- package/web/dist/assets/channel-BHLBPE50.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-B_dbI_xg.js → chunk-2Q5K7J3B-CtZuWH4J.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-BkF68W-R.js → chunk-32BRIVSS-C0M390-k.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-BSbNIbJu.js → chunk-5VM5RSS4-ClvX9u5j.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-Blv_Iqa2.js → chunk-EX3LRPZG-BVAet5KG.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-Dk_dBFKd.js → chunk-JWPE2WC7-BccmZmrr.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-DqNFBrfw.js → chunk-MOJQB5TN-BN5MKe94.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-Dlv7Svpb.js → chunk-RYQCIY6F-DfWw-ACP.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-DozGikwu.js → chunk-V7JOEXUC-BgUEoLnN.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-CmkqvGT4.js → chunk-VR4S4FIN-k1eKfFNA.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-DyByai5N.js → chunk-XXDRQBXY-DPyV10yR.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-rj9D2TSL.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-rj9D2TSL.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-Bv-gRVvA.js → cose-bilkent-JH36ORCC-Bt_rpukK.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-DRnAsyZU.js → cynefin-VYW2F7L2-Z8eMzFMv.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-B1fnUtjo.js → cynefinDiagram-TSTJHNR4-BJSNawFi.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-DM6-QENB.js → dagre-VKFMJZFB-D6dNl8jc.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-BPxxnLdU.js → diagram-FQU43EPY-Cy1nbGsU.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-BgtJT3qF.js → diagram-G47NLZAW-CmrbuA_d.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-uekeIzsy.js → diagram-NH7WQ7WH-PoLNQE2j.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-BVy3JZWE.js → diagram-OA4YK3LP-BZwNFcSV.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-BWdjUr32.js → diagram-WEI45ONY-pKw0b_fN.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-DD4fwhNK.js → ebnfDiagram-CCIWWBDH-UNctuQCV.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-BllD_6Kh.js → erDiagram-Q63AITRT-DxIETMif.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-QEYKkAUN.js → flowDiagram-23GEKE2U-C06IdNKV.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-uZcfKTSW.js → ganttDiagram-NO4QXBWP-fU7aGyP1.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-XPdbPaH-.js → gitGraphDiagram-IHSO6WYX-9UKkPJm6.js} +1 -1
- package/web/dist/assets/index-B8nGlHwb.js +24 -0
- package/web/dist/assets/index-DtL0lbis.css +1 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-DXCnQOJs.js → infoDiagram-FWYZ7A6U-C6F06SQJ.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-687_4FBy.js → ishikawaDiagram-FXEZZL3T-CB-O9NfO.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-GXlKuda8.js → journeyDiagram-5HDEW3XC-B-8MOf-H.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-C38-2DvN.js → kanban-definition-HUTT4EX6-DX0XA3hI.js} +1 -1
- package/web/dist/assets/{linear-lGPXiBcz.js → linear-D60PrjTW.js} +1 -1
- package/web/dist/assets/{mermaid.core-C_neEtab.js → mermaid.core-bonzwapf.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-BaJ9tpEG.js → mindmap-definition-LN4V7U3C-Y-6Dum33.js} +1 -1
- package/web/dist/assets/{papaparse.min-rDwKYbBf.js → papaparse.min-Cas1JfRG.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-BdXJLYgY.js → pegDiagram-2B236MQR-_paO0yDw.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-DU19t6fM.js → pieDiagram-ENE6RG2P-xT1_Gpn4.js} +1 -1
- package/web/dist/assets/pilotswarm-BT6tOknQ.js +96 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-CBkOdPp0.js → quadrantDiagram-ABIIQ3AL-1usdLFOF.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-D2DMcGDw.js → railroadDiagram-RFXS5EU6-R3lwHOz9.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-Bx9tF1pr.js → requirementDiagram-TGXJPOKE-BL8J1__Z.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-DcLlRfsi.js → sankeyDiagram-HTMAVEWB-Dz6--0pl.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-xr6UVRBO.js → sequenceDiagram-DBY2YBRQ-CG2Xpkct.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-0fUkSXmg.js → sizeCapture-X5ZJPWSS-BPbYfjxj.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-BWgJGGpg.js → stateDiagram-2N3HPSRC-46x_y_aA.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-C2tnolSo.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-CZKya4_i.js → swimlanes-5IMT3BWC-SXdMo4zJ.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-Dh6Sn__t.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-Df8BpeAu.js → timeline-definition-FHXFAJF6-CGgT-b38.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-ChX5bjT6.js → vennDiagram-L72KCM5P-BGS3xBa_.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-Dr48BgaJ.js → wardleyDiagram-EHGQE667-m2U3wEZJ.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-Cia_MFfc.js → xychartDiagram-FW5EYKEG-DI6tQysg.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/runtime.js +30 -7
- package/web/dist/assets/channel-jVpSYat3.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-C78zSajE.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-C78zSajE.js +0 -1
- package/web/dist/assets/index-BuDSoRIL.css +0 -1
- package/web/dist/assets/index-D1itTxvB.js +0 -24
- package/web/dist/assets/pilotswarm-BNKGG936.js +0 -96
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DmAC8DdB.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-CVYx20_X.js +0 -8
package/ui/react/src/web-app.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
formatCompactNumber,
|
|
18
18
|
formatCronTimestampForClient,
|
|
19
19
|
formatHumanDurationSeconds,
|
|
20
|
+
getDiagnosticsSplitAdjustBounds,
|
|
20
21
|
getPromptInputRows,
|
|
21
22
|
getTheme,
|
|
22
23
|
isThemeLight,
|
|
@@ -252,6 +253,16 @@ function normalizeProfileSettings(settings) {
|
|
|
252
253
|
if (candidate.rightPaneMode === "canvas" || candidate.rightPaneMode === "panes") {
|
|
253
254
|
normalized.rightPaneMode = candidate.rightPaneMode;
|
|
254
255
|
}
|
|
256
|
+
// The two independent desktop columns. This normalizer WHITELISTS keys, so
|
|
257
|
+
// an unlisted one is dropped on the way to the server without a word —
|
|
258
|
+
// which is exactly how a toggle looks like it never persisted.
|
|
259
|
+
if (hasOwn(candidate, "desktopPanes") && candidate.desktopPanes && typeof candidate.desktopPanes === "object") {
|
|
260
|
+
normalized.desktopPanes = {
|
|
261
|
+
canvasOpen: candidate.desktopPanes.canvasOpen === true,
|
|
262
|
+
diagnosticsOpen: candidate.desktopPanes.diagnosticsOpen === true,
|
|
263
|
+
zen: candidate.desktopPanes.zen === true,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
255
266
|
if (hasOwn(candidate, "canvasPrefs")) {
|
|
256
267
|
normalized.canvasPrefs = normalizeStoredCanvasPrefs(candidate.canvasPrefs);
|
|
257
268
|
}
|
|
@@ -319,7 +330,7 @@ function hasOwn(value, key) {
|
|
|
319
330
|
return Boolean(value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key));
|
|
320
331
|
}
|
|
321
332
|
|
|
322
|
-
function profileSettingsFromViewState(state, preservedOtherTouchScale = null, preservedRightPaneMode = null) {
|
|
333
|
+
function profileSettingsFromViewState(state, preservedOtherTouchScale = null, preservedRightPaneMode = null, preservedDesktopPanes = null) {
|
|
323
334
|
return normalizeProfileSettings({
|
|
324
335
|
themeId: state.themeId,
|
|
325
336
|
[touchScaleKey()]: state.touchScale,
|
|
@@ -329,6 +340,9 @@ function profileSettingsFromViewState(state, preservedOtherTouchScale = null, pr
|
|
|
329
340
|
sessionPaneAdjust: state.sessionPaneAdjust,
|
|
330
341
|
portalSessionColumnAdjust: state.portalSessionColumnAdjust,
|
|
331
342
|
activityPaneAdjust: state.activityPaneAdjust,
|
|
343
|
+
canvasPaneAdjust: state.canvasPaneAdjust,
|
|
344
|
+
diagnosticsPaneAdjust: state.diagnosticsPaneAdjust,
|
|
345
|
+
diagnosticsSplitAdjust: state.diagnosticsSplitAdjust,
|
|
332
346
|
},
|
|
333
347
|
pinnedSessionIds: state.pinnedIds,
|
|
334
348
|
sessionOrder: state.manualOrder,
|
|
@@ -339,11 +353,23 @@ function profileSettingsFromViewState(state, preservedOtherTouchScale = null, pr
|
|
|
339
353
|
// write its own value — it preserves the last-known stored one, or a
|
|
340
354
|
// desktop that flipped to canvas would be flipped back by the phone's
|
|
341
355
|
// next save.
|
|
356
|
+
//
|
|
357
|
+
// desktopPanes replaces it and is preserved the same way, for the same
|
|
358
|
+
// reason. rightPaneMode is still written so a rolled-back build reads
|
|
359
|
+
// something sane.
|
|
342
360
|
...(isNarrowViewport()
|
|
343
|
-
?
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
361
|
+
? {
|
|
362
|
+
...(preservedRightPaneMode === "canvas" || preservedRightPaneMode === "panes"
|
|
363
|
+
? { rightPaneMode: preservedRightPaneMode }
|
|
364
|
+
: {}),
|
|
365
|
+
...(preservedDesktopPanes && typeof preservedDesktopPanes === "object"
|
|
366
|
+
? { desktopPanes: preservedDesktopPanes }
|
|
367
|
+
: {}),
|
|
368
|
+
}
|
|
369
|
+
: {
|
|
370
|
+
rightPaneMode: state.canvasOpen ? "canvas" : "panes",
|
|
371
|
+
desktopPanes: { canvasOpen: state.canvasOpen === true, diagnosticsOpen: state.diagnosticsOpen === true, zen: state.canvasZen === true },
|
|
372
|
+
}),
|
|
347
373
|
canvasPrefs: state.canvasPrefs,
|
|
348
374
|
// setCurrentUserProfileSettings REPLACES the settings object, so the
|
|
349
375
|
// other device class's slot has to be written back verbatim or saving
|
|
@@ -374,7 +400,14 @@ function buildDefaultProfileSettingsFromState(state, preservedOtherTouchScale =
|
|
|
374
400
|
? (preservedRightPaneMode === "canvas" || preservedRightPaneMode === "panes"
|
|
375
401
|
? { rightPaneMode: preservedRightPaneMode }
|
|
376
402
|
: {})
|
|
377
|
-
: {
|
|
403
|
+
: {
|
|
404
|
+
rightPaneMode: state?.ui?.rightPaneMode,
|
|
405
|
+
desktopPanes: {
|
|
406
|
+
canvasOpen: state?.ui?.canvasOpen === true,
|
|
407
|
+
diagnosticsOpen: state?.ui?.diagnosticsOpen === true,
|
|
408
|
+
zen: state?.ui?.canvasZen === true,
|
|
409
|
+
},
|
|
410
|
+
}),
|
|
378
411
|
canvasPrefs: state?.canvas?.prefs,
|
|
379
412
|
...(typeof preservedOtherTouchScale === "boolean"
|
|
380
413
|
? { [otherTouchScaleKey()]: preservedOtherTouchScale }
|
|
@@ -423,6 +456,9 @@ function materializeProfileSettings(remoteSettings, defaults) {
|
|
|
423
456
|
// clobber a local flip/badge advance the server has not persisted yet.
|
|
424
457
|
// An omitted key leaves the profileSettings/apply guards preserving
|
|
425
458
|
// the current value.
|
|
459
|
+
...(hasOwn(normalizedRemote, "desktopPanes")
|
|
460
|
+
? { desktopPanes: normalizedRemote.desktopPanes }
|
|
461
|
+
: {}),
|
|
426
462
|
...(hasOwn(normalizedRemote, "rightPaneMode")
|
|
427
463
|
? { rightPaneMode: normalizedRemote.rightPaneMode }
|
|
428
464
|
: {}),
|
|
@@ -475,6 +511,65 @@ function clampNumber(value, min, max) {
|
|
|
475
511
|
return Math.max(min, Math.min(max, value));
|
|
476
512
|
}
|
|
477
513
|
|
|
514
|
+
/**
|
|
515
|
+
* Below this, releasing a drag CLOSES the column rather than leaving a sliver.
|
|
516
|
+
*
|
|
517
|
+
* "Resized to 0 toggles it off" is the rule, but a pointer rarely lands on
|
|
518
|
+
* exactly 0 — without a threshold you get a 3px column you cannot read, cannot
|
|
519
|
+
* grab, and did not ask for. 120px is about the narrowest either column is
|
|
520
|
+
* still useful at.
|
|
521
|
+
*/
|
|
522
|
+
const PORTAL_COLUMN_SNAP_CLOSED_PX = 120;
|
|
523
|
+
|
|
524
|
+
/** Width of the draggable track between the canvas and diagnostics columns. */
|
|
525
|
+
const PORTAL_COLUMN_RESIZER_PX = 16;
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* The narrowest the canvas should OPEN at. Not a clamp — drag it smaller if
|
|
529
|
+
* you like, or drag it away entirely.
|
|
530
|
+
*
|
|
531
|
+
* A canvas is a rendered page being looked at, so it needs enough width for a
|
|
532
|
+
* normal document column. The inherited fr split left it near 200px.
|
|
533
|
+
*/
|
|
534
|
+
const PORTAL_CANVAS_MIN_PX = 480;
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Wide enough to OPEN the canvas by default on a first visit.
|
|
538
|
+
*
|
|
539
|
+
* Sessions and chat need roughly 224px + 500px to stay comfortable, and the
|
|
540
|
+
* canvas wants PORTAL_CANVAS_MIN_PX beside them. 1440 clears that with room to
|
|
541
|
+
* spare and is a real device width (a 14" MacBook, a 900p desktop), so the
|
|
542
|
+
* common laptop gets a canvas and a small window does not get three cramped
|
|
543
|
+
* columns.
|
|
544
|
+
*/
|
|
545
|
+
const PORTAL_CANVAS_DEFAULT_MIN_VIEWPORT_PX = 1440;
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Whether this profile has ever expressed a preference about the two columns.
|
|
549
|
+
*
|
|
550
|
+
* The width-based default must fire only in its absence: someone who CLOSED
|
|
551
|
+
* the canvas has stored `{canvasOpen: false}`, which is a choice and reads
|
|
552
|
+
* nothing like "unset". A legacy rightPaneMode counts too — it is the same
|
|
553
|
+
* preference in the older shape.
|
|
554
|
+
*/
|
|
555
|
+
function hasStoredDesktopPanes(settings) {
|
|
556
|
+
if (!settings || typeof settings !== "object") return false;
|
|
557
|
+
if (settings.desktopPanes && typeof settings.desktopPanes === "object") return true;
|
|
558
|
+
return settings.rightPaneMode === "canvas" || settings.rightPaneMode === "panes";
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* The widths the two optional columns OPEN at, in pixels. 2:1 — the canvas is
|
|
563
|
+
* the thing being looked at; diagnostics is a readout beside it.
|
|
564
|
+
*
|
|
565
|
+
* Pixels, not shares of the window, because of the rule the fr model broke:
|
|
566
|
+
* when a column closes, the freed space goes to chat + sessions — never to
|
|
567
|
+
* the surviving column. A pane owns its width; chat flexes.
|
|
568
|
+
*/
|
|
569
|
+
const PORTAL_CANVAS_COL_DEFAULT_PX = 640;
|
|
570
|
+
const PORTAL_DIAG_COL_DEFAULT_PX = 320;
|
|
571
|
+
|
|
572
|
+
|
|
478
573
|
function portalSessionColumnBounds(width) {
|
|
479
574
|
const safeWidth = Math.max(0, Number(width) || 0);
|
|
480
575
|
const availableWidth = Math.max(
|
|
@@ -3831,6 +3926,7 @@ function SessionDetailBox({ session, childCount = 0 }) {
|
|
|
3831
3926
|
// used, so the box height still cannot move with the selection.
|
|
3832
3927
|
field("Title", session?.title, "is-title"),
|
|
3833
3928
|
field("ID", session?.sessionId, "is-id"),
|
|
3929
|
+
field("Owner", session?.owner ? formatAdminPrincipalLabel(session.owner) : null),
|
|
3834
3930
|
field("Model", model),
|
|
3835
3931
|
field("Context", context, percent != null && percent >= 85 ? "is-hot" : percent != null && percent >= 70 ? "is-warm" : ""),
|
|
3836
3932
|
field("Cron", cron, session?.cronActive === true ? "is-armed" : ""),
|
|
@@ -4056,6 +4152,7 @@ function SessionRowContent({ row, theme, structured = false, showInlineDetail =
|
|
|
4056
4152
|
return React.createElement(React.Fragment, null,
|
|
4057
4153
|
plain.statusColor ? React.createElement(StatusDot, { color: plain.statusColor, node: row.depth > 0 }) : null,
|
|
4058
4154
|
React.createElement(OwnerAvatar, { badge: row.ownerBadge }),
|
|
4155
|
+
React.createElement(SessionCanvasMark, { mark: row.canvasMark }),
|
|
4059
4156
|
React.createElement("span", { className: "ps-session-row-title__text" },
|
|
4060
4157
|
React.createElement(Runs, { runs: plain.rest, theme })));
|
|
4061
4158
|
}
|
|
@@ -4078,6 +4175,7 @@ function SessionRowContent({ row, theme, structured = false, showInlineDetail =
|
|
|
4078
4175
|
React.createElement("div", { className: "ps-session-row-title" },
|
|
4079
4176
|
title.statusColor ? React.createElement(StatusDot, { color: title.statusColor, node: row.depth > 0 }) : null,
|
|
4080
4177
|
React.createElement(OwnerAvatar, { badge: row.ownerBadge }),
|
|
4178
|
+
React.createElement(SessionCanvasMark, { mark: row.canvasMark }),
|
|
4081
4179
|
// Runs wrapped in ONE element: the title is a flex row so the
|
|
4082
4180
|
// avatar centres against the text, and the runs keep normal
|
|
4083
4181
|
// inline flow inside the wrapper. Flexing the runs directly
|
|
@@ -4233,6 +4331,12 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
4233
4331
|
// so the reconstruction below must carry it or the click does nothing
|
|
4234
4332
|
// visible (the same omission that blinded the Node Map).
|
|
4235
4333
|
listDeselected: Boolean(state.sessions.listDeselected),
|
|
4334
|
+
// The canvas markers read these. This synthetic state is exactly the
|
|
4335
|
+
// trap the branding comment below describes: omit a slice here and
|
|
4336
|
+
// every row computes as if it were empty — the markers were null on
|
|
4337
|
+
// every row until these two lines existed.
|
|
4338
|
+
canvasBySessionId: state.canvas?.bySessionId,
|
|
4339
|
+
canvasPrefs: state.canvas?.prefs,
|
|
4236
4340
|
}), shallowEqualObject);
|
|
4237
4341
|
const computedRows = React.useMemo(() => selectSessionRows({
|
|
4238
4342
|
sessions: {
|
|
@@ -4256,7 +4360,11 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
4256
4360
|
// deployment, while the controller's own calls (which pass real state)
|
|
4257
4361
|
// produced the branded name.
|
|
4258
4362
|
branding: viewState.branding,
|
|
4259
|
-
|
|
4363
|
+
canvas: {
|
|
4364
|
+
bySessionId: viewState.canvasBySessionId,
|
|
4365
|
+
prefs: viewState.canvasPrefs,
|
|
4366
|
+
},
|
|
4367
|
+
}), [viewState.activeSessionId, viewState.auth, viewState.branding, viewState.canvasBySessionId, viewState.canvasPrefs, viewState.connectionMode, viewState.filterQuery, viewState.listDeselected, viewState.ownerFilter, viewState.pinnedIds, viewState.manualOrder, viewState.selectedIds, viewState.selectMode, viewState.sessionsById, viewState.sessionsFlat]);
|
|
4260
4368
|
// Hold the previous rows when a poll produced identical output.
|
|
4261
4369
|
const rows = useStableValue(computedRows);
|
|
4262
4370
|
const activeSession = viewState.activeSessionId
|
|
@@ -5941,6 +6049,49 @@ function ArtifactTakeoverPane({ controller, onClose }) {
|
|
|
5941
6049
|
})));
|
|
5942
6050
|
}
|
|
5943
6051
|
|
|
6052
|
+
/**
|
|
6053
|
+
* The phone's canvas: a LAYER over the content region, not a full screen.
|
|
6054
|
+
*
|
|
6055
|
+
* It used to be a fixed, full-viewport overlay — the portal header and the
|
|
6056
|
+
* toolbar disappeared behind it too, so looking at a drawing meant leaving the
|
|
6057
|
+
* app and finding your way back. It now fills exactly the region the Main,
|
|
6058
|
+
* Activity and Inspector panes share: top edge under the toolbar, bottom edge
|
|
6059
|
+
* at the viewport, edge to edge, no inset. The header and the toolbar (with
|
|
6060
|
+
* its Canvas button, the only way in or out — there is no ✕ here) stay put.
|
|
6061
|
+
*
|
|
6062
|
+
* It is rendered as a SIBLING of that region's content, not inside any pane,
|
|
6063
|
+
* because the frame has to outlive every pane: the canvas runs interactive
|
|
6064
|
+
* pages, and switching to Inspector and back must not reset a running game.
|
|
6065
|
+
*
|
|
6066
|
+
* Maximized, it goes truly full-screen: the same element flips from absolute
|
|
6067
|
+
* inside the region to fixed over the whole viewport, covering the portal
|
|
6068
|
+
* header and the toolbar too. That is a CLASS change on one div and nothing
|
|
6069
|
+
* else — the frame is never moved in the DOM, so a game survives the toggle.
|
|
6070
|
+
* The thin strip persists in both states; only the right-hand button flips.
|
|
6071
|
+
*/
|
|
6072
|
+
function MobileCanvasLayer({ controller, visible = true }) {
|
|
6073
|
+
const [maximized, setMaximized] = React.useState(false);
|
|
6074
|
+
const toggleMaximized = React.useCallback(() => setMaximized((current) => !current), []);
|
|
6075
|
+
// Leaving the canvas drops full-screen, so coming back lands in the inset
|
|
6076
|
+
// presentation rather than a takeover the user has to undo before they can
|
|
6077
|
+
// reach the toolbar again.
|
|
6078
|
+
React.useEffect(() => {
|
|
6079
|
+
if (!visible) setMaximized(false);
|
|
6080
|
+
}, [visible]);
|
|
6081
|
+
// Kept MOUNTED while hidden: unmounting destroys the sandboxed document,
|
|
6082
|
+
// and with it everything the user typed and scrolled. The hidden state is
|
|
6083
|
+
// an off-screen transform (see .ps-mobile-canvas-layer.is-hidden), so the
|
|
6084
|
+
// frame keeps a real, unchanged box and no settle-reload fires on re-show.
|
|
6085
|
+
return React.createElement("div", {
|
|
6086
|
+
className: `ps-mobile-canvas-layer${maximized ? " is-maximized" : ""}${visible ? "" : " is-hidden"}`,
|
|
6087
|
+
...(visible ? {} : { inert: true, "aria-hidden": "true" }),
|
|
6088
|
+
},
|
|
6089
|
+
React.createElement(CanvasPane, {
|
|
6090
|
+
controller, mobile: true, visible, focusOnPromote: true,
|
|
6091
|
+
maximized, onToggleMaximized: toggleMaximized,
|
|
6092
|
+
}));
|
|
6093
|
+
}
|
|
6094
|
+
|
|
5944
6095
|
/**
|
|
5945
6096
|
* Mobile artifact viewer — a genuine full-viewport overlay, not a pane.
|
|
5946
6097
|
*
|
|
@@ -5949,19 +6100,6 @@ function ArtifactTakeoverPane({ controller, onClose }) {
|
|
|
5949
6100
|
* phone should feel like pushing a detail screen, the way a native app does,
|
|
5950
6101
|
* not like shrinking content into one more box. Desktop never mounts this.
|
|
5951
6102
|
*/
|
|
5952
|
-
/**
|
|
5953
|
-
* The phone's canvas: FULL-SCREEN, opened by the same toolbar toggle the
|
|
5954
|
-
* desktop has, closed by the pane's ✕ back to whatever was on screen. The
|
|
5955
|
-
* former bottom-nav tab is gone — one affordance on every device class.
|
|
5956
|
-
*/
|
|
5957
|
-
function MobileCanvasOverlay({ controller, onClose, visible = true }) {
|
|
5958
|
-
// Kept MOUNTED while hidden: unmounting destroys the sandboxed document,
|
|
5959
|
-
// and with it everything the user typed and scrolled. visibility (not
|
|
5960
|
-
// display) keeps layout real so no resize-reload fires on re-show.
|
|
5961
|
-
return React.createElement("div", { className: `ps-artifact-overlay ps-canvas-overlay${visible ? "" : " is-hidden"}`, ...(visible ? {} : { inert: true, "aria-hidden": "true" }) },
|
|
5962
|
-
React.createElement(CanvasPane, { controller, mobile: true, onClose, visible, focusOnPromote: true }));
|
|
5963
|
-
}
|
|
5964
|
-
|
|
5965
6103
|
function MobileArtifactOverlay({ controller }) {
|
|
5966
6104
|
const view = useControllerSelector(controller, (state) => {
|
|
5967
6105
|
const id = state.files.selectedArtifactId || "";
|
|
@@ -6066,6 +6204,114 @@ function CanvasGlyph() {
|
|
|
6066
6204
|
React.createElement("path", { d: "M7 17.5h6M10 14.5v3", ...stroke }));
|
|
6067
6205
|
}
|
|
6068
6206
|
|
|
6207
|
+
/**
|
|
6208
|
+
* Diagnostics: a bug, for "debug".
|
|
6209
|
+
*
|
|
6210
|
+
* Not a cog — the admin console owns that — and not sliders, which the filter
|
|
6211
|
+
* funnel is already close enough to.
|
|
6212
|
+
*/
|
|
6213
|
+
function DiagnosticsGlyph() {
|
|
6214
|
+
// Bigger than its neighbours on purpose, and drawn heavier. A bug is a
|
|
6215
|
+
// busier shape than a funnel or a cog: at 16px with hairline legs it
|
|
6216
|
+
// collapsed into a smudge, so it takes 20px and a thicker stroke to read
|
|
6217
|
+
// as the same visual weight.
|
|
6218
|
+
const stroke = { fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round" };
|
|
6219
|
+
return React.createElement("svg", { viewBox: "0 0 20 20", width: 20, height: 20, "aria-hidden": "true" },
|
|
6220
|
+
// Shell, with the seam that says beetle rather than pill.
|
|
6221
|
+
React.createElement("path", { d: "M6.2 8.6a3.8 3.8 0 0 1 7.6 0v2.9a3.8 3.8 0 0 1-7.6 0z", ...stroke }),
|
|
6222
|
+
React.createElement("path", { d: "M6.2 10.3h7.6", ...stroke }),
|
|
6223
|
+
// Head and antennae.
|
|
6224
|
+
React.createElement("path", { d: "M7.6 6.5a2.4 2.4 0 0 1 4.8 0", ...stroke }),
|
|
6225
|
+
React.createElement("path", { d: "M8 4.4 6.9 2.9M12 4.4l1.1-1.5", ...stroke }),
|
|
6226
|
+
// Legs: three a side, the outer two angled so the silhouette reads.
|
|
6227
|
+
React.createElement("path", { d: "M6.3 8.6 3.8 7.4M6.2 10.6H3.6M6.3 12.6 3.8 13.9", ...stroke }),
|
|
6228
|
+
React.createElement("path", { d: "M13.7 8.6 16.2 7.4M13.8 10.6h2.6M13.7 12.6l2.5 1.3", ...stroke }));
|
|
6229
|
+
}
|
|
6230
|
+
|
|
6231
|
+
/**
|
|
6232
|
+
* The canvas marker on a session row.
|
|
6233
|
+
*
|
|
6234
|
+
* Two states, and they must be tellable apart at a glance in a dense list:
|
|
6235
|
+
* "canvas" outline only — a canvas exists here
|
|
6236
|
+
* "unseen" outline plus a filled dot — it changed since you last looked
|
|
6237
|
+
*
|
|
6238
|
+
* Shape carries the meaning, not colour alone: the dot is visible in either
|
|
6239
|
+
* theme and to a reader who cannot separate the two hues.
|
|
6240
|
+
*/
|
|
6241
|
+
function SessionCanvasMark({ mark }) {
|
|
6242
|
+
if (mark !== "canvas" && mark !== "unseen") return null;
|
|
6243
|
+
const unseen = mark === "unseen";
|
|
6244
|
+
const stroke = { fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" };
|
|
6245
|
+
return React.createElement("span", {
|
|
6246
|
+
className: `ps-session-canvas-mark${unseen ? " is-unseen" : ""}`,
|
|
6247
|
+
title: unseen ? "Canvas updated since you last viewed it" : "This session has a canvas",
|
|
6248
|
+
"aria-label": unseen ? "Canvas updated" : "Has a canvas",
|
|
6249
|
+
role: "img",
|
|
6250
|
+
},
|
|
6251
|
+
React.createElement("svg", { viewBox: "0 0 16 16", width: 12, height: 12, "aria-hidden": "true" },
|
|
6252
|
+
React.createElement("rect", { x: 1.6, y: 2.6, width: 12.8, height: 9.2, rx: 1.4, ...stroke }),
|
|
6253
|
+
React.createElement("path", { d: "M6 14.4h4", ...stroke }),
|
|
6254
|
+
unseen
|
|
6255
|
+
? React.createElement("circle", { cx: 12.4, cy: 4.6, r: 2.4, fill: "currentColor", stroke: "none" })
|
|
6256
|
+
: null));
|
|
6257
|
+
}
|
|
6258
|
+
|
|
6259
|
+
/**
|
|
6260
|
+
* The canvas slot controls: a dropdown when the session has more than one
|
|
6261
|
+
* drawn canvas, plus prev/next arrows. Hidden entirely with one canvas —
|
|
6262
|
+
* the common case stays exactly as it always looked. Slot state is
|
|
6263
|
+
* ui.canvasSlot (in-memory; the selector clamps to a drawn slot).
|
|
6264
|
+
*/
|
|
6265
|
+
function CanvasSlotControls({ controller, view, compact = false }) {
|
|
6266
|
+
const slots = Array.isArray(view.slots) ? view.slots : [];
|
|
6267
|
+
if (slots.length <= 1) return null;
|
|
6268
|
+
const order = slots.map((s2) => s2.slot);
|
|
6269
|
+
const at = Math.max(0, order.indexOf(view.slot));
|
|
6270
|
+
const go = (delta) => {
|
|
6271
|
+
const next = order[(at + delta + order.length) % order.length];
|
|
6272
|
+
controller.dispatch({ type: "ui/canvasSlot", slot: next });
|
|
6273
|
+
};
|
|
6274
|
+
const labelFor = (s2) => (s2.name ? s2.name : `canvas ${s2.slot}`) + (s2.unseen ? " •" : "");
|
|
6275
|
+
return React.createElement("span", { className: `ps-canvas-slot-controls${compact ? " is-compact" : ""}` },
|
|
6276
|
+
React.createElement("button", {
|
|
6277
|
+
type: "button", className: "ps-artifact-pane-btn",
|
|
6278
|
+
onClick: () => go(-1), title: "Previous canvas", "aria-label": "Previous canvas",
|
|
6279
|
+
}, "\u2039"),
|
|
6280
|
+
React.createElement("select", {
|
|
6281
|
+
className: "ps-canvas-slot-select",
|
|
6282
|
+
value: view.slot,
|
|
6283
|
+
onChange: (e) => controller.dispatch({ type: "ui/canvasSlot", slot: Number(e.target.value) }),
|
|
6284
|
+
"aria-label": "Choose canvas",
|
|
6285
|
+
title: "Choose canvas",
|
|
6286
|
+
}, slots.map((s2) => React.createElement("option", { key: s2.slot, value: s2.slot }, labelFor(s2)))),
|
|
6287
|
+
React.createElement("button", {
|
|
6288
|
+
type: "button", className: "ps-artifact-pane-btn",
|
|
6289
|
+
onClick: () => go(1), title: "Next canvas", "aria-label": "Next canvas",
|
|
6290
|
+
}, "\u203a"));
|
|
6291
|
+
}
|
|
6292
|
+
|
|
6293
|
+
/** Zen: a narrow chat rail beside a wide canvas. */
|
|
6294
|
+
function ZenGlyph() {
|
|
6295
|
+
const stroke = { fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round" };
|
|
6296
|
+
return React.createElement("svg", { viewBox: "0 0 20 20", width: 15, height: 15, "aria-hidden": "true" },
|
|
6297
|
+
React.createElement("rect", { x: 2.2, y: 3.4, width: 3.6, height: 13.2, rx: 1, ...stroke }),
|
|
6298
|
+
React.createElement("rect", { x: 8.2, y: 3.4, width: 9.6, height: 13.2, rx: 1, ...stroke }));
|
|
6299
|
+
}
|
|
6300
|
+
|
|
6301
|
+
/** Corners pushing OUT — take the whole screen. */
|
|
6302
|
+
function MaximizeGlyph() {
|
|
6303
|
+
const stroke = { fill: "none", stroke: "currentColor", strokeWidth: 1.9, strokeLinecap: "round", strokeLinejoin: "round" };
|
|
6304
|
+
return React.createElement("svg", { viewBox: "0 0 20 20", width: 13, height: 13, "aria-hidden": "true" },
|
|
6305
|
+
React.createElement("path", { d: "M8 3H3v5M12 3h5v5M12 17h5v-5M8 17H3v-5", ...stroke }));
|
|
6306
|
+
}
|
|
6307
|
+
|
|
6308
|
+
/** Corners pulling IN — back to the canvas inset in the content region. */
|
|
6309
|
+
function RestoreGlyph() {
|
|
6310
|
+
const stroke = { fill: "none", stroke: "currentColor", strokeWidth: 1.9, strokeLinecap: "round", strokeLinejoin: "round" };
|
|
6311
|
+
return React.createElement("svg", { viewBox: "0 0 20 20", width: 13, height: 13, "aria-hidden": "true" },
|
|
6312
|
+
React.createElement("path", { d: "M3 8h5V3M17 8h-5V3M17 12h-5v5M3 12h5v5", ...stroke }));
|
|
6313
|
+
}
|
|
6314
|
+
|
|
6069
6315
|
/**
|
|
6070
6316
|
* Double-buffered canvas frame with PER-REVISION blob URLs.
|
|
6071
6317
|
*
|
|
@@ -6076,7 +6322,7 @@ function CanvasGlyph() {
|
|
|
6076
6322
|
* loads hidden-but-painted behind the live frame and is promoted on load, so
|
|
6077
6323
|
* a live redraw is a composite swap of something already drawn.
|
|
6078
6324
|
*/
|
|
6079
|
-
function CanvasFrame({ controller, sessionId, latestRev, zoom, visible = true, focusOnPromote = false, dataRev = 0, dataPayload = null }) {
|
|
6325
|
+
function CanvasFrame({ controller, sessionId, slot = 1, latestRev, zoom, visible = true, focusOnPromote = false, dataRev = 0, dataPayload = null }) {
|
|
6080
6326
|
const [live, setLive] = React.useState(null); // { url, rev }
|
|
6081
6327
|
const [staging, setStaging] = React.useState(null); // { url, rev }
|
|
6082
6328
|
const [loadError, setLoadError] = React.useState(null); // { rev }
|
|
@@ -6131,7 +6377,7 @@ function CanvasFrame({ controller, sessionId, latestRev, zoom, visible = true, f
|
|
|
6131
6377
|
if (!controller || !sessionId || !latestRev) return undefined;
|
|
6132
6378
|
let cancelled = false;
|
|
6133
6379
|
let retryTimer = null;
|
|
6134
|
-
fetchArtifactHtmlObjectUrl(controller, sessionId, "canvas.html")
|
|
6380
|
+
fetchArtifactHtmlObjectUrl(controller, sessionId, slot <= 1 ? "canvas.html" : `canvas${slot}.html`)
|
|
6135
6381
|
.then((url) => {
|
|
6136
6382
|
if (cancelled) { URL.revokeObjectURL(url); return; }
|
|
6137
6383
|
// A staging revision that never loaded is superseded here —
|
|
@@ -6159,7 +6405,7 @@ function CanvasFrame({ controller, sessionId, latestRev, zoom, visible = true, f
|
|
|
6159
6405
|
cancelled = true;
|
|
6160
6406
|
if (retryTimer) clearTimeout(retryTimer);
|
|
6161
6407
|
};
|
|
6162
|
-
}, [controller, sessionId, latestRev, retryTick]);
|
|
6408
|
+
}, [controller, sessionId, slot, latestRev, retryTick]);
|
|
6163
6409
|
|
|
6164
6410
|
// Session switch / unmount: drop both frames — a different session is a
|
|
6165
6411
|
// different document, and showing the old canvas under a new session is a
|
|
@@ -6201,7 +6447,7 @@ function CanvasFrame({ controller, sessionId, latestRev, zoom, visible = true, f
|
|
|
6201
6447
|
// toggle) still promotes new revisions in the background — those are
|
|
6202
6448
|
// NOT viewed; the badge must light until the user actually looks.
|
|
6203
6449
|
if (controller && sessionId && visibleRef.current) {
|
|
6204
|
-
controller.dispatch({ type: "canvas/viewed", sessionId, rev: frame.rev });
|
|
6450
|
+
controller.dispatch({ type: "canvas/viewed", sessionId, slot, rev: frame.rev });
|
|
6205
6451
|
}
|
|
6206
6452
|
// iOS: a programmatic focus into the freshly promoted frame is the
|
|
6207
6453
|
// synchronous interaction that registers its scroller — the code
|
|
@@ -6229,7 +6475,7 @@ function CanvasFrame({ controller, sessionId, latestRev, zoom, visible = true, f
|
|
|
6229
6475
|
// On-screen injection = seen; hidden injection leaves the badge lit
|
|
6230
6476
|
// until the user actually looks (the catch-up below files it then).
|
|
6231
6477
|
if (controller && sessionId && visibleRef.current) {
|
|
6232
|
-
controller.dispatch({ type: "canvas/viewed", sessionId, dataRev });
|
|
6478
|
+
controller.dispatch({ type: "canvas/viewed", sessionId, slot, dataRev });
|
|
6233
6479
|
}
|
|
6234
6480
|
}, [live, dataRev, dataPayload, controller, sessionId]);
|
|
6235
6481
|
|
|
@@ -6237,7 +6483,7 @@ function CanvasFrame({ controller, sessionId, latestRev, zoom, visible = true, f
|
|
|
6237
6483
|
// viewing it — this is what clears a badge earned while hidden.
|
|
6238
6484
|
React.useEffect(() => {
|
|
6239
6485
|
if (visible && live && controller && sessionId) {
|
|
6240
|
-
controller.dispatch({ type: "canvas/viewed", sessionId, rev: live.rev, ...(dataRev ? { dataRev } : {}) });
|
|
6486
|
+
controller.dispatch({ type: "canvas/viewed", sessionId, slot, rev: live.rev, ...(dataRev ? { dataRev } : {}) });
|
|
6241
6487
|
if (focusOnPromote) {
|
|
6242
6488
|
try { liveIframeElRef.current?.focus({ preventScroll: true }); } catch { /* best effort */ }
|
|
6243
6489
|
}
|
|
@@ -6327,12 +6573,20 @@ function CanvasFrame({ controller, sessionId, latestRev, zoom, visible = true, f
|
|
|
6327
6573
|
}
|
|
6328
6574
|
|
|
6329
6575
|
/**
|
|
6330
|
-
*
|
|
6331
|
-
*
|
|
6332
|
-
*
|
|
6333
|
-
*
|
|
6576
|
+
* The session's standing display: rendered from the reserved canvas.html
|
|
6577
|
+
* artifact and converging live on canvas_updated events.
|
|
6578
|
+
*
|
|
6579
|
+
* Desktop mounts it in the right column, where it replaces the
|
|
6580
|
+
* inspector/activity split while active (the artifact reader still takes
|
|
6581
|
+
* precedence while open and returns here). The phone mounts it as a layer over
|
|
6582
|
+
* the content region — see MobileCanvasLayer — and gets a stripped header: the
|
|
6583
|
+
* revision on one thin line with a maximize toggle, no zoom control and no ✕,
|
|
6584
|
+
* because there the toolbar's Canvas button is the only way in or out.
|
|
6585
|
+
*
|
|
6586
|
+
* maximized / onToggleMaximized are the phone's full-screen state, owned by
|
|
6587
|
+
* MobileCanvasLayer. Passing no handler simply omits the button.
|
|
6334
6588
|
*/
|
|
6335
|
-
function CanvasPane({ controller, mobile = false,
|
|
6589
|
+
function CanvasPane({ controller, mobile = false, visible = true, focusOnPromote = false, maximized = false, onToggleMaximized = null }) {
|
|
6336
6590
|
const view = useControllerSelector(controller, selectCanvasView, shallowEqualObject);
|
|
6337
6591
|
const zoom = useControllerSelector(controller, (state) => Number(state.files.htmlZoom) || 1);
|
|
6338
6592
|
const setZoom = React.useCallback((next) => {
|
|
@@ -6346,38 +6600,127 @@ function CanvasPane({ controller, mobile = false, onClose = null, visible = true
|
|
|
6346
6600
|
if (view.sessionId) controller.ensureCanvasSnapshot(view.sessionId).catch(() => {});
|
|
6347
6601
|
}, [controller, view.sessionId]);
|
|
6348
6602
|
|
|
6349
|
-
const
|
|
6350
|
-
|
|
6603
|
+
const zenOn = useControllerSelector(controller, (s) => s?.ui?.canvasZen === true);
|
|
6604
|
+
const [toolbarSlot, setToolbarSlot] = React.useState(null);
|
|
6605
|
+
React.useEffect(() => {
|
|
6606
|
+
if (!maximized) { setToolbarSlot(null); return undefined; }
|
|
6607
|
+
// The slot is rendered by the Toolbar in the SAME commit that flips
|
|
6608
|
+
// maximized, so poll one frame rather than racing it.
|
|
6609
|
+
let raf = 0;
|
|
6610
|
+
const find = () => {
|
|
6611
|
+
const el = document.getElementById("ps-toolbar-canvas-slot");
|
|
6612
|
+
if (el) setToolbarSlot(el);
|
|
6613
|
+
else raf = requestAnimationFrame(find);
|
|
6614
|
+
};
|
|
6615
|
+
find();
|
|
6616
|
+
return () => cancelAnimationFrame(raf);
|
|
6617
|
+
}, [maximized]);
|
|
6618
|
+
const closeCanvas = React.useCallback(() => {
|
|
6619
|
+
controller.dispatch({ type: "ui/canvasOpen", open: false, sessionId: view.sessionId, manual: true });
|
|
6351
6620
|
}, [controller, view.sessionId]);
|
|
6352
|
-
|
|
6621
|
+
|
|
6622
|
+
// The phone's header is ONE thin line: `rev 4` on the left, the
|
|
6623
|
+
// maximize/restore toggle hard right. Nothing else — no ✕ (the toolbar's
|
|
6624
|
+
// Canvas button is the way out) and no zoom control, because every pixel
|
|
6625
|
+
// of height here is canvas the user can no longer see.
|
|
6626
|
+
//
|
|
6627
|
+
// The strip renders whenever there is a revision to name OR the canvas is
|
|
6628
|
+
// maximized. That second half is not cosmetic: maximized covers the whole
|
|
6629
|
+
// viewport including the toolbar, so if a cleared canvas (sizeBytes 0)
|
|
6630
|
+
// dropped the strip it would take the only way back with it.
|
|
6631
|
+
const showRevStrip = mobile && (view.exists || maximized);
|
|
6632
|
+
const header = mobile
|
|
6633
|
+
? (showRevStrip
|
|
6634
|
+
? React.createElement("header", { className: "ps-artifact-pane-bar is-rev-strip" },
|
|
6635
|
+
React.createElement("span", { className: "ps-artifact-pane-type", title: view.note || undefined },
|
|
6636
|
+
view.exists ? `${view.name ? `${view.name} \u00b7 ` : ""}rev ${view.latestRev}` : ""),
|
|
6637
|
+
React.createElement(CanvasSlotControls, { controller, view, compact: true }),
|
|
6638
|
+
onToggleMaximized
|
|
6639
|
+
? React.createElement("button", {
|
|
6640
|
+
type: "button",
|
|
6641
|
+
className: "ps-canvas-max-btn",
|
|
6642
|
+
onClick: onToggleMaximized,
|
|
6643
|
+
title: maximized ? "Back to the inset canvas" : "Maximize — cover the whole screen",
|
|
6644
|
+
"aria-label": maximized ? "Restore canvas" : "Maximize canvas",
|
|
6645
|
+
}, React.createElement(maximized ? RestoreGlyph : MaximizeGlyph))
|
|
6646
|
+
: null)
|
|
6647
|
+
: null)
|
|
6648
|
+
: (maximized ? (toolbarSlot ? createPortal(React.createElement(React.Fragment, null,
|
|
6649
|
+
view.exists
|
|
6650
|
+
? React.createElement("span", { className: "ps-toolbar-canvas-rev" }, `${view.name ? `${view.name} \u00b7 ` : ""}rev ${view.latestRev}`)
|
|
6651
|
+
: null,
|
|
6652
|
+
React.createElement(CanvasSlotControls, { controller, view, compact: true }),
|
|
6653
|
+
view.exists ? React.createElement(HtmlZoomControl, { zoom, setZoom }) : null,
|
|
6654
|
+
React.createElement("button", {
|
|
6655
|
+
type: "button",
|
|
6656
|
+
className: `ps-artifact-pane-btn${zenOn ? " is-active" : ""}`,
|
|
6657
|
+
// Full screen covers the workspace, so a bare zen toggle
|
|
6658
|
+
// changes state UNDER the layer and looks dead. From here zen
|
|
6659
|
+
// means "step down into zen": drop full screen in the same
|
|
6660
|
+
// click so the change is visible.
|
|
6661
|
+
onClick: () => {
|
|
6662
|
+
controller.dispatch({ type: "ui/canvasMaximized", on: false });
|
|
6663
|
+
controller.dispatch({ type: "ui/canvasZen", on: !zenOn });
|
|
6664
|
+
},
|
|
6665
|
+
title: zenOn ? "Leave zen — bring back sessions and panels" : "Zen — chat rail + canvas only",
|
|
6666
|
+
"aria-label": zenOn ? "Leave zen mode" : "Zen mode",
|
|
6667
|
+
}, React.createElement(ZenGlyph)),
|
|
6668
|
+
React.createElement("button", {
|
|
6669
|
+
type: "button",
|
|
6670
|
+
className: "ps-artifact-pane-btn ps-toolbar-canvas-restore",
|
|
6671
|
+
onClick: () => controller.dispatch({ type: "ui/canvasMaximized", on: false }),
|
|
6672
|
+
title: "Back to the workspace",
|
|
6673
|
+
"aria-label": "Restore canvas",
|
|
6674
|
+
}, React.createElement(RestoreGlyph))), toolbarSlot) : null)
|
|
6675
|
+
: React.createElement("header", { className: "ps-artifact-pane-bar" },
|
|
6676
|
+
React.createElement("span", { className: "ps-artifact-pane-heading" },
|
|
6677
|
+
React.createElement("span", { className: "ps-artifact-pane-name ps-canvas-pane-glyph", title: view.note || "Canvas" },
|
|
6678
|
+
React.createElement(CanvasGlyph)),
|
|
6679
|
+
view.exists
|
|
6680
|
+
? React.createElement("span", { className: "ps-artifact-pane-type", title: view.note || undefined },
|
|
6681
|
+
`${view.name ? `${view.name} \u00b7 ` : ""}rev ${view.latestRev}`)
|
|
6682
|
+
: null,
|
|
6683
|
+
React.createElement(CanvasSlotControls, { controller, view })),
|
|
6684
|
+
React.createElement("span", { className: "ps-artifact-pane-actions" },
|
|
6685
|
+
view.exists ? React.createElement(HtmlZoomControl, { zoom, setZoom }) : null,
|
|
6686
|
+
// Zen: sessions step away, chat becomes a rail, the canvas is
|
|
6687
|
+
// the workbench. The mode for "the work happens on the canvas,
|
|
6688
|
+
// chat is for steering".
|
|
6689
|
+
React.createElement("button", {
|
|
6690
|
+
type: "button",
|
|
6691
|
+
className: `ps-artifact-pane-btn${zenOn ? " is-active" : ""}`,
|
|
6692
|
+
onClick: () => controller.dispatch({ type: "ui/canvasZen" }),
|
|
6693
|
+
title: zenOn ? "Leave zen — bring back sessions and panels" : "Zen — chat rail + canvas only",
|
|
6694
|
+
"aria-label": zenOn ? "Leave zen mode" : "Zen mode",
|
|
6695
|
+
}, React.createElement(ZenGlyph)),
|
|
6696
|
+
// Full screen. No ✕ beside it: the toolbar's Canvas button is
|
|
6697
|
+
// the way out, and a second close control in a pane that also
|
|
6698
|
+
// has a toggle is one affordance too many.
|
|
6699
|
+
//
|
|
6700
|
+
// While full screen this whole bar is hidden and the rev and
|
|
6701
|
+
// the way back are promoted into the portal header, so the
|
|
6702
|
+
// control never disappears with the pane that drew it.
|
|
6703
|
+
React.createElement("button", {
|
|
6704
|
+
type: "button",
|
|
6705
|
+
className: "ps-artifact-pane-btn",
|
|
6706
|
+
onClick: () => controller.dispatch({ type: "ui/canvasMaximized", on: true }),
|
|
6707
|
+
title: "Full screen — hide chat and sessions",
|
|
6708
|
+
"aria-label": "Full screen canvas",
|
|
6709
|
+
}, React.createElement(MaximizeGlyph)))));
|
|
6353
6710
|
|
|
6354
6711
|
return React.createElement("section", {
|
|
6355
6712
|
className: "ps-artifact-pane ps-canvas-pane",
|
|
6356
6713
|
role: "region",
|
|
6357
6714
|
"aria-label": "Session canvas",
|
|
6358
6715
|
},
|
|
6359
|
-
|
|
6360
|
-
React.createElement("span", { className: "ps-artifact-pane-heading" },
|
|
6361
|
-
React.createElement("span", { className: "ps-artifact-pane-name ps-canvas-pane-glyph", title: view.note || "Canvas" },
|
|
6362
|
-
React.createElement(CanvasGlyph)),
|
|
6363
|
-
view.exists
|
|
6364
|
-
? React.createElement("span", { className: "ps-artifact-pane-type", title: view.note || undefined },
|
|
6365
|
-
`rev ${view.latestRev}`)
|
|
6366
|
-
: null),
|
|
6367
|
-
React.createElement("span", { className: "ps-artifact-pane-actions" },
|
|
6368
|
-
view.exists ? React.createElement(HtmlZoomControl, { zoom, setZoom }) : null,
|
|
6369
|
-
close ? React.createElement("button", {
|
|
6370
|
-
type: "button",
|
|
6371
|
-
className: "ps-artifact-pane-btn",
|
|
6372
|
-
onClick: close,
|
|
6373
|
-
title: mobile ? "Close the canvas" : "Back to the inspector and activity panes",
|
|
6374
|
-
"aria-label": mobile ? "Close canvas" : "Back to panes",
|
|
6375
|
-
}, "✕") : null)),
|
|
6716
|
+
header,
|
|
6376
6717
|
React.createElement("div", { className: "ps-artifact-pane-body" },
|
|
6377
6718
|
view.exists
|
|
6378
6719
|
? React.createElement(CanvasFrame, {
|
|
6720
|
+
key: `slot:${view.slot}`,
|
|
6379
6721
|
controller,
|
|
6380
6722
|
sessionId: view.sessionId,
|
|
6723
|
+
slot: view.slot,
|
|
6381
6724
|
latestRev: view.latestRev,
|
|
6382
6725
|
zoom,
|
|
6383
6726
|
visible,
|
|
@@ -7659,7 +8002,7 @@ function IconButton({ icon, label, onClick, disabled = false, active = false, cl
|
|
|
7659
8002
|
tooltipNode);
|
|
7660
8003
|
}
|
|
7661
8004
|
|
|
7662
|
-
function Toolbar({ controller, mobile,
|
|
8005
|
+
function Toolbar({ controller, mobile, canvasPaneOpen = false, onToggleCanvasPane = null, mobilePane = "workspace", onSelectMobilePane = null, mobileMainLayout = "split" }) {
|
|
7663
8006
|
const [headerSlot, setHeaderSlot] = React.useState(null);
|
|
7664
8007
|
React.useEffect(() => {
|
|
7665
8008
|
if (typeof document === "undefined") return;
|
|
@@ -7671,6 +8014,12 @@ function Toolbar({ controller, mobile, canvasOverlayOpen = false, onToggleCanvas
|
|
|
7671
8014
|
}, [mobile]);
|
|
7672
8015
|
const adminVisible = useControllerSelector(controller, (state) => Boolean(state.admin?.visible));
|
|
7673
8016
|
const canvasView = useControllerSelector(controller, selectCanvasView, shallowEqualObject);
|
|
8017
|
+
const diagnosticsOpen = useControllerSelector(controller, (s) => s?.ui?.diagnosticsOpen === true);
|
|
8018
|
+
const canvasMaximized = useControllerSelector(controller, (s) => s?.ui?.canvasMaximized === true);
|
|
8019
|
+
// The artifact reader takes over the right side; while it is up, the
|
|
8020
|
+
// surface on screen is NOT the canvas, so the canvas toggle must not
|
|
8021
|
+
// read as active (its next press still shows the canvas).
|
|
8022
|
+
const artifactReaderOpen = useControllerSelector(controller, (s) => Boolean(s?.files?.paneOpen && s?.files?.selectedArtifactId));
|
|
7674
8023
|
|
|
7675
8024
|
// Icon-first toolbar: the glyph is the affordance, the label rides a
|
|
7676
8025
|
// tooltip (desktop hover via title; mobile long-press via IconButton).
|
|
@@ -7698,37 +8047,54 @@ function Toolbar({ controller, mobile, canvasOverlayOpen = false, onToggleCanvas
|
|
|
7698
8047
|
onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_THEME_PICKER).catch(() => {}),
|
|
7699
8048
|
},
|
|
7700
8049
|
// Canvas toggle, both device classes: the desktop flips the right
|
|
7701
|
-
// column, the phone
|
|
7702
|
-
// one badge
|
|
7703
|
-
//
|
|
8050
|
+
// column, the phone lays the canvas over the chat transcript — one
|
|
8051
|
+
// affordance, one badge, and on the phone the ONLY way in or out (the
|
|
8052
|
+
// canvas itself carries no ✕ there). While the canvas is hidden, an
|
|
8053
|
+
// undisplayed revision lights the badge.
|
|
7704
8054
|
{
|
|
7705
8055
|
key: "canvas",
|
|
7706
8056
|
icon: React.createElement(CanvasGlyph),
|
|
7707
|
-
label: (mobile ?
|
|
7708
|
-
?
|
|
8057
|
+
label: (mobile ? canvasPaneOpen : (canvasView.mode === "canvas" && !artifactReaderOpen))
|
|
8058
|
+
? "Hide the canvas"
|
|
7709
8059
|
: "Show canvas",
|
|
7710
8060
|
onClick: () => {
|
|
7711
8061
|
if (mobile) {
|
|
7712
|
-
if (
|
|
8062
|
+
if (onToggleCanvasPane) onToggleCanvasPane();
|
|
7713
8063
|
return;
|
|
7714
8064
|
}
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
8065
|
+
// Desktop: the canvas is a column of its own now. Closing it
|
|
8066
|
+
// shows nothing in its place — Diagnostics has its own toggle.
|
|
8067
|
+
controller.dispatch({
|
|
8068
|
+
type: "ui/canvasOpen",
|
|
8069
|
+
open: canvasView.mode !== "canvas",
|
|
8070
|
+
sessionId: canvasView.sessionId,
|
|
8071
|
+
manual: true,
|
|
8072
|
+
});
|
|
7720
8073
|
},
|
|
7721
|
-
active: mobile ?
|
|
8074
|
+
active: mobile ? canvasPaneOpen : (canvasView.mode === "canvas" && !artifactReaderOpen),
|
|
7722
8075
|
badge: mobile
|
|
7723
8076
|
? (canvasView.exists
|
|
7724
8077
|
&& (canvasView.latestRev > (canvasView.lastViewedRev || 0)
|
|
7725
8078
|
|| (canvasView.latestDataRev || 0) > (canvasView.lastViewedDataRev || 0))
|
|
7726
|
-
&& !
|
|
8079
|
+
&& !canvasPaneOpen)
|
|
7727
8080
|
: canvasView.unseen,
|
|
7728
8081
|
// Three states: plain outline (empty), green dot bottom-left
|
|
7729
8082
|
// (loaded), green + yellow (loaded with unseen changes).
|
|
7730
8083
|
loadedDot: canvasView.exists,
|
|
7731
8084
|
},
|
|
8085
|
+
// Diagnostics — Inspector + Activity as one column, desktop only.
|
|
8086
|
+
//
|
|
8087
|
+
// They used to be tabs inside the chat pane and were always present.
|
|
8088
|
+
// Off by default now, and behind this toggle: they are instrumentation,
|
|
8089
|
+
// not part of reading a conversation. The phone keeps them as nav tabs,
|
|
8090
|
+
// where there is no column to put them in.
|
|
8091
|
+
...(mobile ? [] : [{
|
|
8092
|
+
key: "diagnostics",
|
|
8093
|
+
icon: React.createElement(DiagnosticsGlyph),
|
|
8094
|
+
label: diagnosticsOpen ? "Hide diagnostics" : "Show diagnostics (inspector and activity)",
|
|
8095
|
+
onClick: () => controller.dispatch({ type: "ui/diagnosticsOpen" }),
|
|
8096
|
+
active: diagnosticsOpen,
|
|
8097
|
+
}]),
|
|
7732
8098
|
// Admin console is desktop-only: its settings tree, package detail
|
|
7733
8099
|
// and file preview need width the phone layout cannot give them, so
|
|
7734
8100
|
// the button is omitted rather than shipped half-working.
|
|
@@ -7773,20 +8139,34 @@ function Toolbar({ controller, mobile, canvasOverlayOpen = false, onToggleCanvas
|
|
|
7773
8139
|
chat: "Main — chat only (tap for sessions only)",
|
|
7774
8140
|
sessions: "Main — sessions only (tap for both)",
|
|
7775
8141
|
};
|
|
8142
|
+
// Two tabs, not three: the row was out of space, and Inspector +
|
|
8143
|
+
// Activity are one idea on the desktop already (Diagnostics, the
|
|
8144
|
+
// bug). The bug button opens the inspector; tapping it again while
|
|
8145
|
+
// inside diagnostics flips inspector <-> activity; the main button is
|
|
8146
|
+
// always the way back.
|
|
8147
|
+
const onDiagnostics = mobilePane === "inspector" || mobilePane === "activity";
|
|
8148
|
+
const nextDiagnosticsPane = mobilePane === "inspector" ? "activity" : "inspector";
|
|
7776
8149
|
const paneDefs = [
|
|
7777
8150
|
{ id: "workspace", icon: MainLayoutGlyph, label: mainLabels[mobileMainLayout] || mainLabels.split, focus: "chat" },
|
|
7778
|
-
{
|
|
7779
|
-
|
|
8151
|
+
{
|
|
8152
|
+
id: nextDiagnosticsPane,
|
|
8153
|
+
icon: DiagnosticsGlyph,
|
|
8154
|
+
active: onDiagnostics,
|
|
8155
|
+
label: onDiagnostics
|
|
8156
|
+
? (mobilePane === "inspector" ? "Diagnostics — inspector (tap for activity)" : "Diagnostics — activity (tap for inspector)")
|
|
8157
|
+
: "Diagnostics — inspector and activity",
|
|
8158
|
+
focus: nextDiagnosticsPane,
|
|
8159
|
+
},
|
|
7780
8160
|
];
|
|
7781
8161
|
return React.createElement("div", { className: "ps-toolbar is-mobile" },
|
|
7782
8162
|
React.createElement("div", { className: "ps-toolbar-row ps-toolbar-row-primary" },
|
|
7783
8163
|
React.createElement("div", { className: "ps-toolbar-row-actions" }, buttonDefs.map(renderButton)),
|
|
7784
8164
|
React.createElement("div", { className: "ps-toolbar-row-actions is-panes" },
|
|
7785
8165
|
paneDefs.map((def) => React.createElement(IconButton, {
|
|
7786
|
-
key: def.id,
|
|
8166
|
+
key: def.id === "workspace" ? "workspace" : "diagnostics",
|
|
7787
8167
|
icon: React.createElement(def.icon),
|
|
7788
8168
|
label: def.label,
|
|
7789
|
-
active: mobilePane === def.id,
|
|
8169
|
+
active: def.active ?? (mobilePane === def.id),
|
|
7790
8170
|
onClick: () => {
|
|
7791
8171
|
if (onSelectMobilePane) onSelectMobilePane(def.id, def.focus);
|
|
7792
8172
|
},
|
|
@@ -7794,8 +8174,54 @@ function Toolbar({ controller, mobile, canvasOverlayOpen = false, onToggleCanvas
|
|
|
7794
8174
|
);
|
|
7795
8175
|
}
|
|
7796
8176
|
|
|
8177
|
+
// Two groups. Diagnostics and the admin console are TOOLS — they act on
|
|
8178
|
+
// the app rather than on the conversation — so they sit apart from the
|
|
8179
|
+
// session controls, hard right. Everything else keeps its order.
|
|
8180
|
+
const RIGHT_GROUP = new Set(["diagnostics", "admin"]);
|
|
8181
|
+
|
|
8182
|
+
// While the canvas is full screen, ANY other button first drops full
|
|
8183
|
+
// screen and then does its own job. Pressing Filter and watching nothing
|
|
8184
|
+
// happen behind a canvas — or watching a panel open where you cannot see
|
|
8185
|
+
// it — is the confusing half of every full-screen mode. The Canvas toggle
|
|
8186
|
+
// is exempt: it already means "put the canvas away", and closing drops the
|
|
8187
|
+
// flag in the reducer.
|
|
8188
|
+
const withRestore = (def) => (!canvasMaximized || def.key === "canvas" ? def : {
|
|
8189
|
+
...def,
|
|
8190
|
+
onClick: (...args) => {
|
|
8191
|
+
controller.dispatch({ type: "ui/canvasMaximized", on: false });
|
|
8192
|
+
return def.onClick?.(...args);
|
|
8193
|
+
},
|
|
8194
|
+
});
|
|
8195
|
+
|
|
8196
|
+
const leftButtons = buttonDefs.filter((d) => !RIGHT_GROUP.has(d.key)).map(withRestore);
|
|
8197
|
+
const rightButtons = buttonDefs.filter((d) => RIGHT_GROUP.has(d.key)).map(withRestore);
|
|
7797
8198
|
const toolbar = React.createElement("div", { className: "ps-toolbar" },
|
|
7798
|
-
|
|
8199
|
+
// Three columns: left rail | main controls | right rail. Full-screen
|
|
8200
|
+
// canvas uses the otherwise-empty left rail for the normal actions so
|
|
8201
|
+
// they do not crowd the canvas metadata and controls across the top.
|
|
8202
|
+
React.createElement("div", { className: "ps-toolbar-side is-left" },
|
|
8203
|
+
canvasMaximized ? leftButtons.map(renderButton) : null),
|
|
8204
|
+
React.createElement("div", { className: "ps-toolbar-actions" },
|
|
8205
|
+
canvasMaximized ? null : leftButtons.map(renderButton)),
|
|
8206
|
+
React.createElement("div", { className: "ps-toolbar-side is-right" },
|
|
8207
|
+
// The portal header parks its version/status meta here, LEFT of
|
|
8208
|
+
// the tool buttons, and its sign-out glyph in the slot after
|
|
8209
|
+
// them — one right-aligned cluster: meta · bug · settings · out.
|
|
8210
|
+
React.createElement("span", { className: "ps-toolbar-meta-slot", id: "ps-toolbar-meta-slot" }),
|
|
8211
|
+
// While the canvas is full screen its header controls (rev, zoom,
|
|
8212
|
+
// zen, restore) portal INTO this slot from CanvasPane — zoom state
|
|
8213
|
+
// lives there, so the controls come to the toolbar rather than
|
|
8214
|
+
// their state moving out.
|
|
8215
|
+
canvasMaximized
|
|
8216
|
+
? React.createElement("span", { className: "ps-toolbar-canvas-slot", id: "ps-toolbar-canvas-slot" })
|
|
8217
|
+
: null,
|
|
8218
|
+
canvasMaximized
|
|
8219
|
+
? React.createElement("span", { className: "ps-toolbar-divider", "aria-hidden": "true" })
|
|
8220
|
+
: null,
|
|
8221
|
+
rightButtons.length
|
|
8222
|
+
? React.createElement("div", { className: "ps-toolbar-actions is-tools" }, rightButtons.map(renderButton))
|
|
8223
|
+
: null,
|
|
8224
|
+
React.createElement("span", { className: "ps-toolbar-signout-slot", id: "ps-toolbar-signout-slot" })),
|
|
7799
8225
|
);
|
|
7800
8226
|
|
|
7801
8227
|
// Rich desktop UI: the toolbar lives IN the portal header (one top bar)
|
|
@@ -8084,112 +8510,318 @@ function SessionColumnResizeHandle({ controller, portalSessionColumnAdjust = 0 }
|
|
|
8084
8510
|
React.createElement("span", { className: "ps-column-resizer-dot" })));
|
|
8085
8511
|
}
|
|
8086
8512
|
|
|
8513
|
+
|
|
8087
8514
|
/**
|
|
8088
|
-
*
|
|
8515
|
+
* The seam between the canvas and diagnostics columns.
|
|
8089
8516
|
*
|
|
8090
|
-
*
|
|
8091
|
-
*
|
|
8092
|
-
* pixels, whatever that happens to be. Dividing the pointer delta by a fixed
|
|
8093
|
-
* GRID_CELL_HEIGHT only tracks if the column is exactly rows × 19px tall, and
|
|
8094
|
-
* it is not: the column excludes the header and toolbar above it, and the row
|
|
8095
|
-
* height moves with the type scale. So the seam ran ahead of the pointer or
|
|
8096
|
-
* trailed behind it, and the error grew with the drag.
|
|
8517
|
+
* Only rendered when both are open — with one column the outer chat/right
|
|
8518
|
+
* handle is already the only split there is.
|
|
8097
8519
|
*
|
|
8098
|
-
*
|
|
8099
|
-
*
|
|
8100
|
-
*
|
|
8520
|
+
* Drag either column below PORTAL_COLUMN_SNAP_CLOSED_PX and releasing CLOSES
|
|
8521
|
+
* it: the user asked for "resized to 0 means toggled off, and it has to be
|
|
8522
|
+
* toggled back on". The adjust is reset at the same time, so re-opening from
|
|
8523
|
+
* the toolbar comes back at the even split rather than at the sliver that
|
|
8524
|
+
* closed it.
|
|
8101
8525
|
*/
|
|
8102
|
-
function
|
|
8103
|
-
try {
|
|
8104
|
-
const layout = controller.getCurrentLayout?.();
|
|
8105
|
-
const rows = (layout?.inspectorPaneHeight || 0) + (layout?.activityPaneHeight || 0);
|
|
8106
|
-
const column = handleEl?.closest?.(".ps-workspace-column");
|
|
8107
|
-
if (!column || rows <= 0) return GRID_CELL_HEIGHT;
|
|
8108
|
-
// The resizer's own track is not part of either pane's share.
|
|
8109
|
-
const usable = column.clientHeight - (handleEl.getBoundingClientRect().height || 0);
|
|
8110
|
-
const pxPerRow = usable / rows;
|
|
8111
|
-
return Number.isFinite(pxPerRow) && pxPerRow > 0 ? pxPerRow : GRID_CELL_HEIGHT;
|
|
8112
|
-
} catch {
|
|
8113
|
-
return GRID_CELL_HEIGHT;
|
|
8114
|
-
}
|
|
8115
|
-
}
|
|
8116
|
-
|
|
8117
|
-
function ActivityRowResizeHandle({ controller, activityPaneAdjust = 0 }) {
|
|
8526
|
+
function CanvasDiagnosticsResizeHandle({ controller, canvasPaneAdjust = 0, diagnosticsPaneAdjust = 0 }) {
|
|
8118
8527
|
const dragStateRef = React.useRef(null);
|
|
8119
8528
|
const [dragging, setDragging] = React.useState(false);
|
|
8120
8529
|
|
|
8530
|
+
const widths = (ca, da) => ({
|
|
8531
|
+
canvas: PORTAL_CANVAS_COL_DEFAULT_PX + (Number(ca) || 0),
|
|
8532
|
+
diag: PORTAL_DIAG_COL_DEFAULT_PX + (Number(da) || 0),
|
|
8533
|
+
});
|
|
8534
|
+
|
|
8535
|
+
// Moving the seam trades pixels between the two columns; the total — and
|
|
8536
|
+
// therefore chat — does not move. Below the snap on release, the starved
|
|
8537
|
+
// column CLOSES and both widths reset, so reopening is always usable.
|
|
8538
|
+
const commit = (ca, da) => {
|
|
8539
|
+
const w = widths(ca, da);
|
|
8540
|
+
if (w.canvas < PORTAL_COLUMN_SNAP_CLOSED_PX || w.diag < PORTAL_COLUMN_SNAP_CLOSED_PX) {
|
|
8541
|
+
controller.dispatch({ type: "ui/canvasPaneAdjust", canvasPaneAdjust: 0 });
|
|
8542
|
+
controller.dispatch({ type: "ui/diagnosticsPaneAdjust", diagnosticsPaneAdjust: 0 });
|
|
8543
|
+
controller.dispatch(w.canvas < PORTAL_COLUMN_SNAP_CLOSED_PX
|
|
8544
|
+
? { type: "ui/canvasOpen", open: false, manual: true }
|
|
8545
|
+
: { type: "ui/diagnosticsOpen", open: false });
|
|
8546
|
+
return;
|
|
8547
|
+
}
|
|
8548
|
+
controller.dispatch({ type: "ui/canvasPaneAdjust", canvasPaneAdjust: ca });
|
|
8549
|
+
controller.dispatch({ type: "ui/diagnosticsPaneAdjust", diagnosticsPaneAdjust: da });
|
|
8550
|
+
};
|
|
8551
|
+
|
|
8121
8552
|
React.useEffect(() => {
|
|
8122
8553
|
if (!dragging) return undefined;
|
|
8554
|
+
const onMove = (event) => {
|
|
8555
|
+
const st = dragStateRef.current;
|
|
8556
|
+
if (!st) return;
|
|
8557
|
+
const dx = event.clientX - st.startX;
|
|
8558
|
+
controller.dispatch({ type: "ui/canvasPaneAdjust", canvasPaneAdjust: st.startCanvas + dx });
|
|
8559
|
+
controller.dispatch({ type: "ui/diagnosticsPaneAdjust", diagnosticsPaneAdjust: st.startDiag - dx });
|
|
8560
|
+
};
|
|
8561
|
+
const onUp = () => {
|
|
8562
|
+
const st = dragStateRef.current;
|
|
8563
|
+
dragStateRef.current = null;
|
|
8564
|
+
setDragging(false);
|
|
8565
|
+
document.body.classList.remove("is-resizing-pane-x");
|
|
8566
|
+
if (st) {
|
|
8567
|
+
const ui = controller.getState()?.ui?.layout || {};
|
|
8568
|
+
commit(ui.canvasPaneAdjust ?? 0, ui.diagnosticsPaneAdjust ?? 0);
|
|
8569
|
+
}
|
|
8570
|
+
};
|
|
8571
|
+
window.addEventListener("pointermove", onMove);
|
|
8572
|
+
window.addEventListener("pointerup", onUp);
|
|
8573
|
+
window.addEventListener("pointercancel", onUp);
|
|
8574
|
+
return () => {
|
|
8575
|
+
window.removeEventListener("pointermove", onMove);
|
|
8576
|
+
window.removeEventListener("pointerup", onUp);
|
|
8577
|
+
window.removeEventListener("pointercancel", onUp);
|
|
8578
|
+
};
|
|
8579
|
+
}, [controller, dragging]);
|
|
8123
8580
|
|
|
8124
|
-
|
|
8581
|
+
return React.createElement("button", {
|
|
8582
|
+
type: "button",
|
|
8583
|
+
className: `ps-column-resizer${dragging ? " is-dragging" : ""}`,
|
|
8584
|
+
title: "Drag to trade width between the canvas and diagnostics. Drag one away to close it. Double-click to reset.",
|
|
8585
|
+
"aria-label": "Resize canvas and diagnostics columns",
|
|
8586
|
+
onPointerDown: (event) => {
|
|
8587
|
+
if (event.button !== 0) return;
|
|
8588
|
+
event.preventDefault();
|
|
8589
|
+
capturePointerForDrag(event);
|
|
8590
|
+
dragStateRef.current = {
|
|
8591
|
+
startX: event.clientX,
|
|
8592
|
+
startCanvas: Number(canvasPaneAdjust) || 0,
|
|
8593
|
+
startDiag: Number(diagnosticsPaneAdjust) || 0,
|
|
8594
|
+
};
|
|
8595
|
+
setDragging(true);
|
|
8596
|
+
document.body.classList.add("is-resizing-pane-x");
|
|
8597
|
+
},
|
|
8598
|
+
onDoubleClick: () => {
|
|
8599
|
+
controller.dispatch({ type: "ui/canvasPaneAdjust", canvasPaneAdjust: 0 });
|
|
8600
|
+
controller.dispatch({ type: "ui/diagnosticsPaneAdjust", diagnosticsPaneAdjust: 0 });
|
|
8601
|
+
},
|
|
8602
|
+
onKeyDown: (event) => {
|
|
8603
|
+
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
8604
|
+
event.preventDefault();
|
|
8605
|
+
const dx = event.key === "ArrowRight" ? 24 : -24;
|
|
8606
|
+
commit((Number(canvasPaneAdjust) || 0) + dx, (Number(diagnosticsPaneAdjust) || 0) - dx);
|
|
8607
|
+
},
|
|
8608
|
+
},
|
|
8609
|
+
React.createElement("span", { className: "ps-column-resizer-handle", "aria-hidden": "true" },
|
|
8610
|
+
React.createElement("span", { className: "ps-column-resizer-dot" }),
|
|
8611
|
+
React.createElement("span", { className: "ps-column-resizer-dot" }),
|
|
8612
|
+
React.createElement("span", { className: "ps-column-resizer-dot" })));
|
|
8613
|
+
}
|
|
8614
|
+
|
|
8615
|
+
/** The zen seam: drags the chat rail, remembered per session. */
|
|
8616
|
+
function ZenRailResizeHandle({ controller, sessionId, railPx = 380 }) {
|
|
8617
|
+
const dragStateRef = React.useRef(null);
|
|
8618
|
+
const [dragging, setDragging] = React.useState(false);
|
|
8619
|
+
React.useEffect(() => {
|
|
8620
|
+
if (!dragging) return undefined;
|
|
8621
|
+
const onMove = (event) => {
|
|
8622
|
+
const st = dragStateRef.current;
|
|
8623
|
+
if (!st) return;
|
|
8624
|
+
controller.dispatch({ type: "canvas/zenRail", sessionId, px: st.start + (event.clientX - st.startX) });
|
|
8625
|
+
};
|
|
8626
|
+
const onUp = () => {
|
|
8125
8627
|
dragStateRef.current = null;
|
|
8126
8628
|
setDragging(false);
|
|
8127
|
-
document.body.classList.remove("is-resizing-pane-
|
|
8629
|
+
document.body.classList.remove("is-resizing-pane-x");
|
|
8128
8630
|
};
|
|
8631
|
+
window.addEventListener("pointermove", onMove);
|
|
8632
|
+
window.addEventListener("pointerup", onUp);
|
|
8633
|
+
window.addEventListener("pointercancel", onUp);
|
|
8634
|
+
return () => {
|
|
8635
|
+
window.removeEventListener("pointermove", onMove);
|
|
8636
|
+
window.removeEventListener("pointerup", onUp);
|
|
8637
|
+
window.removeEventListener("pointercancel", onUp);
|
|
8638
|
+
};
|
|
8639
|
+
}, [controller, dragging, sessionId]);
|
|
8640
|
+
return React.createElement("button", {
|
|
8641
|
+
type: "button",
|
|
8642
|
+
className: `ps-column-resizer${dragging ? " is-dragging" : ""}`,
|
|
8643
|
+
title: "Drag to resize chat against the canvas (remembered for this session)",
|
|
8644
|
+
"aria-label": "Resize the zen chat rail",
|
|
8645
|
+
onPointerDown: (event) => {
|
|
8646
|
+
if (event.button !== 0) return;
|
|
8647
|
+
event.preventDefault();
|
|
8648
|
+
capturePointerForDrag(event);
|
|
8649
|
+
dragStateRef.current = { startX: event.clientX, start: railPx };
|
|
8650
|
+
setDragging(true);
|
|
8651
|
+
document.body.classList.add("is-resizing-pane-x");
|
|
8652
|
+
},
|
|
8653
|
+
onKeyDown: (event) => {
|
|
8654
|
+
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
8655
|
+
event.preventDefault();
|
|
8656
|
+
controller.dispatch({ type: "canvas/zenRail", sessionId, px: railPx + (event.key === "ArrowRight" ? 24 : -24) });
|
|
8657
|
+
},
|
|
8658
|
+
},
|
|
8659
|
+
React.createElement("span", { className: "ps-column-resizer-handle", "aria-hidden": "true" },
|
|
8660
|
+
React.createElement("span", { className: "ps-column-resizer-dot" }),
|
|
8661
|
+
React.createElement("span", { className: "ps-column-resizer-dot" }),
|
|
8662
|
+
React.createElement("span", { className: "ps-column-resizer-dot" })));
|
|
8663
|
+
}
|
|
8129
8664
|
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8665
|
+
/**
|
|
8666
|
+
* The seam between chat and the right block. Chat flexes, so this drags the
|
|
8667
|
+
* width of the column beside it — canvas when open, else diagnostics. The
|
|
8668
|
+
* other column never moves: growing chat shrinks only its neighbour.
|
|
8669
|
+
*/
|
|
8670
|
+
function ChatRightResizeHandle({ controller, target, adjust = 0 }) {
|
|
8671
|
+
const dragStateRef = React.useRef(null);
|
|
8672
|
+
const [dragging, setDragging] = React.useState(false);
|
|
8673
|
+
const actionType = target === "canvas" ? "ui/canvasPaneAdjust" : "ui/diagnosticsPaneAdjust";
|
|
8674
|
+
const key = target === "canvas" ? "canvasPaneAdjust" : "diagnosticsPaneAdjust";
|
|
8675
|
+
const base = target === "canvas" ? PORTAL_CANVAS_COL_DEFAULT_PX : PORTAL_DIAG_COL_DEFAULT_PX;
|
|
8676
|
+
|
|
8677
|
+
const commit = (next) => {
|
|
8678
|
+
if (base + next < PORTAL_COLUMN_SNAP_CLOSED_PX) {
|
|
8679
|
+
controller.dispatch({ type: actionType, [key]: 0 });
|
|
8680
|
+
controller.dispatch(target === "canvas"
|
|
8681
|
+
? { type: "ui/canvasOpen", open: false, manual: true }
|
|
8682
|
+
: { type: "ui/diagnosticsOpen", open: false });
|
|
8683
|
+
return;
|
|
8684
|
+
}
|
|
8685
|
+
controller.dispatch({ type: actionType, [key]: next });
|
|
8686
|
+
};
|
|
8687
|
+
|
|
8688
|
+
React.useEffect(() => {
|
|
8689
|
+
if (!dragging) return undefined;
|
|
8690
|
+
const onMove = (event) => {
|
|
8691
|
+
const st = dragStateRef.current;
|
|
8692
|
+
if (!st) return;
|
|
8693
|
+
// Pointer right = chat grows = the neighbour gives up the pixels.
|
|
8694
|
+
controller.dispatch({ type: actionType, [key]: st.startAdjust - (event.clientX - st.startX) });
|
|
8695
|
+
};
|
|
8696
|
+
const onUp = () => {
|
|
8697
|
+
const st = dragStateRef.current;
|
|
8698
|
+
dragStateRef.current = null;
|
|
8699
|
+
setDragging(false);
|
|
8700
|
+
document.body.classList.remove("is-resizing-pane-x");
|
|
8701
|
+
if (st) commit((controller.getState()?.ui?.layout || {})[key] ?? 0);
|
|
8702
|
+
};
|
|
8703
|
+
window.addEventListener("pointermove", onMove);
|
|
8704
|
+
window.addEventListener("pointerup", onUp);
|
|
8705
|
+
window.addEventListener("pointercancel", onUp);
|
|
8706
|
+
return () => {
|
|
8707
|
+
window.removeEventListener("pointermove", onMove);
|
|
8708
|
+
window.removeEventListener("pointerup", onUp);
|
|
8709
|
+
window.removeEventListener("pointercancel", onUp);
|
|
8138
8710
|
};
|
|
8711
|
+
}, [controller, dragging, actionType, key]);
|
|
8139
8712
|
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8713
|
+
return React.createElement("button", {
|
|
8714
|
+
type: "button",
|
|
8715
|
+
className: `ps-column-resizer${dragging ? " is-dragging" : ""}`,
|
|
8716
|
+
title: "Drag to resize. Drag the column away to close it. Double-click to reset.",
|
|
8717
|
+
"aria-label": "Resize the right column",
|
|
8718
|
+
onPointerDown: (event) => {
|
|
8719
|
+
if (event.button !== 0) return;
|
|
8720
|
+
event.preventDefault();
|
|
8721
|
+
capturePointerForDrag(event);
|
|
8722
|
+
dragStateRef.current = { startX: event.clientX, startAdjust: Number(adjust) || 0 };
|
|
8723
|
+
setDragging(true);
|
|
8724
|
+
document.body.classList.add("is-resizing-pane-x");
|
|
8725
|
+
},
|
|
8726
|
+
onDoubleClick: () => controller.dispatch({ type: actionType, [key]: 0 }),
|
|
8727
|
+
onKeyDown: (event) => {
|
|
8728
|
+
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
8729
|
+
event.preventDefault();
|
|
8730
|
+
commit((Number(adjust) || 0) - (event.key === "ArrowRight" ? 24 : -24));
|
|
8731
|
+
},
|
|
8732
|
+
},
|
|
8733
|
+
React.createElement("span", { className: "ps-column-resizer-handle", "aria-hidden": "true" },
|
|
8734
|
+
React.createElement("span", { className: "ps-column-resizer-dot" }),
|
|
8735
|
+
React.createElement("span", { className: "ps-column-resizer-dot" }),
|
|
8736
|
+
React.createElement("span", { className: "ps-column-resizer-dot" })));
|
|
8737
|
+
}
|
|
8143
8738
|
|
|
8739
|
+
/**
|
|
8740
|
+
* The inspector/activity seam, in continuous pixels — it follows the pointer
|
|
8741
|
+
* 1:1. Replaces the row-quantized ActivityRowResizeHandle for the portal:
|
|
8742
|
+
* that one converted drags to whole terminal rows (~20px notches) against
|
|
8743
|
+
* geometry computed from the whole viewport rather than this column, and the
|
|
8744
|
+
* legacy layout could snap-collapse a pane mid-drag. The grid's minmax does
|
|
8745
|
+
* all the clamping now.
|
|
8746
|
+
*/
|
|
8747
|
+
function DiagnosticsSplitResizeHandle({ controller, splitAdjust = 0 }) {
|
|
8748
|
+
const dragStateRef = React.useRef(null);
|
|
8749
|
+
const [dragging, setDragging] = React.useState(false);
|
|
8750
|
+
React.useEffect(() => {
|
|
8751
|
+
if (!dragging) return undefined;
|
|
8752
|
+
const onMove = (event) => {
|
|
8753
|
+
const st = dragStateRef.current;
|
|
8754
|
+
if (!st) return;
|
|
8755
|
+
// The first grid track is `50% + adjust`, while the divider takes
|
|
8756
|
+
// real space between the two panes. Its bounds are asymmetric:
|
|
8757
|
+
// -column/2 makes Inspector 0px; column/2-divider makes Activity
|
|
8758
|
+
// 0px. Using one symmetric half-range left an 8-9px pane remnant.
|
|
8759
|
+
const next = clamp(st.start + (event.clientY - st.startY), st.minAdjust, st.maxAdjust);
|
|
8760
|
+
controller.dispatch({ type: "ui/diagnosticsSplitAdjust", diagnosticsSplitAdjust: next });
|
|
8761
|
+
};
|
|
8762
|
+
const onUp = () => {
|
|
8763
|
+
dragStateRef.current = null;
|
|
8764
|
+
setDragging(false);
|
|
8765
|
+
document.body.classList.remove("is-resizing-pane-y");
|
|
8766
|
+
};
|
|
8767
|
+
window.addEventListener("pointermove", onMove);
|
|
8768
|
+
window.addEventListener("pointerup", onUp);
|
|
8769
|
+
window.addEventListener("pointercancel", onUp);
|
|
8144
8770
|
return () => {
|
|
8145
|
-
window.removeEventListener("pointermove",
|
|
8146
|
-
window.removeEventListener("pointerup",
|
|
8147
|
-
window.removeEventListener("pointercancel",
|
|
8771
|
+
window.removeEventListener("pointermove", onMove);
|
|
8772
|
+
window.removeEventListener("pointerup", onUp);
|
|
8773
|
+
window.removeEventListener("pointercancel", onUp);
|
|
8148
8774
|
document.body.classList.remove("is-resizing-pane-y");
|
|
8149
8775
|
};
|
|
8150
8776
|
}, [controller, dragging]);
|
|
8151
|
-
|
|
8152
8777
|
return React.createElement("button", {
|
|
8153
8778
|
type: "button",
|
|
8154
8779
|
className: `ps-row-resizer${dragging ? " is-dragging" : ""}`,
|
|
8155
|
-
title: "Drag to resize the activity
|
|
8156
|
-
"aria-label": "Resize activity
|
|
8780
|
+
title: "Drag to resize the inspector and activity panes. Double-click to reset.",
|
|
8781
|
+
"aria-label": "Resize the inspector and activity panes",
|
|
8157
8782
|
onPointerDown: (event) => {
|
|
8158
8783
|
if (event.button !== 0) return;
|
|
8159
8784
|
event.preventDefault();
|
|
8160
8785
|
capturePointerForDrag(event);
|
|
8786
|
+
const col = event.currentTarget.closest(".ps-workspace-column");
|
|
8787
|
+
const columnHeight = Math.max(0, col?.getBoundingClientRect?.().height || 0);
|
|
8788
|
+
const dividerHeight = Math.max(0, event.currentTarget.getBoundingClientRect().height || 0);
|
|
8789
|
+
const { minAdjust, maxAdjust } = getDiagnosticsSplitAdjustBounds(columnHeight, dividerHeight);
|
|
8161
8790
|
dragStateRef.current = {
|
|
8162
8791
|
startY: event.clientY,
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8792
|
+
start: clamp(Number(splitAdjust) || 0, minAdjust, maxAdjust),
|
|
8793
|
+
minAdjust,
|
|
8794
|
+
maxAdjust,
|
|
8166
8795
|
};
|
|
8167
8796
|
setDragging(true);
|
|
8168
8797
|
document.body.classList.add("is-resizing-pane-y");
|
|
8169
8798
|
},
|
|
8170
|
-
onDoubleClick: () => {
|
|
8171
|
-
if (!activityPaneAdjust) return;
|
|
8172
|
-
controller.adjustActivityPaneSplit(-activityPaneAdjust);
|
|
8173
|
-
},
|
|
8799
|
+
onDoubleClick: () => controller.dispatch({ type: "ui/diagnosticsSplitAdjust", diagnosticsSplitAdjust: 0 }),
|
|
8174
8800
|
onKeyDown: (event) => {
|
|
8175
|
-
if (event.key
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8801
|
+
if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
|
|
8802
|
+
event.preventDefault();
|
|
8803
|
+
const col = event.currentTarget.closest(".ps-workspace-column");
|
|
8804
|
+
const columnHeight = Math.max(0, col?.getBoundingClientRect?.().height || 0);
|
|
8805
|
+
const dividerHeight = Math.max(0, event.currentTarget.getBoundingClientRect().height || 0);
|
|
8806
|
+
const { minAdjust, maxAdjust } = getDiagnosticsSplitAdjustBounds(columnHeight, dividerHeight);
|
|
8807
|
+
controller.dispatch({
|
|
8808
|
+
type: "ui/diagnosticsSplitAdjust",
|
|
8809
|
+
diagnosticsSplitAdjust: clamp(
|
|
8810
|
+
(Number(splitAdjust) || 0) + (event.key === "ArrowDown" ? dividerHeight : -dividerHeight),
|
|
8811
|
+
minAdjust,
|
|
8812
|
+
maxAdjust,
|
|
8813
|
+
),
|
|
8814
|
+
});
|
|
8184
8815
|
},
|
|
8185
8816
|
},
|
|
8186
8817
|
React.createElement("span", { className: "ps-row-resizer-handle", "aria-hidden": "true" },
|
|
8187
|
-
React.createElement("span", { className: "ps-
|
|
8188
|
-
React.createElement("span", { className: "ps-
|
|
8189
|
-
React.createElement("span", { className: "ps-
|
|
8818
|
+
React.createElement("span", { className: "ps-column-resizer-dot" }),
|
|
8819
|
+
React.createElement("span", { className: "ps-column-resizer-dot" }),
|
|
8820
|
+
React.createElement("span", { className: "ps-column-resizer-dot" })));
|
|
8190
8821
|
}
|
|
8191
8822
|
|
|
8192
8823
|
|
|
8824
|
+
|
|
8193
8825
|
function ModalLayer({ controller }) {
|
|
8194
8826
|
const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
|
|
8195
8827
|
const touchScale = useControllerSelector(controller, (state) => Boolean(state.ui.touchScale));
|
|
@@ -9225,7 +9857,13 @@ function AdminConsolePanel({ controller, mobile = false }) {
|
|
|
9225
9857
|
const header = React.createElement("header", { className: "ps-admin-console__header" },
|
|
9226
9858
|
React.createElement("h2", null, "Admin Console"),
|
|
9227
9859
|
React.createElement("span", { className: "ps-admin-console__who" }, principalLabel),
|
|
9228
|
-
React.createElement("button", {
|
|
9860
|
+
React.createElement("button", {
|
|
9861
|
+
type: "button",
|
|
9862
|
+
className: "ps-mini-button is-icon",
|
|
9863
|
+
onClick: onClose,
|
|
9864
|
+
title: "Close the admin console",
|
|
9865
|
+
"aria-label": "Close the admin console",
|
|
9866
|
+
}, "\u2715"));
|
|
9229
9867
|
|
|
9230
9868
|
const ghcpSection = React.createElement(AdminGhcpSection, {
|
|
9231
9869
|
view, draftRef, onBeginEdit, onCancelEdit, onClear, onSubmit, onDraftChange, onRefresh, controller,
|
|
@@ -9312,10 +9950,15 @@ function AdminWorkersPane({ controller, view }) {
|
|
|
9312
9950
|
`${workers.summaryText}${counts.pools > 1 ? ` · ${counts.pools} pools` : ""}${counts.draining ? ` · ${counts.draining} draining` : ""}`)
|
|
9313
9951
|
: null,
|
|
9314
9952
|
React.createElement("button", {
|
|
9315
|
-
type: "button",
|
|
9316
|
-
|
|
9953
|
+
type: "button",
|
|
9954
|
+
className: "ps-mini-button is-icon",
|
|
9317
9955
|
onClick: () => controller.refreshAdminWorkers(),
|
|
9318
|
-
|
|
9956
|
+
disabled: Boolean(workers.loading),
|
|
9957
|
+
title: workers.loading ? "Refreshing…" : "Refresh the worker list",
|
|
9958
|
+
"aria-label": "Refresh the worker list",
|
|
9959
|
+
}, React.createElement("svg", { viewBox: "0 0 20 20", width: 15, height: 15, fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true" },
|
|
9960
|
+
React.createElement("path", { d: "M16.2 8.2A6.4 6.4 0 0 0 4.6 6.4M3.8 11.8a6.4 6.4 0 0 0 11.6 1.8" }),
|
|
9961
|
+
React.createElement("path", { d: "M16.4 3.6v4.6h-4.6M3.6 16.4v-4.6h4.6" })))),
|
|
9319
9962
|
workers.error
|
|
9320
9963
|
? React.createElement("div", { className: "ps-admin-console__error", role: "alert" }, workers.error)
|
|
9321
9964
|
: null,
|
|
@@ -9359,7 +10002,14 @@ function AdminSettingsTree({ controller, view }) {
|
|
|
9359
10002
|
`${row.label}`, React.createElement("span", { className: "ps-admin-tree__count" }, String(row.count ?? 0)));
|
|
9360
10003
|
}
|
|
9361
10004
|
const onClick = row.kind === "package"
|
|
9362
|
-
|
|
10005
|
+
// The selector says WHICH copy this row is — one name can be a
|
|
10006
|
+
// shared package and a user copy at once (scope shadowing).
|
|
10007
|
+
? () => controller.selectAdminPackage(row.name, {
|
|
10008
|
+
scope: row.scope === "shared" ? "shared" : "user",
|
|
10009
|
+
...(row.scope !== "shared" && row.owner?.provider && row.owner?.subject
|
|
10010
|
+
? { owner: { provider: row.owner.provider, subject: row.owner.subject } }
|
|
10011
|
+
: {}),
|
|
10012
|
+
})
|
|
9363
10013
|
: () => controller.setAdminSection(row.id === "agents" ? "packages" : row.id === "workers" ? "workers" : "ghcp");
|
|
9364
10014
|
return React.createElement("button", {
|
|
9365
10015
|
key: row.id,
|
|
@@ -9475,8 +10125,29 @@ function AdminPackageDetailPane({ controller, view }) {
|
|
|
9475
10125
|
disabled: Boolean(pending),
|
|
9476
10126
|
title: `Publish a new version of ${detail.name} from a repo folder`,
|
|
9477
10127
|
}, "Update"),
|
|
9478
|
-
|
|
9479
|
-
|
|
10128
|
+
// Promote MOVES this row to shared scope, so it only works
|
|
10129
|
+
// while the shared name is free. Once a shared copy exists,
|
|
10130
|
+
// the update path is republish: this version's exact bytes
|
|
10131
|
+
// become a new version of the existing shared package.
|
|
10132
|
+
detail.scope === "user" && detail.hasSharedCounterpart
|
|
10133
|
+
? React.createElement("button", {
|
|
10134
|
+
type: "button",
|
|
10135
|
+
className: "ps-mini-button",
|
|
10136
|
+
onClick: act("republish", detail.activeSemver),
|
|
10137
|
+
disabled: Boolean(pending) || !detail.activeSemver,
|
|
10138
|
+
title: `Publish ${detail.name}@${detail.activeSemver ?? "?"} into the existing shared package`,
|
|
10139
|
+
}, pending === "republish" ? "Publishing…" : `Publish ${detail.activeSemver ?? ""} to shared`)
|
|
10140
|
+
: React.createElement("button", { type: "button", className: "ps-mini-button", onClick: act(detail.scope === "shared" ? "demote" : "promote"), disabled: Boolean(pending) },
|
|
10141
|
+
detail.scope === "shared" ? "Demote to user" : "Promote to shared"),
|
|
10142
|
+
detail.scope === "shared" && !detail.hasOwnUserCounterpart
|
|
10143
|
+
? React.createElement("button", {
|
|
10144
|
+
type: "button",
|
|
10145
|
+
className: "ps-mini-button",
|
|
10146
|
+
onClick: act("republish", detail.activeSemver),
|
|
10147
|
+
disabled: Boolean(pending) || !detail.activeSemver,
|
|
10148
|
+
title: "Copy the active version into your user scope as a private test bench",
|
|
10149
|
+
}, pending === "republish" ? "Copying…" : "Copy to my user scope")
|
|
10150
|
+
: null,
|
|
9480
10151
|
React.createElement("button", { type: "button", className: "ps-mini-button", onClick: act(detail.enabled ? "disable" : "enable"), disabled: Boolean(pending) },
|
|
9481
10152
|
detail.enabled ? "Disable" : "Enable"),
|
|
9482
10153
|
React.createElement("button", { type: "button", className: "ps-mini-button is-danger", onClick: confirmDelete, disabled: Boolean(pending) }, "Delete"))
|
|
@@ -9951,6 +10622,11 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
9951
10622
|
activeSessionId: rootState.sessions.activeSessionId || null,
|
|
9952
10623
|
promptRows: getStatePromptRows(rootState),
|
|
9953
10624
|
rightPaneMode: rootState.ui.rightPaneMode || "panes",
|
|
10625
|
+
// The two optional desktop columns, independent of each other.
|
|
10626
|
+
canvasOpen: rootState.ui.canvasOpen === true,
|
|
10627
|
+
diagnosticsOpen: rootState.ui.diagnosticsOpen === true,
|
|
10628
|
+
canvasMaximized: rootState.ui.canvasMaximized === true,
|
|
10629
|
+
canvasZen: rootState.ui.canvasZen === true,
|
|
9954
10630
|
// Live reference — reducer updates replace the object, so
|
|
9955
10631
|
// shallow-equal sees changes and the save effect fires.
|
|
9956
10632
|
canvasPrefs: rootState.canvas?.prefs,
|
|
@@ -9959,7 +10635,11 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
9959
10635
|
sessionPaneAdjust: rootState.ui.layout?.sessionPaneAdjust ?? 0,
|
|
9960
10636
|
portalSessionColumnAdjust: rootState.ui.layout?.portalSessionColumnAdjust ?? 0,
|
|
9961
10637
|
activityPaneAdjust: rootState.ui.layout?.activityPaneAdjust ?? 0,
|
|
10638
|
+
canvasPaneAdjust: rootState.ui.layout?.canvasPaneAdjust ?? 0,
|
|
10639
|
+
diagnosticsPaneAdjust: rootState.ui.layout?.diagnosticsPaneAdjust ?? 0,
|
|
10640
|
+
diagnosticsSplitAdjust: rootState.ui.layout?.diagnosticsSplitAdjust ?? 0,
|
|
9962
10641
|
focusRegion: rootState.ui.focusRegion,
|
|
10642
|
+
requestedFocusRegion: rootState.ui.requestedFocusRegion,
|
|
9963
10643
|
inspectorTab: rootState.ui.inspectorTab,
|
|
9964
10644
|
filesFullscreen: Boolean(rootState.files.fullscreen),
|
|
9965
10645
|
selectedArtifactId: rootState.files.selectedArtifactId || null,
|
|
@@ -9975,6 +10655,9 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
9975
10655
|
// Last-known stored rightPaneMode — the desktop-owned slot a phone must
|
|
9976
10656
|
// carry through its saves untouched (see profileSettingsFromViewState).
|
|
9977
10657
|
const desktopRightPaneModeRef = React.useRef(null);
|
|
10658
|
+
// Same preservation contract as desktopRightPaneModeRef: a phone must not
|
|
10659
|
+
// overwrite the desktop's column toggles with its own (absent) ones.
|
|
10660
|
+
const desktopPanesRef = React.useRef(null);
|
|
9978
10661
|
const profileSettingsSaveTimerRef = React.useRef(null);
|
|
9979
10662
|
const profileSettingsPollTimerRef = React.useRef(null);
|
|
9980
10663
|
const profileSettingsPollInFlightRef = React.useRef(false);
|
|
@@ -9993,44 +10676,54 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
9993
10676
|
// so it follows the flip TICK instead (which fires even when the mode was
|
|
9994
10677
|
// already "canvas" — e.g. a persisted mode with the user on Main).
|
|
9995
10678
|
const canvasShellView = useControllerSelector(controller, selectCanvasView, shallowEqualObject);
|
|
9996
|
-
|
|
10679
|
+
// "canvas" is a fourth value of mobilePane, alongside workspace, activity
|
|
10680
|
+
// and inspector — the canvas is one more thing that can be showing in the
|
|
10681
|
+
// content region, not a screen layered over the app.
|
|
10682
|
+
const mobileCanvasOpen = mobilePane === "canvas";
|
|
10683
|
+
// Which pane the canvas was opened FROM. Toggling it off puts that back —
|
|
10684
|
+
// chat, activity or inspector — instead of dropping the user on Main.
|
|
10685
|
+
const paneBeforeCanvasRef = React.useRef("workspace");
|
|
9997
10686
|
// Once opened, the canvas stays MOUNTED (hidden) for the rest of the
|
|
9998
10687
|
// session selection — page state (typed input, scroll, game progress)
|
|
9999
10688
|
// survives toggling. A session switch remounts fresh via the key below.
|
|
10000
10689
|
const [mobileCanvasEverOpened, setMobileCanvasEverOpened] = React.useState(false);
|
|
10001
10690
|
const [desktopCanvasEverOpened, setDesktopCanvasEverOpened] = React.useState(false);
|
|
10002
10691
|
React.useEffect(() => {
|
|
10003
|
-
|
|
10692
|
+
setMobilePane((current) => (current === "canvas" ? (paneBeforeCanvasRef.current || "workspace") : current));
|
|
10004
10693
|
setMobileCanvasEverOpened(false);
|
|
10005
10694
|
setDesktopCanvasEverOpened(false);
|
|
10006
10695
|
}, [canvasShellView.sessionId]);
|
|
10007
|
-
// iOS scroll-chaining: while the
|
|
10696
|
+
// iOS scroll-chaining: while the canvas is up, a pan that Safari decides
|
|
10008
10697
|
// is not the iframe's must not scroll the app shell behind it.
|
|
10009
10698
|
React.useEffect(() => {
|
|
10010
10699
|
if (typeof document === "undefined") return undefined;
|
|
10011
10700
|
document.body.classList.toggle("ps-canvas-lock", Boolean(mobile && mobileCanvasOpen));
|
|
10012
10701
|
return () => document.body.classList.remove("ps-canvas-lock");
|
|
10013
10702
|
}, [mobile, mobileCanvasOpen]);
|
|
10014
|
-
const
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10703
|
+
const openMobileCanvas = React.useCallback(() => {
|
|
10704
|
+
if (mobilePane !== "canvas") paneBeforeCanvasRef.current = mobilePane;
|
|
10705
|
+
setMobilePane("canvas");
|
|
10706
|
+
setMobileCanvasEverOpened(true);
|
|
10707
|
+
// Entering by hand un-opts-out, same as the desktop toggle.
|
|
10708
|
+
controller.dispatch({ type: "ui/rightPaneMode", mode: "canvas", sessionId: canvasShellView.sessionId });
|
|
10709
|
+
}, [controller, mobilePane, canvasShellView.sessionId]);
|
|
10020
10710
|
const closeMobileCanvas = React.useCallback(() => {
|
|
10021
|
-
|
|
10711
|
+
setMobilePane(paneBeforeCanvasRef.current || "workspace");
|
|
10022
10712
|
// Mirror the desktop ✕ exactly: a deliberate close is the opt-out
|
|
10023
10713
|
// gesture — later draws badge the toggle instead of re-covering the
|
|
10024
|
-
//
|
|
10714
|
+
// transcript, and reopening clears the opt-out.
|
|
10025
10715
|
controller.dispatch({ type: "ui/rightPaneMode", mode: "panes", sessionId: canvasShellView.sessionId, manual: true });
|
|
10026
10716
|
}, [controller, canvasShellView.sessionId]);
|
|
10717
|
+
const canvasFlipSeqRef = React.useRef(canvasShellView.flipSeq);
|
|
10718
|
+
React.useEffect(() => {
|
|
10719
|
+
if (canvasShellView.flipSeq === canvasFlipSeqRef.current) return;
|
|
10720
|
+
canvasFlipSeqRef.current = canvasShellView.flipSeq;
|
|
10721
|
+
if (mobile) openMobileCanvas();
|
|
10722
|
+
}, [canvasShellView.flipSeq, mobile, openMobileCanvas]);
|
|
10027
10723
|
const toggleMobileCanvas = React.useCallback(() => {
|
|
10028
10724
|
if (mobileCanvasOpen) { closeMobileCanvas(); return; }
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
// Entering by hand un-opts-out, same as the desktop toggle.
|
|
10032
|
-
controller.dispatch({ type: "ui/rightPaneMode", mode: "canvas", sessionId: canvasShellView.sessionId });
|
|
10033
|
-
}, [mobileCanvasOpen, closeMobileCanvas, controller, canvasShellView.sessionId]);
|
|
10725
|
+
openMobileCanvas();
|
|
10726
|
+
}, [mobileCanvasOpen, closeMobileCanvas, openMobileCanvas]);
|
|
10034
10727
|
|
|
10035
10728
|
// gridViewport used to be PUBLISHED here via controller.setViewport — a
|
|
10036
10729
|
// second writer to ui.layout that derived columns from the hard-coded
|
|
@@ -10083,16 +10776,40 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
10083
10776
|
if (remoteNormalized.rightPaneMode === "canvas" || remoteNormalized.rightPaneMode === "panes") {
|
|
10084
10777
|
desktopRightPaneModeRef.current = remoteNormalized.rightPaneMode;
|
|
10085
10778
|
}
|
|
10779
|
+
// Its own check: a profile written by this build carries
|
|
10780
|
+
// desktopPanes and need not carry the legacy enum at all.
|
|
10781
|
+
if (remoteNormalized.desktopPanes) {
|
|
10782
|
+
desktopPanesRef.current = remoteNormalized.desktopPanes;
|
|
10783
|
+
}
|
|
10086
10784
|
defaultProfileSettingsRef.current = buildDefaultProfileSettingsFromState(
|
|
10087
|
-
controller.getState(), otherTouchScaleRef.current, desktopRightPaneModeRef.current,
|
|
10785
|
+
controller.getState(), otherTouchScaleRef.current, desktopRightPaneModeRef.current, desktopPanesRef.current,
|
|
10088
10786
|
);
|
|
10787
|
+
// First visit on a desktop wide enough to hold it: open the
|
|
10788
|
+
// canvas. A workspace that opens as chat alone hides the thing
|
|
10789
|
+
// most sessions are FOR, and the canvas needs real width to be
|
|
10790
|
+
// worth showing — below the breakpoint chat alone is better.
|
|
10791
|
+
//
|
|
10792
|
+
// Folded into the settings rather than dispatched separately.
|
|
10793
|
+
// A separate dispatch raced the re-baseline at the end of this
|
|
10794
|
+
// same poll: the new value was recorded as "already saved",
|
|
10795
|
+
// never written, and the next poll put it back. Going through
|
|
10796
|
+
// the settings means the normal apply-and-save path carries it.
|
|
10797
|
+
//
|
|
10798
|
+
// Only when NOTHING is stored. An explicit choice, including
|
|
10799
|
+
// closing the canvas, is a preference from then on. Not
|
|
10800
|
+
// live-responsive either: narrowing the window never closes a
|
|
10801
|
+
// canvas the user opened on purpose.
|
|
10802
|
+
const wantsDefaultCanvas = !hasStoredDesktopPanes(remoteNormalized)
|
|
10803
|
+
&& !isNarrowViewport()
|
|
10804
|
+
&& window.innerWidth >= PORTAL_CANVAS_DEFAULT_MIN_VIEWPORT_PX;
|
|
10089
10805
|
const settings = materializeProfileSettings(
|
|
10090
10806
|
profile?.profileSettings,
|
|
10091
10807
|
defaultProfileSettingsRef.current,
|
|
10092
10808
|
);
|
|
10809
|
+
if (wantsDefaultCanvas) settings.desktopPanes = { canvasOpen: true, diagnosticsOpen: false };
|
|
10093
10810
|
const settingsJson = JSON.stringify(settings);
|
|
10094
10811
|
const currentSettingsBeforeApply = profileSettingsFromViewState(
|
|
10095
|
-
controller.getState(), otherTouchScaleRef.current, desktopRightPaneModeRef.current,
|
|
10812
|
+
controller.getState(), otherTouchScaleRef.current, desktopRightPaneModeRef.current, desktopPanesRef.current,
|
|
10096
10813
|
);
|
|
10097
10814
|
const currentSettingsBeforeApplyJson = JSON.stringify(currentSettingsBeforeApply);
|
|
10098
10815
|
const hasUnpersistedLocalChange = profileSettingsHydratedRef.current
|
|
@@ -10106,6 +10823,22 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
10106
10823
|
appliedProfileSettingsJsonRef.current = settingsJson;
|
|
10107
10824
|
}
|
|
10108
10825
|
|
|
10826
|
+
// First run on this account, on a desktop wide enough to hold
|
|
10827
|
+
// it: open the canvas. A workspace that opens as chat alone
|
|
10828
|
+
// hides the thing most sessions are FOR, and the canvas needs
|
|
10829
|
+
// real width to be worth showing — below the breakpoint chat
|
|
10830
|
+
// alone is the better default.
|
|
10831
|
+
//
|
|
10832
|
+
// Once only, and only when nothing is stored. An explicit
|
|
10833
|
+
// choice, including closing it, is a stored preference from
|
|
10834
|
+
// then on and is never second-guessed. Not live-responsive
|
|
10835
|
+
// either: narrowing the window does not close a canvas the
|
|
10836
|
+
// user opened on purpose.
|
|
10837
|
+
// remoteNormalized, NOT `settings`: the merged object always
|
|
10838
|
+
// carries desktopPanes because the defaults are synthesized
|
|
10839
|
+
// from current state, so it can never say "unset" and the
|
|
10840
|
+
// default below would never fire.
|
|
10841
|
+
|
|
10109
10842
|
// lastProfileSettingsJson is what the SAVE effect diffs against
|
|
10110
10843
|
// to decide whether anything needs writing. Refreshing it on
|
|
10111
10844
|
// every poll made the poll adopt UNSAVED local state as if it
|
|
@@ -10119,7 +10852,7 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
10119
10852
|
// remote state by definition and there is nothing to lose).
|
|
10120
10853
|
if (!profileSettingsHydratedRef.current || !hasPendingLocalWrite) {
|
|
10121
10854
|
const currentSettings = profileSettingsFromViewState(
|
|
10122
|
-
controller.getState(), otherTouchScaleRef.current, desktopRightPaneModeRef.current,
|
|
10855
|
+
controller.getState(), otherTouchScaleRef.current, desktopRightPaneModeRef.current, desktopPanesRef.current,
|
|
10123
10856
|
);
|
|
10124
10857
|
lastProfileSettingsJsonRef.current = JSON.stringify(currentSettings);
|
|
10125
10858
|
}
|
|
@@ -10153,7 +10886,7 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
10153
10886
|
|
|
10154
10887
|
React.useEffect(() => {
|
|
10155
10888
|
if (!profileSettingsHydratedRef.current) return undefined;
|
|
10156
|
-
const settings = profileSettingsFromViewState(state, otherTouchScaleRef.current, desktopRightPaneModeRef.current);
|
|
10889
|
+
const settings = profileSettingsFromViewState(state, otherTouchScaleRef.current, desktopRightPaneModeRef.current, desktopPanesRef.current);
|
|
10157
10890
|
const settingsJson = JSON.stringify(settings);
|
|
10158
10891
|
if (lastProfileSettingsJsonRef.current === settingsJson) return undefined;
|
|
10159
10892
|
lastProfileSettingsJsonRef.current = settingsJson;
|
|
@@ -10195,10 +10928,20 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
10195
10928
|
// toolbar: the sessions-only layout's list reclaims focus, which
|
|
10196
10929
|
// bounced the user out of Inspector the instant they tapped it.
|
|
10197
10930
|
// Returning to Main is the Main button's job now.
|
|
10198
|
-
|
|
10931
|
+
//
|
|
10932
|
+
// Gate on the RAW request, not the normalized region: on a phone a
|
|
10933
|
+
// workspace region (sessions/chat) normalizes to inspector, so the
|
|
10934
|
+
// Main button's own setFocus("chat") would otherwise land here as an
|
|
10935
|
+
// "inspector" focus and yank the user into diagnostics the instant
|
|
10936
|
+
// they asked for the workspace. Only a focus the caller genuinely
|
|
10937
|
+
// aimed at inspector/activity switches the pane.
|
|
10938
|
+
const requested = state.requestedFocusRegion;
|
|
10939
|
+
if (mobile
|
|
10940
|
+
&& (state.focusRegion === "inspector" || state.focusRegion === "activity")
|
|
10941
|
+
&& (requested === "inspector" || requested === "activity")) {
|
|
10199
10942
|
setMobilePane(state.focusRegion);
|
|
10200
10943
|
}
|
|
10201
|
-
}, [mobile, state.focusRegion]);
|
|
10944
|
+
}, [mobile, state.focusRegion, state.requestedFocusRegion]);
|
|
10202
10945
|
|
|
10203
10946
|
React.useEffect(() => {
|
|
10204
10947
|
const visibleTabs = getVisibleInspectorTabs(controller);
|
|
@@ -10230,11 +10973,66 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
10230
10973
|
// precedence while open, and its ✕ leaves rightPaneMode untouched — so
|
|
10231
10974
|
// closing a preview returns to the canvas, restore-what-was-displaced.
|
|
10232
10975
|
const canvasModeActive = !mobile
|
|
10233
|
-
&& state.
|
|
10976
|
+
&& state.canvasOpen
|
|
10234
10977
|
&& !filesFullscreenActive;
|
|
10978
|
+
// sessionId is a dep ON PURPOSE, not just canvasModeActive. The reset
|
|
10979
|
+
// effect above wipes the gate whenever the session settles or switches —
|
|
10980
|
+
// and on a cold load the settings poll applies canvasOpen BEFORE the
|
|
10981
|
+
// sessions list has loaded, so activeSessionId lands in a LATER commit.
|
|
10982
|
+
// Keyed on canvasModeActive alone this never re-fired (the flag had not
|
|
10983
|
+
// changed), the gate stayed false, and a canvas that was open by stored
|
|
10984
|
+
// preference or by the wide-screen default simply never rendered.
|
|
10985
|
+
// Effects run in declaration order, so within one commit the reset runs
|
|
10986
|
+
// first and this one wins: active canvas => mounted canvas, always.
|
|
10235
10987
|
React.useEffect(() => {
|
|
10236
10988
|
if (canvasModeActive) setDesktopCanvasEverOpened(true);
|
|
10237
|
-
}, [canvasModeActive]);
|
|
10989
|
+
}, [canvasModeActive, canvasShellView.sessionId]);
|
|
10990
|
+
|
|
10991
|
+
// Diagnostics — Inspector + Activity as one column, beside the canvas
|
|
10992
|
+
// rather than instead of it. The artifact reader still takes the whole
|
|
10993
|
+
// right side while it is open, as it always did.
|
|
10994
|
+
const diagnosticsActive = !mobile
|
|
10995
|
+
&& state.diagnosticsOpen
|
|
10996
|
+
&& !filesFullscreenActive
|
|
10997
|
+
&& !artifactPaneActive
|
|
10998
|
+
// Zen is chat + canvas only; diagnostics keeps its stored toggle and
|
|
10999
|
+
// returns when zen ends.
|
|
11000
|
+
&& !(state.canvasZen && state.canvasOpen);
|
|
11001
|
+
// Whether the right side of the workspace exists at all. Both columns off
|
|
11002
|
+
// is the default: sessions and chat, nothing else. An absent column takes
|
|
11003
|
+
// no width and grows no resizer — off means gone, not collapsed.
|
|
11004
|
+
const rightSideActive = artifactPaneActive || canvasModeActive || diagnosticsActive;
|
|
11005
|
+
|
|
11006
|
+
// In the STORE, not local state: the header owns the way out (the rev
|
|
11007
|
+
// strip is promoted up there while full screen), and every other toolbar
|
|
11008
|
+
// button has to be able to drop it before doing its own job.
|
|
11009
|
+
const canvasMaximized = state.canvasMaximized && canvasModeActive;
|
|
11010
|
+
// Zen: chat rail + canvas workbench. Diagnostics steps aside but keeps
|
|
11011
|
+
// its stored toggle; the session list hides entirely.
|
|
11012
|
+
const zenActive = state.canvasZen && canvasModeActive && !artifactPaneActive;
|
|
11013
|
+
const zenRailPx = Math.max(260, Math.min(720,
|
|
11014
|
+
Number(state.canvasPrefs?.[state.activeSessionId]?.zenRailPx) || 380));
|
|
11015
|
+
// Escape steps down one rung: full screen -> zen -> normal workspace.
|
|
11016
|
+
React.useEffect(() => {
|
|
11017
|
+
if (!canvasMaximized && !zenActive) return undefined;
|
|
11018
|
+
const onKey = (e) => {
|
|
11019
|
+
if (e.key !== "Escape") return;
|
|
11020
|
+
controller.dispatch(canvasMaximized
|
|
11021
|
+
? { type: "ui/canvasMaximized", on: false }
|
|
11022
|
+
: { type: "ui/canvasZen", on: false });
|
|
11023
|
+
};
|
|
11024
|
+
window.addEventListener("keydown", onKey);
|
|
11025
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
11026
|
+
}, [controller, canvasMaximized, zenActive]);
|
|
11027
|
+
|
|
11028
|
+
// The right block is exactly as wide as the columns in it. Pixels, not a
|
|
11029
|
+
// share of the window: when a column closes, its pixels go to chat and
|
|
11030
|
+
// sessions — never to the surviving column. That is the whole model.
|
|
11031
|
+
const canvasColPx = Math.max(0, PORTAL_CANVAS_COL_DEFAULT_PX + (Number(state.canvasPaneAdjust) || 0));
|
|
11032
|
+
const diagColPx = Math.max(0, PORTAL_DIAG_COL_DEFAULT_PX + (Number(state.diagnosticsPaneAdjust) || 0));
|
|
11033
|
+
const rightSidePx = (canvasModeActive ? canvasColPx : 0)
|
|
11034
|
+
+ (canvasModeActive && diagnosticsActive ? 1 : 0) // the hairline seam
|
|
11035
|
+
+ (diagnosticsActive ? diagColPx : 0);
|
|
10238
11036
|
|
|
10239
11037
|
// Closing hands the column back — or collapses it again, if the user had
|
|
10240
11038
|
// already resized it away before opening the pane. Both cases live in the
|
|
@@ -10246,15 +11044,34 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
10246
11044
|
const estimatedMainGridWidth = Math.max(0, (viewport.width || 0) * 0.68);
|
|
10247
11045
|
const measuredMainGridWidth = mainGridViewport.width || estimatedMainGridWidth;
|
|
10248
11046
|
const sessionColumnWidth = portalSessionColumnWidth(measuredMainGridWidth, state.portalSessionColumnAdjust);
|
|
10249
|
-
const sessionColumnMode = portalSessionColumnMode(sessionColumnWidth);
|
|
11047
|
+
const sessionColumnMode = zenActive ? "hidden" : portalSessionColumnMode(sessionColumnWidth);
|
|
10250
11048
|
const sessionColumnTrack = sessionColumnMode === "hidden"
|
|
10251
11049
|
? "0px"
|
|
10252
11050
|
: `clamp(0px, calc(${PORTAL_SESSION_COLUMN_RATIO * 100}% + ${Number(state.portalSessionColumnAdjust) || 0}px), max(0px, calc(100% - 14rem - 32px)))`;
|
|
10253
11051
|
|
|
10254
11052
|
const desktopWorkspace = React.createElement("div", {
|
|
10255
|
-
className:
|
|
11053
|
+
className: `ps-workspace-grid${rightSideActive ? "" : " is-right-hidden"}`,
|
|
10256
11054
|
style: {
|
|
10257
|
-
|
|
11055
|
+
// With both optional columns off there is no right side and no
|
|
11056
|
+
// resizer for one: chat takes the whole width. Leaving a 0fr track
|
|
11057
|
+
// and its 16px resizer behind would put a drag handle against the
|
|
11058
|
+
// window edge that reveals nothing.
|
|
11059
|
+
//
|
|
11060
|
+
// With the canvas up the right track gets a pixel FLOOR. The fr
|
|
11061
|
+
// ratio comes from computeLegacyLayout, which was tuned when the
|
|
11062
|
+
// right column only ever held the inspector/activity split — it
|
|
11063
|
+
// left the canvas around 200px, too narrow to read a page in.
|
|
11064
|
+
// Capped at 55% so the floor can never crush chat on a narrow
|
|
11065
|
+
// window; below that the user drags, or closes a column.
|
|
11066
|
+
gridTemplateColumns: artifactPaneActive
|
|
11067
|
+
? `minmax(0, ${layout.leftWidth}fr) var(--ps-resizer-track, 16px) minmax(0, ${layout.rightWidth}fr)`
|
|
11068
|
+
: (zenActive
|
|
11069
|
+
// Zen: chat is a rail, the canvas is the workbench. The
|
|
11070
|
+
// rail width is remembered for THIS session only.
|
|
11071
|
+
? `${zenRailPx}px var(--ps-resizer-track, 16px) minmax(0, 1fr)`
|
|
11072
|
+
: (rightSideActive
|
|
11073
|
+
? `minmax(0, 1fr) var(--ps-resizer-track, 16px) min(${rightSidePx}px, 60%)`
|
|
11074
|
+
: "minmax(0, 1fr)")),
|
|
10258
11075
|
},
|
|
10259
11076
|
},
|
|
10260
11077
|
React.createElement("div", {
|
|
@@ -10282,53 +11099,102 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
10282
11099
|
style: { gridColumn: "3" },
|
|
10283
11100
|
},
|
|
10284
11101
|
React.createElement(ChatPane, { controller }))),
|
|
10285
|
-
|
|
10286
|
-
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
11102
|
+
rightSideActive
|
|
11103
|
+
? (artifactPaneActive
|
|
11104
|
+
? React.createElement(ColumnResizeHandle, { controller, paneAdjust: state.paneAdjust })
|
|
11105
|
+
: (zenActive
|
|
11106
|
+
? React.createElement(ZenRailResizeHandle, { controller, sessionId: state.activeSessionId, railPx: zenRailPx })
|
|
11107
|
+
: React.createElement(ChatRightResizeHandle, {
|
|
11108
|
+
controller,
|
|
11109
|
+
target: canvasModeActive ? "canvas" : "diagnostics",
|
|
11110
|
+
adjust: canvasModeActive ? state.canvasPaneAdjust : state.diagnosticsPaneAdjust,
|
|
11111
|
+
})))
|
|
11112
|
+
: null,
|
|
11113
|
+
// The right side of the workspace: canvas and diagnostics, side by side
|
|
11114
|
+
// and independent. Either, both, or neither.
|
|
11115
|
+
//
|
|
11116
|
+
// A flex ROW rather than more grid tracks. The outer grid's third track is
|
|
11117
|
+
// still sized against chat by the handle above, so that drag keeps meaning
|
|
11118
|
+
// exactly what it always meant: chat versus everything to its right.
|
|
11119
|
+
//
|
|
11120
|
+
// DOM order is the visual order: canvas, then diagnostics. Diagnostics is
|
|
11121
|
+
// always rightmost, whether or not the canvas is up.
|
|
11122
|
+
//
|
|
11123
|
+
// The artifact reader still takes the WHOLE right side while it is open —
|
|
11124
|
+
// canvas, inspector and activity all step aside — which is why
|
|
11125
|
+
// diagnosticsActive already excludes it.
|
|
11126
|
+
React.createElement("div", { className: `ps-workspace-column-host is-split${canvasMaximized ? " has-maximized" : ""}` },
|
|
10290
11127
|
artifactPaneActive
|
|
10291
11128
|
? React.createElement("div", { className: "ps-workspace-column is-artifact" },
|
|
10292
11129
|
React.createElement(ArtifactTakeoverPane, { controller, onClose: closeArtifactPane }))
|
|
10293
|
-
:
|
|
10294
|
-
|
|
10295
|
-
|
|
10296
|
-
|
|
10297
|
-
|
|
10298
|
-
|
|
10299
|
-
style: { gridRow: "1" },
|
|
10300
|
-
},
|
|
10301
|
-
!layout.inspectorHidden ? React.createElement(InspectorPane, { controller, mobile: false }) : null),
|
|
10302
|
-
React.createElement("div", {
|
|
10303
|
-
style: {
|
|
10304
|
-
gridRow: "2",
|
|
10305
|
-
minHeight: 0,
|
|
10306
|
-
display: "flex",
|
|
10307
|
-
flexDirection: "column",
|
|
10308
|
-
},
|
|
10309
|
-
},
|
|
10310
|
-
React.createElement(ActivityRowResizeHandle, { controller, activityPaneAdjust: state.activityPaneAdjust })),
|
|
10311
|
-
React.createElement("div", {
|
|
10312
|
-
className: "ps-workspace-pane-slot",
|
|
10313
|
-
style: { gridRow: "3" },
|
|
10314
|
-
},
|
|
10315
|
-
// The artifact preview takes over the activity slot while an artifact
|
|
10316
|
-
// is selected, so it inherits the row resizer and can be sized freely.
|
|
10317
|
-
// It yields back to Activity the moment the selection clears.
|
|
10318
|
-
artifactPreviewDetached
|
|
10319
|
-
? React.createElement(FilesPane, { controller, focused: false, previewOnly: true })
|
|
10320
|
-
: (!layout.activityHidden ? React.createElement(ActivityPane, { controller }) : null))),
|
|
10321
|
-
// The canvas keeps its DOCUMENT alive across toggles: once opened for
|
|
10322
|
-
// this session it stays mounted in a hidden layer — unmounting destroys
|
|
10323
|
-
// typed input, scroll position, and any running page. Session switches
|
|
10324
|
-
// remount fresh via the key.
|
|
11130
|
+
: null,
|
|
11131
|
+
// The canvas keeps its DOCUMENT alive across toggles: once opened for this
|
|
11132
|
+
// session it stays mounted in a hidden layer — unmounting destroys typed
|
|
11133
|
+
// input, scroll position, and any running page. Session switches remount
|
|
11134
|
+
// fresh via the key. It is FIRST in the DOM so it sits left of
|
|
11135
|
+
// diagnostics; while hidden it is display:none and takes no width.
|
|
10325
11136
|
desktopCanvasEverOpened
|
|
10326
11137
|
? React.createElement("div", {
|
|
10327
11138
|
key: `canvas:${state.activeSessionId || ""}`,
|
|
10328
|
-
className: `ps-workspace-column is-artifact ps-canvas-layer${canvasModeActive && !artifactPaneActive ? "" : " is-hidden"}`,
|
|
11139
|
+
className: `ps-workspace-column is-artifact ps-canvas-layer${canvasModeActive && !artifactPaneActive ? "" : " is-hidden"}${canvasMaximized && canvasModeActive && !artifactPaneActive ? " is-maximized" : ""}`,
|
|
11140
|
+
// Sharing the right side: canvas takes a fixed slice and
|
|
11141
|
+
// diagnostics absorbs the rest, so the handle between them moves
|
|
11142
|
+
// one number. Alone, or full screen, it simply fills its box.
|
|
11143
|
+
style: canvasModeActive && !artifactPaneActive && diagnosticsActive && !canvasMaximized
|
|
11144
|
+
? { flex: `0 0 ${canvasColPx}px`, minWidth: 0 }
|
|
11145
|
+
: undefined,
|
|
10329
11146
|
...(canvasModeActive && !artifactPaneActive ? {} : { inert: true, "aria-hidden": "true" }),
|
|
10330
11147
|
},
|
|
10331
|
-
React.createElement(CanvasPane, {
|
|
11148
|
+
React.createElement(CanvasPane, {
|
|
11149
|
+
controller,
|
|
11150
|
+
visible: canvasModeActive && !artifactPaneActive,
|
|
11151
|
+
maximized: canvasMaximized,
|
|
11152
|
+
}))
|
|
11153
|
+
: null,
|
|
11154
|
+
// Only when BOTH are up. With one column the outer chat/right handle is
|
|
11155
|
+
// already the only split there is, and a second handle would resize
|
|
11156
|
+
// nothing.
|
|
11157
|
+
canvasModeActive && diagnosticsActive
|
|
11158
|
+
? React.createElement(CanvasDiagnosticsResizeHandle, { controller, canvasPaneAdjust: state.canvasPaneAdjust, diagnosticsPaneAdjust: state.diagnosticsPaneAdjust })
|
|
11159
|
+
: null,
|
|
11160
|
+
// Diagnostics: yesterday's inspector-over-activity column, unchanged
|
|
11161
|
+
// inside, now behind its own toggle and rightmost.
|
|
11162
|
+
diagnosticsActive
|
|
11163
|
+
? React.createElement("div", {
|
|
11164
|
+
className: "ps-workspace-column",
|
|
11165
|
+
style: {
|
|
11166
|
+
// Continuous PIXEL split, minmax-clamped so neither pane can
|
|
11167
|
+
// be dragged away or collapse behind your back. The old
|
|
11168
|
+
// `${rows}fr` template rode the TUI's row-quantized layout —
|
|
11169
|
+
// ~20px notches and threshold-triggered collapse flips.
|
|
11170
|
+
// minmax(0,...): either pane may be dragged fully shut and
|
|
11171
|
+
// pulled back open with the same seam. The handle clamps the
|
|
11172
|
+
// adjust to the column's real height at drag time, so the bar
|
|
11173
|
+
// tracks the pointer 1:1 instead of detaching at a floor.
|
|
11174
|
+
gridTemplateRows: `minmax(0px, calc(50% + ${Math.round(Number(state.diagnosticsSplitAdjust) || 0)}px)) var(--ps-resizer-track, 16px) minmax(0px, 1fr)`,
|
|
11175
|
+
...(canvasModeActive ? { flex: "1 1 0%", minWidth: 0 } : null),
|
|
11176
|
+
},
|
|
11177
|
+
},
|
|
11178
|
+
React.createElement("div", {
|
|
11179
|
+
className: "ps-workspace-pane-slot",
|
|
11180
|
+
style: { gridRow: "1" },
|
|
11181
|
+
},
|
|
11182
|
+
React.createElement(InspectorPane, { controller, mobile: false })),
|
|
11183
|
+
React.createElement("div", {
|
|
11184
|
+
style: { gridRow: "2", minHeight: 0, display: "flex", flexDirection: "column" },
|
|
11185
|
+
},
|
|
11186
|
+
React.createElement(DiagnosticsSplitResizeHandle, { controller, splitAdjust: state.diagnosticsSplitAdjust })),
|
|
11187
|
+
React.createElement("div", {
|
|
11188
|
+
className: "ps-workspace-pane-slot",
|
|
11189
|
+
style: { gridRow: "3" },
|
|
11190
|
+
},
|
|
11191
|
+
// The artifact preview takes over the activity slot while an
|
|
11192
|
+
// artifact is selected, so it inherits the row resizer and can
|
|
11193
|
+
// be sized freely. It yields back to Activity the moment the
|
|
11194
|
+
// selection clears.
|
|
11195
|
+
artifactPreviewDetached
|
|
11196
|
+
? React.createElement(FilesPane, { controller, focused: false, previewOnly: true })
|
|
11197
|
+
: React.createElement(ActivityPane, { controller })))
|
|
10332
11198
|
: null));
|
|
10333
11199
|
const fullscreenWorkspace = React.createElement("div", { className: "ps-workspace-full" },
|
|
10334
11200
|
React.createElement(InspectorPane, { controller, mobile: false }));
|
|
@@ -10352,8 +11218,25 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
10352
11218
|
React.createElement(InspectorPane, { controller, mobile: true }));
|
|
10353
11219
|
else if (mobilePane === "activity") mobileContent = React.createElement("div", { className: "ps-mobile-pane-fill" },
|
|
10354
11220
|
React.createElement(ActivityPane, { controller }));
|
|
11221
|
+
// The canvas fills the whole region, so there is nothing to render under
|
|
11222
|
+
// it — the layer below is a SIBLING of this content and covers it. Drawing
|
|
11223
|
+
// a pane here would only be invisible work.
|
|
11224
|
+
else if (mobilePane === "canvas") mobileContent = null;
|
|
10355
11225
|
else mobileContent = React.createElement(MobileWorkspace, { controller, layout: mobileMainLayout });
|
|
10356
11226
|
|
|
11227
|
+
// The phone's canvas layer: a sibling of the content region's pane, NOT a
|
|
11228
|
+
// child of any pane. That is deliberate — panes mount and unmount as the
|
|
11229
|
+
// toolbar switches between Main, Activity and Inspector, and the canvas
|
|
11230
|
+
// frame has to survive all of it. Only a session change (which rekeys it)
|
|
11231
|
+
// or a reload may take a running page down.
|
|
11232
|
+
const mobileCanvasLayer = mobile && mobileCanvasEverOpened
|
|
11233
|
+
? React.createElement(MobileCanvasLayer, {
|
|
11234
|
+
key: `canvas:${canvasShellView.sessionId || ""}`,
|
|
11235
|
+
controller,
|
|
11236
|
+
visible: mobileCanvasOpen,
|
|
11237
|
+
})
|
|
11238
|
+
: null;
|
|
11239
|
+
|
|
10357
11240
|
return React.createElement(ControllerContext.Provider, { value: controller },
|
|
10358
11241
|
React.createElement("div", { ref: viewportRef, className: "ps-web-shell" },
|
|
10359
11242
|
// The phone's toolbar carries its NAVIGATION (the three view modes,
|
|
@@ -10362,13 +11245,14 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
10362
11245
|
React.createElement(Toolbar, {
|
|
10363
11246
|
controller,
|
|
10364
11247
|
mobile,
|
|
10365
|
-
|
|
10366
|
-
|
|
11248
|
+
canvasPaneOpen: mobileCanvasOpen,
|
|
11249
|
+
onToggleCanvasPane: toggleMobileCanvas,
|
|
10367
11250
|
mobilePane,
|
|
10368
11251
|
mobileMainLayout,
|
|
10369
11252
|
onSelectMobilePane: (paneId, focus) => {
|
|
10370
11253
|
// Tapping Main while already on Main cycles its layout; from
|
|
10371
|
-
// another pane it just returns you to
|
|
11254
|
+
// another pane (the canvas included) it just returns you to
|
|
11255
|
+
// Main, unchanged.
|
|
10372
11256
|
if (paneId === "workspace" && mobilePane === "workspace") {
|
|
10373
11257
|
setMobileMainLayout((current) => (
|
|
10374
11258
|
current === "split" ? "chat" : current === "chat" ? "sessions" : "split"
|
|
@@ -10383,17 +11267,10 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
10383
11267
|
? React.createElement(AdminConsolePanel, { controller, mobile })
|
|
10384
11268
|
: (filesFullscreenActive
|
|
10385
11269
|
? fullscreenWorkspace
|
|
10386
|
-
: (mobile ? mobileContent : desktopWorkspace))
|
|
11270
|
+
: (mobile ? mobileContent : desktopWorkspace)),
|
|
11271
|
+
mobileCanvasLayer),
|
|
10387
11272
|
mobile && filesFullscreenActive
|
|
10388
11273
|
? React.createElement(MobileArtifactOverlay, { controller })
|
|
10389
11274
|
: null,
|
|
10390
|
-
mobile && mobileCanvasEverOpened
|
|
10391
|
-
? React.createElement(MobileCanvasOverlay, {
|
|
10392
|
-
key: `canvas:${canvasShellView.sessionId || ""}`,
|
|
10393
|
-
controller,
|
|
10394
|
-
onClose: closeMobileCanvas,
|
|
10395
|
-
visible: mobileCanvasOpen,
|
|
10396
|
-
})
|
|
10397
|
-
: null,
|
|
10398
11275
|
React.createElement(ModalLayer, { controller })));
|
|
10399
11276
|
}
|