pilotswarm 0.5.32 → 0.5.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/tui/src/app.js +10 -0
- package/tui/src/node-sdk-transport.js +11 -0
- package/ui/core/src/controller.js +381 -12
- package/ui/core/src/reducer.js +125 -28
- package/ui/core/src/selectors.js +158 -268
- package/ui/core/src/state.js +6 -1
- package/ui/core/src/themes/catppuccin-mocha.js +2 -1
- package/ui/core/src/themes/cobalt2.js +2 -1
- package/ui/core/src/themes/doom.js +2 -1
- package/ui/core/src/themes/dracula.js +2 -1
- package/ui/core/src/themes/github-dark.js +2 -1
- package/ui/core/src/themes/github-light.js +2 -1
- package/ui/core/src/themes/gruvbox-dark.js +2 -1
- package/ui/core/src/themes/hacker-x-matrix.js +2 -1
- package/ui/core/src/themes/hacker-x-orion-prime.js +2 -1
- package/ui/core/src/themes/helpers.js +22 -4
- package/ui/core/src/themes/high-contrast-mono.js +2 -1
- package/ui/core/src/themes/index.js +24 -5
- package/ui/core/src/themes/ms-dos.js +2 -1
- package/ui/core/src/themes/noctis-obscuro.js +2 -1
- package/ui/core/src/themes/noctis.js +2 -1
- package/ui/core/src/themes/rust.js +125 -0
- package/ui/core/src/themes/spongebob.js +110 -0
- package/ui/core/src/themes/terminal-green.js +2 -1
- package/ui/core/src/themes/win95.js +2 -1
- package/ui/core/src/themes/winamp.js +2 -1
- package/ui/core/src/themes/workspace-dark.js +2 -1
- package/ui/react/src/web-app.js +240 -266
- package/web/dist/assets/{abnfDiagram-VRR7QNED-mXFQ4a3c.js → abnfDiagram-VRR7QNED-BFOimVWK.js} +1 -1
- package/web/dist/assets/{arc-BPuuvbge.js → arc-uUHjvwEo.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-Dt4uUYIe.js → architectureDiagram-ZJ3FMSHR-CxeX_vea.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-Ax8Q22vV.js → blockDiagram-677ZJIJ3-Cno8kltG.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-CPiukyHt.js → c4Diagram-LMCZKHZV-viONVybf.js} +1 -1
- package/web/dist/assets/channel-IURmSsjn.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-VJLqCZJX.js → chunk-2Q5K7J3B-CKfICIGD.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-DqOshxhf.js → chunk-32BRIVSS-CxmwEY56.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-KPqV21jV.js → chunk-5VM5RSS4-_eVYLGll.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-DfhiCVl_.js → chunk-EX3LRPZG-C4aqbjrf.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-H6khJPuW.js → chunk-JWPE2WC7-Bh8ZxrI8.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-BKMAzQxj.js → chunk-MOJQB5TN-BXgLlWVP.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-DNXRTxtj.js → chunk-RYQCIY6F-B1rMIdfF.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-B56PM6Gv.js → chunk-V7JOEXUC-C3AnSHXk.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-Boi3hGkB.js → chunk-VR4S4FIN-CeoMJIBs.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-DmJyn890.js → chunk-XXDRQBXY-DzaxQ9iP.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-FsfpM3iu.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-FsfpM3iu.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-BRdFlFWt.js → cose-bilkent-JH36ORCC-G46fvUOx.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-DqE-xib2.js → cynefin-VYW2F7L2-DXwqVhWA.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-D-QBZjIL.js → cynefinDiagram-TSTJHNR4-DNNFmaXb.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-DYu9oQV6.js → dagre-VKFMJZFB-BCfh-h3o.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-DOwYccCg.js → diagram-FQU43EPY-CyG9b6bj.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-DTSEmrPr.js → diagram-G47NLZAW-DdxDahK3.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-OqbzzquA.js → diagram-NH7WQ7WH-CjrZ_eja.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-BMVG6A8I.js → diagram-OA4YK3LP-5NAjQpk2.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-Rn2Q5xaz.js → diagram-WEI45ONY-fLuDb54-.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-aL32_v5y.js → ebnfDiagram-CCIWWBDH-DL9qnZpa.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-DKCpyczx.js → erDiagram-Q63AITRT-C_ynfrWY.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-CrZTPgR_.js → flowDiagram-23GEKE2U-dYuH8HYv.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-DnSE1OtC.js → ganttDiagram-NO4QXBWP-e8Meh3f3.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-C5SeYPUR.js → gitGraphDiagram-IHSO6WYX-D_tZ4VL9.js} +1 -1
- package/web/dist/assets/index-Ct7SQv1M.css +1 -0
- package/web/dist/assets/{index-B-TEKrRB.js → index-DcQes9h8.js} +6 -6
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-DDnJZp0X.js → infoDiagram-FWYZ7A6U-Dffhqp73.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-ewhzfFkq.js → ishikawaDiagram-FXEZZL3T-DYt9H7kU.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-T0iWWkLM.js → journeyDiagram-5HDEW3XC-Qv1vVu0F.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-D1MFQZjb.js → kanban-definition-HUTT4EX6-CTAUo40G.js} +1 -1
- package/web/dist/assets/{linear-BKfPK9wE.js → linear-CqLsWFLj.js} +1 -1
- package/web/dist/assets/{mermaid.core-CyzD7dTc.js → mermaid.core-okRCGPQQ.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-CWXtcTlP.js → mindmap-definition-LN4V7U3C-DVOOrIwD.js} +1 -1
- package/web/dist/assets/{papaparse.min-DLUoV5k5.js → papaparse.min-Ddflu8oC.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-BFjZun-r.js → pegDiagram-2B236MQR-BLJlC0MZ.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-DMDtW4g7.js → pieDiagram-ENE6RG2P-DdOwICOg.js} +1 -1
- package/web/dist/assets/pilotswarm-CKh2MxfL.js +99 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-CAwPVaYL.js → quadrantDiagram-ABIIQ3AL-B0Th1YO5.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-Co102PkQ.js → railroadDiagram-RFXS5EU6-BF2XKMKf.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-D19VlRSl.js → requirementDiagram-TGXJPOKE-Mr1iSwbS.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-Sd_a3ukL.js → sankeyDiagram-HTMAVEWB-CrOjVZyg.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CojDRK1c.js → sequenceDiagram-DBY2YBRQ-H0X4Lydk.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-XqXcxnDo.js → sizeCapture-X5ZJPWSS-Dus1QEqh.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-BcN_w699.js → stateDiagram-2N3HPSRC-BOBUSwdR.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DABTDwFh.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-BcO6ai_I.js → swimlanes-5IMT3BWC-BoqT-Lh7.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BF5mFN4t.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-DOPfYxUq.js → timeline-definition-FHXFAJF6-BOrqUQHX.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-BSKLkEOC.js → vennDiagram-L72KCM5P-DDkNL-Oo.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-BdcYAVw2.js → wardleyDiagram-EHGQE667-XUAJQ2X0.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-C6EJT4jB.js → xychartDiagram-FW5EYKEG-ph69PV9y.js} +1 -1
- package/web/dist/index.html +3 -3
- package/ui/core/src/themes/dark-high-contrast.js +0 -56
- package/ui/core/src/themes/workspace-dark-rich.js +0 -65
- package/web/dist/assets/channel-BCzDctYg.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-ChqjVZil.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-ChqjVZil.js +0 -1
- package/web/dist/assets/index-D2B5bzSN.css +0 -1
- package/web/dist/assets/pilotswarm-DA0u7Cq_.js +0 -99
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-BV_w1D3C.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BnYdP0qP.js +0 -8
package/ui/react/src/web-app.js
CHANGED
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
selectArtifactPickerModal,
|
|
29
29
|
selectArtifactUploadModal,
|
|
30
30
|
selectLiveActivityLines,
|
|
31
|
-
selectChatBlocks,
|
|
32
31
|
selectChatLines,
|
|
33
32
|
selectChatPaneChrome,
|
|
34
33
|
selectOutboxOverlayLines,
|
|
@@ -49,6 +48,7 @@ import {
|
|
|
49
48
|
selectSessionGroupPickerModal,
|
|
50
49
|
selectSessionOwnerFilterModal,
|
|
51
50
|
selectSessionRows,
|
|
51
|
+
selectSessionStatusSummary,
|
|
52
52
|
selectStatusBar,
|
|
53
53
|
selectThemePickerModal,
|
|
54
54
|
selectConfirmModal,
|
|
@@ -199,6 +199,9 @@ function normalizeProfileSettings(settings) {
|
|
|
199
199
|
if (typeof candidate.themeId === "string" && candidate.themeId.trim()) {
|
|
200
200
|
normalized.themeId = candidate.themeId.trim();
|
|
201
201
|
}
|
|
202
|
+
if (typeof candidate.touchScale === "boolean") {
|
|
203
|
+
normalized.touchScale = candidate.touchScale;
|
|
204
|
+
}
|
|
202
205
|
if (hasOwn(candidate, "sessionOwnerFilter") && candidate.sessionOwnerFilter && typeof candidate.sessionOwnerFilter === "object") {
|
|
203
206
|
const storedFilter = candidate.sessionOwnerFilter;
|
|
204
207
|
// Profiles saved before the "Shared with me" bucket existed have no
|
|
@@ -287,6 +290,7 @@ function hasOwn(value, key) {
|
|
|
287
290
|
function profileSettingsFromViewState(state, preservedOtherChatViewMode = null) {
|
|
288
291
|
return normalizeProfileSettings({
|
|
289
292
|
themeId: state.themeId,
|
|
293
|
+
touchScale: state.touchScale,
|
|
290
294
|
sessionOwnerFilter: state.ownerFilter,
|
|
291
295
|
layoutAdjustments: {
|
|
292
296
|
paneAdjust: state.paneAdjust,
|
|
@@ -311,6 +315,7 @@ function profileSettingsFromViewState(state, preservedOtherChatViewMode = null)
|
|
|
311
315
|
function buildDefaultProfileSettingsFromState(state, preservedOtherChatViewMode = null) {
|
|
312
316
|
return normalizeProfileSettings({
|
|
313
317
|
themeId: state?.ui?.themeId,
|
|
318
|
+
touchScale: state?.ui?.touchScale,
|
|
314
319
|
// Derive the owner-filter default from the RESOLVED principal, never a
|
|
315
320
|
// snapshot of state.sessions.ownerFilter — at mount that snapshot is
|
|
316
321
|
// still {all:true} (principal not yet applied), and using it as the
|
|
@@ -593,6 +598,15 @@ function compactTitleRuns(title, maxWidth = 40) {
|
|
|
593
598
|
return compactRuns.length > 0 ? compactRuns : title;
|
|
594
599
|
}
|
|
595
600
|
|
|
601
|
+
function applyTouchScale(enabled) {
|
|
602
|
+
if (typeof document === "undefined") return;
|
|
603
|
+
// One attribute, one CSS block. Everything the scale touches — type ramp,
|
|
604
|
+
// control padding, hit targets — is already expressed as a custom property
|
|
605
|
+
// or a class the stylesheet owns, so this needs no per-component wiring.
|
|
606
|
+
if (enabled) document.documentElement.dataset.psTouch = "1";
|
|
607
|
+
else delete document.documentElement.dataset.psTouch;
|
|
608
|
+
}
|
|
609
|
+
|
|
596
610
|
function applyDocumentTheme(themeId) {
|
|
597
611
|
const theme = getTheme(themeId);
|
|
598
612
|
if (!theme || typeof document === "undefined") return;
|
|
@@ -2508,108 +2522,6 @@ function StructuredChatBlocks({ lines, theme, controller = null }) {
|
|
|
2508
2522
|
return React.createElement(StructuredBlockList, { blocks, theme, controller });
|
|
2509
2523
|
}
|
|
2510
2524
|
|
|
2511
|
-
// Rich (desktop-style) chat transcript. Consumes selectChatBlocks: plain
|
|
2512
|
-
// user/assistant prose renders as proportional-type markdown messages;
|
|
2513
|
-
// every other block (splash, thinking cards, system cards, epoch dividers)
|
|
2514
|
-
// reuses the structured line renderer so the terminal-era special cases
|
|
2515
|
-
// keep exactly one implementation.
|
|
2516
|
-
/**
|
|
2517
|
-
* One rich message, memoized.
|
|
2518
|
-
*
|
|
2519
|
-
* The body runs markdown through MarkdownPreviewContent — the single most
|
|
2520
|
-
* expensive thing in the transcript — and a pane resize used to re-run it for
|
|
2521
|
-
* every loaded message. `selectChatBlocks` now returns identity-stable blocks,
|
|
2522
|
-
* so this bails out instead. Every prop must stay referentially stable for
|
|
2523
|
-
* that to hold: `theme` comes from a Map, `controller` is a singleton, and
|
|
2524
|
-
* `continued` is hoisted to a boolean because it depends on the PREVIOUS block
|
|
2525
|
-
* and would otherwise have to be recomputed inside.
|
|
2526
|
-
*/
|
|
2527
|
-
const RichChatMessage = React.memo(function RichChatMessage({ block, theme, controller, continued }) {
|
|
2528
|
-
const header = block.header || {};
|
|
2529
|
-
const roleClass = block.role === "user" ? " is-user" : " is-assistant";
|
|
2530
|
-
const pendingClass = block.pendingPhase ? ` is-${block.pendingPhase}` : "";
|
|
2531
|
-
const text = String(block.text || "");
|
|
2532
|
-
// A speaker label only earns its place when it disambiguates: another human
|
|
2533
|
-
// in a shared session. The viewer's own turns and the agent's are obvious
|
|
2534
|
-
// from side and styling.
|
|
2535
|
-
const showLabel = block.role === "user" && header.fromOtherPerson;
|
|
2536
|
-
return React.createElement("article", {
|
|
2537
|
-
className: `ps-rich-msg${roleClass}${pendingClass}${continued ? " is-continued" : ""}`,
|
|
2538
|
-
},
|
|
2539
|
-
React.createElement("header", { className: "ps-rich-msg-head" },
|
|
2540
|
-
showLabel
|
|
2541
|
-
? React.createElement("span", {
|
|
2542
|
-
className: "ps-rich-msg-label",
|
|
2543
|
-
style: { color: resolveColor(theme, header.roleColor) || undefined },
|
|
2544
|
-
}, header.roleLabel)
|
|
2545
|
-
: null,
|
|
2546
|
-
// Delivery glyph + timestamp are on-demand detail:
|
|
2547
|
-
// revealed on hover so a quiet transcript stays quiet.
|
|
2548
|
-
React.createElement("span", { className: "ps-rich-msg-meta" },
|
|
2549
|
-
header.glyph
|
|
2550
|
-
? React.createElement("span", {
|
|
2551
|
-
className: "ps-rich-msg-glyph",
|
|
2552
|
-
title: block.pendingPhase || undefined,
|
|
2553
|
-
style: { color: resolveColor(theme, header.glyphColor) || undefined },
|
|
2554
|
-
}, header.glyph)
|
|
2555
|
-
: null,
|
|
2556
|
-
header.time
|
|
2557
|
-
? React.createElement("span", { className: "ps-rich-msg-time" }, header.time)
|
|
2558
|
-
: null)),
|
|
2559
|
-
text.trim()
|
|
2560
|
-
? React.createElement("div", { className: "ps-rich-msg-body" },
|
|
2561
|
-
React.createElement(MarkdownPreviewContent, { content: text, theme }))
|
|
2562
|
-
: null,
|
|
2563
|
-
block.attachments
|
|
2564
|
-
? React.createElement(ArtifactImageStrip, {
|
|
2565
|
-
controller,
|
|
2566
|
-
sessionId: block.attachments.sessionId,
|
|
2567
|
-
attachments: block.attachments.attachments,
|
|
2568
|
-
})
|
|
2569
|
-
: null);
|
|
2570
|
-
});
|
|
2571
|
-
|
|
2572
|
-
/** A non-message block (splash, system card, divider), memoized the same way. */
|
|
2573
|
-
const RichChatLineBlock = React.memo(function RichChatLineBlock({ block, theme, controller }) {
|
|
2574
|
-
const lines = React.useMemo(() => normalizeLines(block.lines || []), [block.lines]);
|
|
2575
|
-
return React.createElement("div", {
|
|
2576
|
-
className: `ps-rich-lineblock is-${block.variant || "event"}`,
|
|
2577
|
-
}, React.createElement(StructuredChatBlocks, { lines, theme, controller }));
|
|
2578
|
-
});
|
|
2579
|
-
|
|
2580
|
-
function RichChatBlocks({ blocks, theme, controller = null }) {
|
|
2581
|
-
return React.createElement("div", { className: "ps-rich-chat" },
|
|
2582
|
-
(blocks || []).map((block, index) => {
|
|
2583
|
-
if (block.kind === "message") {
|
|
2584
|
-
// Consecutive turns from the same speaker read as one
|
|
2585
|
-
// continuous passage — no repeated header, tighter spacing.
|
|
2586
|
-
// Computed here because it reads the previous block; passing it
|
|
2587
|
-
// down as a boolean is what keeps the child memoizable.
|
|
2588
|
-
const previous = blocks[index - 1];
|
|
2589
|
-
const header = block.header || {};
|
|
2590
|
-
const showLabel = block.role === "user" && header.fromOtherPerson;
|
|
2591
|
-
const continued = Boolean(previous
|
|
2592
|
-
&& previous.kind === "message"
|
|
2593
|
-
&& previous.role === block.role
|
|
2594
|
-
&& !(previous.header && previous.header.fromOtherPerson)
|
|
2595
|
-
&& !showLabel);
|
|
2596
|
-
return React.createElement(RichChatMessage, {
|
|
2597
|
-
key: block.id != null ? `msg:${block.id}` : `msg:${index}`,
|
|
2598
|
-
block,
|
|
2599
|
-
theme,
|
|
2600
|
-
controller,
|
|
2601
|
-
continued,
|
|
2602
|
-
});
|
|
2603
|
-
}
|
|
2604
|
-
return React.createElement(RichChatLineBlock, {
|
|
2605
|
-
key: `lines:${index}`,
|
|
2606
|
-
block,
|
|
2607
|
-
theme,
|
|
2608
|
-
controller,
|
|
2609
|
-
});
|
|
2610
|
-
}));
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
2525
|
// Authenticated artifact thumbnails: bytes are fetched through the transport
|
|
2614
2526
|
// (Bearer token) and served to <img> as object URLs. Module-level cache so
|
|
2615
2527
|
// scrolling the transcript doesn't refetch; oldest entries are revoked at cap.
|
|
@@ -2820,19 +2732,6 @@ function StructuredBlockList({ blocks, theme, controller = null }) {
|
|
|
2820
2732
|
}));
|
|
2821
2733
|
}
|
|
2822
2734
|
|
|
2823
|
-
// Pane titles carry terminal-style bracketed runs (" [+3]", " [5f086c7c]").
|
|
2824
|
-
// The rich view keeps the information but drops the brackets — a web title
|
|
2825
|
-
// bar shows a count and an id, it does not draw them in ASCII delimiters.
|
|
2826
|
-
function unbracketTitleRuns(runs) {
|
|
2827
|
-
if (!Array.isArray(runs)) return runs;
|
|
2828
|
-
return runs.map((run) => {
|
|
2829
|
-
const text = String(run?.text ?? "");
|
|
2830
|
-
const match = /^(\s*)\[(.+)\](\s*)$/.exec(text);
|
|
2831
|
-
if (!match) return run;
|
|
2832
|
-
return { ...run, text: `${match[1]}${match[2]}${match[3]}` };
|
|
2833
|
-
});
|
|
2834
|
-
}
|
|
2835
|
-
|
|
2836
2735
|
function Panel({ title, titleRight = null, color = "gray", focused = false, actions = null, children, theme, className = "" }) {
|
|
2837
2736
|
const accent = resolveColor(theme, color);
|
|
2838
2737
|
// A panel with nothing to put in its header should not render one. Nested
|
|
@@ -3118,7 +3017,6 @@ function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, l
|
|
|
3118
3017
|
// title in proportional type, and the id/age/model metadata demoted to a
|
|
3119
3018
|
// muted second line under the selected row. Depth becomes real indentation
|
|
3120
3019
|
// with a guide rail rather than an ASCII "└" prefix.
|
|
3121
|
-
const SESSION_KIND_GLYPHS = { group: "🗂", system: "⚙", service: "⚗" };
|
|
3122
3020
|
|
|
3123
3021
|
/**
|
|
3124
3022
|
* Selected-session details, pinned to the bottom of the Sessions panel.
|
|
@@ -3188,6 +3086,8 @@ function SessionDetailBox({ session, childCount = 0 }) {
|
|
|
3188
3086
|
? (session.memberCount == null ? null : String(session.memberCount))
|
|
3189
3087
|
: (childCount > 0 ? String(childCount) : null);
|
|
3190
3088
|
|
|
3089
|
+
const statusSummary = selectSessionStatusSummary(session);
|
|
3090
|
+
|
|
3191
3091
|
return React.createElement("div", { className: "ps-session-detail-box" },
|
|
3192
3092
|
// The row above ellipsizes to one line, so this is the only place the
|
|
3193
3093
|
// whole name is legible. Two lines are RESERVED whether or not they are
|
|
@@ -3198,7 +3098,10 @@ function SessionDetailBox({ session, childCount = 0 }) {
|
|
|
3198
3098
|
field("Context", context, percent != null && percent >= 85 ? "is-hot" : percent != null && percent >= 70 ? "is-warm" : ""),
|
|
3199
3099
|
field("Cron", cron, session?.cronActive === true ? "is-armed" : ""),
|
|
3200
3100
|
field("Agent", session?.agentId),
|
|
3201
|
-
|
|
3101
|
+
// "when, and what it is doing" in one row. The status comes from the
|
|
3102
|
+
// shared derivation rather than the raw field — see
|
|
3103
|
+
// selectSessionStatusSummary for why raw made this flip idle/waiting.
|
|
3104
|
+
field("Updated", statusSummary ? `${statusSummary.relative} (${statusSummary.status})` : null),
|
|
3202
3105
|
field("Children", children),
|
|
3203
3106
|
field("Access", access));
|
|
3204
3107
|
}
|
|
@@ -3248,7 +3151,7 @@ function railNodeInsetPx(depth) {
|
|
|
3248
3151
|
}
|
|
3249
3152
|
|
|
3250
3153
|
const SessionListRow = React.memo(function SessionListRow({
|
|
3251
|
-
row, theme,
|
|
3154
|
+
row, theme, structuredRows, mobile, onRowClick, setRef, drag,
|
|
3252
3155
|
}) {
|
|
3253
3156
|
const depth = Math.max(0, row.depth);
|
|
3254
3157
|
const railStyle = React.useMemo(() => {
|
|
@@ -3340,77 +3243,18 @@ const SessionListRow = React.memo(function SessionListRow({
|
|
|
3340
3243
|
},
|
|
3341
3244
|
},
|
|
3342
3245
|
React.createElement("div", {
|
|
3343
|
-
className:
|
|
3246
|
+
className: "ps-line ps-session-row-content",
|
|
3344
3247
|
style: {
|
|
3345
3248
|
// Nested content starts where its node mark does, so the mark
|
|
3346
3249
|
// lands centred on the deepest rail and the text clears it.
|
|
3347
3250
|
paddingInlineStart: depth > 0 ? `${railNodeInsetPx(depth)}px` : "0px",
|
|
3348
3251
|
},
|
|
3349
3252
|
},
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
row, theme, structured: structuredRows, showInlineDetail: mobile,
|
|
3354
|
-
})));
|
|
3253
|
+
React.createElement(SessionRowContent, {
|
|
3254
|
+
row, theme, structured: structuredRows, showInlineDetail: mobile,
|
|
3255
|
+
})));
|
|
3355
3256
|
});
|
|
3356
3257
|
|
|
3357
|
-
function RichSessionRow({ row, theme, showDetail = false }) {
|
|
3358
|
-
const chrome = row.chrome;
|
|
3359
|
-
if (!chrome) return React.createElement(SessionRowContent, { row, theme, structured: true, showInlineDetail: showDetail });
|
|
3360
|
-
|
|
3361
|
-
const kindGlyph = SESSION_KIND_GLYPHS[chrome.kind] || null;
|
|
3362
|
-
const accent = resolveColor(theme, chrome.accentColor) || undefined;
|
|
3363
|
-
// Mobile has no room for a detail box, so it keeps the inline detail line
|
|
3364
|
-
// it always had. Desktop moves that content to the panel footer.
|
|
3365
|
-
const detailRuns = showDetail && Array.isArray(row.detailRuns) ? row.detailRuns : [];
|
|
3366
|
-
|
|
3367
|
-
return React.createElement("div", { className: "ps-rich-session-row" },
|
|
3368
|
-
React.createElement("div", { className: "ps-rich-session-main" },
|
|
3369
|
-
chrome.selectMode
|
|
3370
|
-
? React.createElement("span", {
|
|
3371
|
-
className: `ps-rich-session-check${chrome.checked ? " is-checked" : ""}`,
|
|
3372
|
-
}, chrome.checked ? "✓" : "")
|
|
3373
|
-
: null,
|
|
3374
|
-
kindGlyph
|
|
3375
|
-
? React.createElement("span", { className: "ps-rich-session-kind" }, kindGlyph)
|
|
3376
|
-
: React.createElement("span", {
|
|
3377
|
-
className: `ps-rich-session-dot${row.depth > 0 ? " is-node" : ""}`,
|
|
3378
|
-
style: row.depth > 0
|
|
3379
|
-
? { borderColor: resolveColor(theme, chrome.statusColor) || undefined }
|
|
3380
|
-
: { background: resolveColor(theme, chrome.statusColor) || undefined },
|
|
3381
|
-
title: row.status || undefined,
|
|
3382
|
-
}),
|
|
3383
|
-
chrome.owner
|
|
3384
|
-
? React.createElement("span", {
|
|
3385
|
-
className: `ps-rich-session-owner${chrome.owner.isMine ? " is-mine" : ""}`,
|
|
3386
|
-
title: chrome.owner.initials,
|
|
3387
|
-
}, chrome.owner.initials)
|
|
3388
|
-
: null,
|
|
3389
|
-
React.createElement("span", {
|
|
3390
|
-
className: `ps-rich-session-title${chrome.untitled ? " is-untitled" : ""}`,
|
|
3391
|
-
style: chrome.kind === "session" ? undefined : { color: accent },
|
|
3392
|
-
title: chrome.title,
|
|
3393
|
-
}, chrome.title),
|
|
3394
|
-
chrome.pinned ? React.createElement("span", { className: "ps-rich-session-pin", title: "Pinned" }, React.createElement(PinGlyph)) : null,
|
|
3395
|
-
chrome.cron ? React.createElement("span", { className: "ps-rich-session-cron", title: "Scheduled" }, "⏱") : null,
|
|
3396
|
-
chrome.childBadge
|
|
3397
|
-
? React.createElement("span", {
|
|
3398
|
-
className: "ps-rich-session-count",
|
|
3399
|
-
style: { color: resolveColor(theme, chrome.childBadge.color) || undefined },
|
|
3400
|
-
}, chrome.childBadge.text)
|
|
3401
|
-
: null,
|
|
3402
|
-
chrome.ctx
|
|
3403
|
-
? React.createElement("span", {
|
|
3404
|
-
className: "ps-rich-session-ctx",
|
|
3405
|
-
style: { color: resolveColor(theme, chrome.ctx.color) || undefined },
|
|
3406
|
-
}, chrome.ctx.text)
|
|
3407
|
-
: null),
|
|
3408
|
-
detailRuns.length > 0
|
|
3409
|
-
? React.createElement("div", { className: "ps-rich-session-detail" },
|
|
3410
|
-
React.createElement(Runs, { runs: detailRuns, theme }))
|
|
3411
|
-
: null);
|
|
3412
|
-
}
|
|
3413
|
-
|
|
3414
3258
|
/**
|
|
3415
3259
|
* Portal row prefix: drop the "└ " depth glyph (the guide rail says it, and
|
|
3416
3260
|
* better — a rail shows a branch that CONTINUES, which the glyph cannot), and
|
|
@@ -3426,6 +3270,9 @@ function portalRowRuns(runs, theme) {
|
|
|
3426
3270
|
const rest = [];
|
|
3427
3271
|
for (const run of runs) {
|
|
3428
3272
|
if (run?.role === "depth") continue;
|
|
3273
|
+
// The row's own styling already marks selection; drawing the bar too
|
|
3274
|
+
// would cost a column and truncate the row early.
|
|
3275
|
+
if (run?.role === "selection") continue;
|
|
3429
3276
|
if (run?.role === "status") {
|
|
3430
3277
|
statusColor = resolveColor(theme, run.color) || null;
|
|
3431
3278
|
continue;
|
|
@@ -3643,11 +3490,6 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
3643
3490
|
connectionMode: state.connection?.mode || "local",
|
|
3644
3491
|
modalOpen: Boolean(state.ui.modal),
|
|
3645
3492
|
focused: state.ui.focusRegion === "sessions",
|
|
3646
|
-
// The rich chat view also restyles the session list, so the portal
|
|
3647
|
-
// reads as one product rather than half terminal / half desktop.
|
|
3648
|
-
// TODO: promote this to its own `ui.richUi` setting if the two are
|
|
3649
|
-
// ever wanted independently.
|
|
3650
|
-
rich: Boolean(getTheme(state.ui.themeId)?.richChat),
|
|
3651
3493
|
// Clicking empty space clears the list highlight; the row VM reads it,
|
|
3652
3494
|
// so the reconstruction below must carry it or the click does nothing
|
|
3653
3495
|
// visible (the same omission that blinded the Node Map).
|
|
@@ -3736,9 +3578,21 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
3736
3578
|
&& String(activeSession.owner.provider) === String(authPrincipal.provider)
|
|
3737
3579
|
&& String(activeSession.owner.subject) === String(authPrincipal.subject),
|
|
3738
3580
|
);
|
|
3581
|
+
// System sessions are ADMIN-modifiable and read-only for everyone else —
|
|
3582
|
+
// which is exactly what the server already enforces in
|
|
3583
|
+
// evaluateSessionAccess: an admin passes every class, and for a non-admin
|
|
3584
|
+
// a system session allows read and refuses every write with "System
|
|
3585
|
+
// sessions are managed by administrators."
|
|
3586
|
+
//
|
|
3587
|
+
// This gate used to be STRICTER than the server, excluding isSystem for
|
|
3588
|
+
// everybody, so an admin who was entitled to rename or re-model the
|
|
3589
|
+
// sweeper had the button disabled with no way to reach the capability.
|
|
3590
|
+
// Client and server now agree; the server remains the enforcement point.
|
|
3739
3591
|
const canModifyActiveSession = Boolean(
|
|
3740
|
-
activeSession && !activeSession.
|
|
3741
|
-
&& (
|
|
3592
|
+
activeSession && !activeSession.isGroup
|
|
3593
|
+
&& (activeSession.isSystem
|
|
3594
|
+
? isAdminViewer
|
|
3595
|
+
: (isAdminViewer || ownsActiveSession)),
|
|
3742
3596
|
);
|
|
3743
3597
|
const selectedCount = Array.isArray(viewState.selectedIds) ? viewState.selectedIds.length : 0;
|
|
3744
3598
|
const isBulkSelection = selectedCount > 1;
|
|
@@ -3775,7 +3629,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
3775
3629
|
: (activeIsGroupable ? [activeSession.sessionId] : []);
|
|
3776
3630
|
// The folder button is never disabled: with nothing to move it makes an
|
|
3777
3631
|
// empty folder to drag into, which is the natural way to get one at all.
|
|
3778
|
-
const combinedPanelClassName = `ps-session-pane${
|
|
3632
|
+
const combinedPanelClassName = `ps-session-pane${panelClassName ? ` ${panelClassName}` : ""}`;
|
|
3779
3633
|
// The click handler must be referentially stable or every memoized row
|
|
3780
3634
|
// re-renders on each keypress, defeating the point. It reads the current
|
|
3781
3635
|
// rows/viewState from a ref that is refreshed on every render instead of
|
|
@@ -3800,15 +3654,40 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
3800
3654
|
cancelAnimationFrame(autoScrollRef.current);
|
|
3801
3655
|
autoScrollRef.current = null;
|
|
3802
3656
|
}, []);
|
|
3657
|
+
/**
|
|
3658
|
+
* The element that actually scrolls the session list.
|
|
3659
|
+
*
|
|
3660
|
+
* Walked up from the list itself rather than resolved from the pointer:
|
|
3661
|
+
* `[data-session-scroll]` is only stamped on the pane while it holds
|
|
3662
|
+
* focus, and the pane and the list inside it BOTH declare overflow — which
|
|
3663
|
+
* of the two really scrolls depends on how the flex layout resolved. The
|
|
3664
|
+
* old lookup could land on the one whose scrollHeight equals its
|
|
3665
|
+
* clientHeight, where `scrollTop +=` is silently a no-op and the list just
|
|
3666
|
+
* sat there while you dragged past its edge.
|
|
3667
|
+
*/
|
|
3668
|
+
const findSessionScroller = React.useCallback(() => {
|
|
3669
|
+
let node = sessionListRef.current;
|
|
3670
|
+
while (node && node !== document.body) {
|
|
3671
|
+
const overflowY = window.getComputedStyle(node).overflowY;
|
|
3672
|
+
if (/(auto|scroll|overlay)/.test(overflowY) && node.scrollHeight - node.clientHeight > 1) {
|
|
3673
|
+
return node;
|
|
3674
|
+
}
|
|
3675
|
+
node = node.parentElement;
|
|
3676
|
+
}
|
|
3677
|
+
return null;
|
|
3678
|
+
}, []);
|
|
3679
|
+
|
|
3803
3680
|
const updateAutoScroll = React.useCallback((x, y) => {
|
|
3804
3681
|
const EDGE_PX = 48;
|
|
3805
3682
|
const MAX_STEP_PX = 14;
|
|
3806
|
-
const
|
|
3807
|
-
const scroller = el?.closest?.("[data-session-scroll]")
|
|
3808
|
-
|| document.querySelector("[data-session-scroll]");
|
|
3683
|
+
const scroller = findSessionScroller();
|
|
3809
3684
|
if (!scroller) { stopAutoScroll(); return; }
|
|
3810
3685
|
const box = scroller.getBoundingClientRect();
|
|
3811
3686
|
let step = 0;
|
|
3687
|
+
// Each branch's own guard already makes its numerator positive, and
|
|
3688
|
+
// Math.min caps the ramp at full speed — so dragging PAST the edge, the
|
|
3689
|
+
// case this exists for, keeps scrolling at MAX_STEP rather than
|
|
3690
|
+
// accelerating without bound.
|
|
3812
3691
|
if (y < box.top + EDGE_PX) {
|
|
3813
3692
|
step = -MAX_STEP_PX * Math.min(1, (box.top + EDGE_PX - y) / EDGE_PX);
|
|
3814
3693
|
} else if (y > box.bottom - EDGE_PX) {
|
|
@@ -3827,7 +3706,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
3827
3706
|
autoScrollRef.current = requestAnimationFrame(tick);
|
|
3828
3707
|
};
|
|
3829
3708
|
autoScrollRef.current = requestAnimationFrame(tick);
|
|
3830
|
-
}, [stopAutoScroll]);
|
|
3709
|
+
}, [findSessionScroller, stopAutoScroll]);
|
|
3831
3710
|
|
|
3832
3711
|
const finishDrag = React.useCallback((commit) => {
|
|
3833
3712
|
const pending = dragRef.current;
|
|
@@ -3913,6 +3792,18 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
3913
3792
|
*/
|
|
3914
3793
|
const resolveDropIntent = React.useCallback((x, y, pending) => {
|
|
3915
3794
|
const overGroupId = resolveDropGroup(x, y);
|
|
3795
|
+
// A FOLDER has a groupId of its own — its own id. Feeding that through
|
|
3796
|
+
// the filing branches below meant every position in the list resolved
|
|
3797
|
+
// to "file into that other folder" or "take it out of itself", and a
|
|
3798
|
+
// folder could never once be reordered. Folders nest in nothing, so
|
|
3799
|
+
// reorder is the only gesture they have.
|
|
3800
|
+
if (pending?.isGroup) {
|
|
3801
|
+
const target = pending.ids.length === 1
|
|
3802
|
+
? resolveReorderTarget(x, y, pending.container, pending.ids[0])
|
|
3803
|
+
: undefined;
|
|
3804
|
+
if (target === undefined) return { kind: "none", overGroupId: null, before: undefined, after: null };
|
|
3805
|
+
return { kind: "reorder", overGroupId: null, before: target.before, after: target.after };
|
|
3806
|
+
}
|
|
3916
3807
|
const ownGroupId = pending?.ownGroupId || null;
|
|
3917
3808
|
if (overGroupId && overGroupId !== ownGroupId) {
|
|
3918
3809
|
return { kind: "file", overGroupId, before: undefined, after: null };
|
|
@@ -4033,6 +3924,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
4033
3924
|
beforeSessionId: undefined,
|
|
4034
3925
|
orderable: Boolean(row.orderable),
|
|
4035
3926
|
container: row.orderContainer ?? null,
|
|
3927
|
+
isGroup: Boolean(row.isGroup),
|
|
4036
3928
|
// The folder the row currently lives in: filing into the
|
|
4037
3929
|
// folder it is ALREADY in is a reorder, not a move.
|
|
4038
3930
|
ownGroupId: row.groupId || null,
|
|
@@ -4131,21 +4023,56 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
4131
4023
|
}
|
|
4132
4024
|
}, []);
|
|
4133
4025
|
|
|
4026
|
+
// Which (session, focus, modal) combination has already been pulled into
|
|
4027
|
+
// view. Anything else re-arms the reveal; a bare list refresh does not.
|
|
4028
|
+
const revealedRowKeyRef = React.useRef(null);
|
|
4029
|
+
const activeRowRevealKey = `${viewState.activeSessionId || ""}${viewState.focused ? 1 : 0}${viewState.modalOpen ? 1 : 0}`;
|
|
4030
|
+
|
|
4134
4031
|
React.useEffect(() => {
|
|
4135
|
-
if (viewState.modalOpen || !viewState.focused || !viewState.activeSessionId)
|
|
4032
|
+
if (viewState.modalOpen || !viewState.focused || !viewState.activeSessionId) {
|
|
4033
|
+
// Disarm, so returning from a modal (or refocusing the pane) counts
|
|
4034
|
+
// as a fresh arming. Without this the ref still holds the key from
|
|
4035
|
+
// before the modal opened, the comparison below matches, and the
|
|
4036
|
+
// row is never brought back into view on the way out.
|
|
4037
|
+
revealedRowKeyRef.current = null;
|
|
4038
|
+
return;
|
|
4039
|
+
}
|
|
4136
4040
|
const activeButton = sessionButtonRefs.current.get(viewState.activeSessionId);
|
|
4041
|
+
// The row is not rendered yet. On a page reload the active session id
|
|
4042
|
+
// is restored from the profile BEFORE the listing arrives, so the first
|
|
4043
|
+
// run lands here — leaving the key un-consumed is what lets the reveal
|
|
4044
|
+
// retry once `sessionsFlat` brings the row in.
|
|
4137
4045
|
if (!activeButton) return;
|
|
4138
4046
|
|
|
4139
|
-
|
|
4047
|
+
// Everything below runs ONCE per arming. This effect is woken by every
|
|
4048
|
+
// list refresh (~4×/sec while a session streams), and moving DOM focus
|
|
4049
|
+
// on each of those was actively dangerous: the Manage and Copy-link
|
|
4050
|
+
// dialogs are local React state rather than `ui.modal`, so
|
|
4051
|
+
// `viewState.modalOpen` is false while they are open — focus was ripped
|
|
4052
|
+
// out of their text inputs mid-keystroke, and because the global
|
|
4053
|
+
// shortcut handler decides "am I editable?" from the event target, the
|
|
4054
|
+
// rest of what the user typed ran as commands (d = complete,
|
|
4055
|
+
// D = delete). Scrolling on every refresh has a milder failure but the
|
|
4056
|
+
// same shape: the list yanks back to the active row while the user is
|
|
4057
|
+
// deliberately scrolled away browsing older sessions.
|
|
4058
|
+
if (revealedRowKeyRef.current === activeRowRevealKey) return;
|
|
4059
|
+
revealedRowKeyRef.current = activeRowRevealKey;
|
|
4060
|
+
|
|
4061
|
+
// Never take focus off something the user is typing into. Belt and
|
|
4062
|
+
// braces on top of the arming guard, for any future caller of this
|
|
4063
|
+
// effect that fires while a local dialog is up.
|
|
4064
|
+
const focused = document.activeElement;
|
|
4065
|
+
const isTypingTarget = Boolean(focused && (
|
|
4066
|
+
focused.tagName === "INPUT"
|
|
4067
|
+
|| focused.tagName === "TEXTAREA"
|
|
4068
|
+
|| focused.tagName === "SELECT"
|
|
4069
|
+
|| focused.isContentEditable
|
|
4070
|
+
));
|
|
4071
|
+
if (focused !== activeButton && !isTypingTarget) {
|
|
4140
4072
|
activeButton.focus({ preventScroll: true });
|
|
4141
4073
|
}
|
|
4142
|
-
// Only pull the active row into view when the active session itself
|
|
4143
|
-
// changes (or on focus/modal transitions). Re-running scrollIntoView
|
|
4144
|
-
// on every `sessions/loaded` refresh would yank the list back to the
|
|
4145
|
-
// active row even when the user has deliberately scrolled away to
|
|
4146
|
-
// browse older sessions in a long list.
|
|
4147
4074
|
activeButton.scrollIntoView({ block: "nearest" });
|
|
4148
|
-
}, [viewState.activeSessionId, viewState.focused, viewState.modalOpen]);
|
|
4075
|
+
}, [activeRowRevealKey, viewState.activeSessionId, viewState.focused, viewState.modalOpen, viewState.sessionsFlat]);
|
|
4149
4076
|
|
|
4150
4077
|
const panelActions = React.createElement(React.Fragment, null,
|
|
4151
4078
|
isBulkSelection
|
|
@@ -4191,7 +4118,20 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
4191
4118
|
icon: React.createElement(ManageGlyph),
|
|
4192
4119
|
onClick: () => setManageOpen(true),
|
|
4193
4120
|
disabled: !canModifyActiveSession || isBulkSelection,
|
|
4194
|
-
|
|
4121
|
+
// A disabled control has to say WHY. This label used to describe
|
|
4122
|
+
// the ENABLED behaviour in every disabled case except bulk
|
|
4123
|
+
// selection, so on a system session the tooltip promised "rename,
|
|
4124
|
+
// switch model, and sharing" while the click did nothing — which
|
|
4125
|
+
// reads as a broken dialog rather than an unavailable action.
|
|
4126
|
+
label: isBulkSelection
|
|
4127
|
+
? "Disabled while multiple sessions are selected"
|
|
4128
|
+
: activeSession?.isSystem
|
|
4129
|
+
? "System sessions are managed by administrators"
|
|
4130
|
+
: activeSession?.isGroup
|
|
4131
|
+
? "Folders cannot be managed here — use the folder controls"
|
|
4132
|
+
: !canModifyActiveSession
|
|
4133
|
+
? "Only this session's owner or an admin can manage it"
|
|
4134
|
+
: "Manage session — rename, switch model, and sharing",
|
|
4195
4135
|
}),
|
|
4196
4136
|
React.createElement(IconButton, {
|
|
4197
4137
|
className: "ps-mini-button",
|
|
@@ -4241,7 +4181,14 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
4241
4181
|
? `${dragState.ids.length} sessions`
|
|
4242
4182
|
: (dragState.titles[0] || "session"))),
|
|
4243
4183
|
React.createElement("div", { className: "ps-drag-ghost__hint" },
|
|
4244
|
-
|
|
4184
|
+
// Read the resolved INTENT, not just the folder under the
|
|
4185
|
+
// pointer. Folders always resolve to reorder with a null
|
|
4186
|
+
// overGroupId, so the old test labelled every folder drag
|
|
4187
|
+
// "Release to remove from folder" — a folder is in no folder.
|
|
4188
|
+
dragState.intent === "file" ? "Release to file here"
|
|
4189
|
+
: dragState.intent === "unfile" ? "Release to remove from folder"
|
|
4190
|
+
: dragState.intent === "reorder" ? "Release to move here"
|
|
4191
|
+
: "Release to cancel"))
|
|
4245
4192
|
: null;
|
|
4246
4193
|
|
|
4247
4194
|
return React.createElement(React.Fragment, null,
|
|
@@ -4275,7 +4222,6 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
4275
4222
|
key: row.sessionId,
|
|
4276
4223
|
row,
|
|
4277
4224
|
theme,
|
|
4278
|
-
rich: viewState.rich,
|
|
4279
4225
|
structuredRows,
|
|
4280
4226
|
mobile: isMobilePane,
|
|
4281
4227
|
onRowClick: handleRowClick,
|
|
@@ -4977,7 +4923,6 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
4977
4923
|
() => appendAnimatedDotsToRuns(chrome.titleRight, chrome.animateTitleRight ? animatedDots : ""),
|
|
4978
4924
|
[animatedDots, chrome.animateTitleRight, chrome.titleRight],
|
|
4979
4925
|
);
|
|
4980
|
-
const richMode = Boolean(theme?.richChat) && !viewState.activeSessionIsGroup;
|
|
4981
4926
|
const [loadingOlder, setLoadingOlder] = React.useState(false);
|
|
4982
4927
|
// Scroll-up expands the transcript automatically until the soft cap, then
|
|
4983
4928
|
// refuses — and the portal had no control to ask for more, so a busy
|
|
@@ -4987,23 +4932,10 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
4987
4932
|
viewState.activeHistory?.hasOlderEvents
|
|
4988
4933
|
&& Number(viewState.activeHistory?.loadedEventCount || 0) >= AUTO_HISTORY_EVENT_SOFT_CAP,
|
|
4989
4934
|
);
|
|
4990
|
-
const
|
|
4991
|
-
() => (
|
|
4992
|
-
[
|
|
4935
|
+
const lines = React.useMemo(
|
|
4936
|
+
() => selectChatLines(selectorState, viewState.contentWidth, { tableMode: "sentinel" }),
|
|
4937
|
+
[selectorState, viewState.contentWidth],
|
|
4993
4938
|
);
|
|
4994
|
-
const lines = React.useMemo(() => {
|
|
4995
|
-
if (richBlocks) {
|
|
4996
|
-
// Rich mode paints from `richBlocks` via renderBody; these
|
|
4997
|
-
// pseudo-lines exist only as the scroll-sync change signal
|
|
4998
|
-
// (autoscroll-to-bottom fires when a block grows or arrives).
|
|
4999
|
-
return richBlocks.map((block, index) => ({
|
|
5000
|
-
text: block.kind === "message"
|
|
5001
|
-
? `m:${block.id ?? index}:${(block.text || "").length}:${block.pendingPhase || ""}`
|
|
5002
|
-
: `l:${index}:${(block.lines || []).length}`,
|
|
5003
|
-
}));
|
|
5004
|
-
}
|
|
5005
|
-
return selectChatLines(selectorState, viewState.contentWidth, { tableMode: "sentinel" });
|
|
5006
|
-
}, [richBlocks, selectorState, viewState.contentWidth]);
|
|
5007
4939
|
const spinnerFrame = useSpinnerFrame(viewState.activeSessionStatus === "running");
|
|
5008
4940
|
const liveActivityLines = React.useMemo(
|
|
5009
4941
|
() => selectLiveActivityLines(selectorState, { spinnerFrame, maxWidth: viewState.contentWidth }),
|
|
@@ -5072,10 +5004,7 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
5072
5004
|
|
|
5073
5005
|
return React.createElement(ScrollLinesPanel, {
|
|
5074
5006
|
controller,
|
|
5075
|
-
title: (
|
|
5076
|
-
const baseTitle = mobile ? compactTitleRuns(chrome.title, 28) : chrome.title;
|
|
5077
|
-
return richMode ? unbracketTitleRuns(baseTitle) : baseTitle;
|
|
5078
|
-
})(),
|
|
5007
|
+
title: mobile ? compactTitleRuns(chrome.title, 28) : chrome.title,
|
|
5079
5008
|
titleRight: mobile && titleRight ? compactTitleRuns(titleRight, 18) : titleRight,
|
|
5080
5009
|
// Chat/Summary toggling lives on the top toolbar so the pane chrome
|
|
5081
5010
|
// stays clean. The toolbar button is disabled for group sessions.
|
|
@@ -5087,8 +5016,8 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
5087
5016
|
scrollOffset: viewState.scroll,
|
|
5088
5017
|
scrollMode: "bottom",
|
|
5089
5018
|
paneKey: "chat",
|
|
5090
|
-
className:
|
|
5091
|
-
panelClassName:
|
|
5019
|
+
className: "is-wrapped",
|
|
5020
|
+
panelClassName: "ps-chat-panel",
|
|
5092
5021
|
bottomContent: composer,
|
|
5093
5022
|
topContent: showLoadOlder
|
|
5094
5023
|
? React.createElement("div", { className: "ps-load-older-bar" },
|
|
@@ -5105,13 +5034,7 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
5105
5034
|
}, loadingOlder ? "Loading older messages…" : "↑ Load older messages"))
|
|
5106
5035
|
: null,
|
|
5107
5036
|
structuredBlocks: true,
|
|
5108
|
-
renderBody:
|
|
5109
|
-
? (_bodyLines, bodyTheme) => React.createElement(RichChatBlocks, {
|
|
5110
|
-
blocks: richBlocks,
|
|
5111
|
-
theme: bodyTheme,
|
|
5112
|
-
controller,
|
|
5113
|
-
})
|
|
5114
|
-
: null,
|
|
5037
|
+
renderBody: null,
|
|
5115
5038
|
});
|
|
5116
5039
|
}
|
|
5117
5040
|
|
|
@@ -6429,7 +6352,6 @@ function IconButton({ icon, label, onClick, disabled = false, active = false, cl
|
|
|
6429
6352
|
}
|
|
6430
6353
|
|
|
6431
6354
|
function Toolbar({ controller, mobile, chatFocusMode = false, onToggleChatFocus = null, chatFocusDisabled = false }) {
|
|
6432
|
-
const richUi = useControllerSelector(controller, (state) => Boolean(getTheme(state.ui.themeId)?.richChat));
|
|
6433
6355
|
const [headerSlot, setHeaderSlot] = React.useState(null);
|
|
6434
6356
|
React.useEffect(() => {
|
|
6435
6357
|
if (typeof document === "undefined") return;
|
|
@@ -6438,7 +6360,7 @@ function Toolbar({ controller, mobile, chatFocusMode = false, onToggleChatFocus
|
|
|
6438
6360
|
// rich view had already handed that row back to the panes. Mobile keeps
|
|
6439
6361
|
// the inline strip; there is no header room for it on a phone.
|
|
6440
6362
|
setHeaderSlot(!mobile ? document.getElementById("ps-header-toolbar-slot") : null);
|
|
6441
|
-
}, [
|
|
6363
|
+
}, [mobile]);
|
|
6442
6364
|
const adminVisible = useControllerSelector(controller, (state) => Boolean(state.admin?.visible));
|
|
6443
6365
|
const chatView = useControllerSelector(controller, (state) => ({
|
|
6444
6366
|
mode: state.ui.chatViewMode || "transcript",
|
|
@@ -6874,6 +6796,7 @@ function MobileNav({ activePane, setActivePane, controller }) {
|
|
|
6874
6796
|
|
|
6875
6797
|
function ModalLayer({ controller }) {
|
|
6876
6798
|
const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
|
|
6799
|
+
const touchScale = useControllerSelector(controller, (state) => Boolean(state.ui.touchScale));
|
|
6877
6800
|
const theme = getTheme(themeId);
|
|
6878
6801
|
const modalState = useControllerSelector(controller, (state) => ({
|
|
6879
6802
|
rawModal: state.ui.modal,
|
|
@@ -6977,7 +6900,22 @@ function ModalLayer({ controller }) {
|
|
|
6977
6900
|
|
|
6978
6901
|
const close = () => controller.handleCommand(UI_COMMANDS.CLOSE_MODAL).catch(() => {});
|
|
6979
6902
|
|
|
6980
|
-
|
|
6903
|
+
// Display preferences that belong beside the theme rather than in a
|
|
6904
|
+
// settings screen of their own. Rendered in the theme picker's footer: it
|
|
6905
|
+
// is the "how this looks" surface, and a checkbox there is discoverable in
|
|
6906
|
+
// a way a keyboard-only command never is.
|
|
6907
|
+
const renderTouchScaleToggle = () => React.createElement("label", {
|
|
6908
|
+
className: "ps-modal-toggle",
|
|
6909
|
+
title: "Larger type and bigger hit targets, for phones and touch screens.",
|
|
6910
|
+
},
|
|
6911
|
+
React.createElement("input", {
|
|
6912
|
+
type: "checkbox",
|
|
6913
|
+
checked: touchScale,
|
|
6914
|
+
onChange: (event) => controller.dispatch({ type: "ui/touchScale", enabled: event.target.checked }),
|
|
6915
|
+
}),
|
|
6916
|
+
React.createElement("span", null, "Mobile"));
|
|
6917
|
+
|
|
6918
|
+
const renderListModal = (presentation, confirmLabel = "Apply", footerExtras = null) => {
|
|
6981
6919
|
const rows = Array.isArray(presentation.rows) ? presentation.rows : [];
|
|
6982
6920
|
const rowItemIndexes = Array.isArray(presentation.rowItemIndexes) ? presentation.rowItemIndexes : null;
|
|
6983
6921
|
const usesHangingIndent = modal.type === "modelPicker" || modal.type === "reasoningEffortPicker" || modal.type === "contextTierPicker" || modal.type === "sessionAgentPicker";
|
|
@@ -6998,24 +6936,48 @@ function ModalLayer({ controller }) {
|
|
|
6998
6936
|
key: item?.id || `row:${rowIndex}`,
|
|
6999
6937
|
type: "button",
|
|
7000
6938
|
className: `ps-list-button ps-modal-list-button${itemIndex === modal.selectedIndex ? " is-selected" : ""}${usesHangingIndent ? " is-hanging" : ""}${item?.disabled ? " is-disabled" : ""}`,
|
|
7001
|
-
|
|
6939
|
+
// A section header is a disclosure control: one click opens
|
|
6940
|
+
// or closes it. Selecting it and making the user hit the
|
|
6941
|
+
// footer button would be a two-step for something that
|
|
6942
|
+
// reads as a twisty.
|
|
6943
|
+
onClick: () => {
|
|
6944
|
+
controller.dispatch({ type: "ui/modalSelection", index: itemIndex });
|
|
6945
|
+
if (item?.kind === "section") controller.toggleAgentPickerSection(item.sectionKey);
|
|
6946
|
+
},
|
|
7002
6947
|
},
|
|
7003
6948
|
React.createElement("div", { className: "ps-line ps-modal-list-line" },
|
|
7004
6949
|
React.createElement(Runs, { runs, theme })));
|
|
7005
6950
|
})
|
|
7006
|
-
: (modal.items || []).
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
React.createElement(
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
6951
|
+
: (modal.items || []).flatMap((item, index, allItems) => {
|
|
6952
|
+
const button = React.createElement("button", {
|
|
6953
|
+
key: item.id || index,
|
|
6954
|
+
type: "button",
|
|
6955
|
+
className: `ps-list-button ps-modal-list-button${index === modal.selectedIndex ? " is-selected" : ""}${usesHangingIndent ? " is-hanging" : ""}${item?.disabled ? " is-disabled" : ""}`,
|
|
6956
|
+
onClick: () => controller.dispatch({ type: "ui/modalSelection", index }),
|
|
6957
|
+
},
|
|
6958
|
+
React.createElement("div", { className: "ps-line ps-modal-list-line" },
|
|
6959
|
+
React.createElement(Runs, {
|
|
6960
|
+
runs: Array.isArray(rows?.[index])
|
|
6961
|
+
? rows[index]
|
|
6962
|
+
: normalizeLines([rows?.[index]])[0]?.runs || [{ text: rows?.[index]?.text || "", color: rows?.[index]?.color }],
|
|
6963
|
+
theme,
|
|
6964
|
+
})));
|
|
6965
|
+
// A group heading is emitted BETWEEN buttons, never as one of
|
|
6966
|
+
// them: `index` still addresses items 1:1, so selectedIndex,
|
|
6967
|
+
// arrow-key navigation and the scroll-into-view math are all
|
|
6968
|
+
// untouched by grouping. Only lists whose items carry a
|
|
6969
|
+
// `group` (today: the theme picker) get headings at all.
|
|
6970
|
+
const startsGroup = item?.group && item.group !== allItems[index - 1]?.group;
|
|
6971
|
+
if (!startsGroup) return [button];
|
|
6972
|
+
return [
|
|
6973
|
+
React.createElement("div", {
|
|
6974
|
+
key: `group:${item.group}`,
|
|
6975
|
+
className: "ps-modal-list-group",
|
|
6976
|
+
"aria-hidden": "true",
|
|
6977
|
+
}, item.group),
|
|
6978
|
+
button,
|
|
6979
|
+
];
|
|
6980
|
+
});
|
|
7019
6981
|
|
|
7020
6982
|
return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
|
|
7021
6983
|
React.createElement("div", { className: `ps-modal is-list${modal.type === "themePicker" ? " is-theme-picker" : ""}`, onClick: (event) => event.stopPropagation() },
|
|
@@ -7033,6 +6995,7 @@ function ModalLayer({ controller }) {
|
|
|
7033
6995
|
),
|
|
7034
6996
|
),
|
|
7035
6997
|
React.createElement("div", { className: "ps-modal-footer" },
|
|
6998
|
+
footerExtras,
|
|
7036
6999
|
React.createElement("button", { type: "button", className: "ps-modal-button", onClick: close }, "Cancel"),
|
|
7037
7000
|
React.createElement("button", {
|
|
7038
7001
|
type: "button",
|
|
@@ -7097,7 +7060,7 @@ function ModalLayer({ controller }) {
|
|
|
7097
7060
|
));
|
|
7098
7061
|
}
|
|
7099
7062
|
if (modal.type === "themePicker" && modalState.themePicker) {
|
|
7100
|
-
return renderListModal(modalState.themePicker, "Apply Theme");
|
|
7063
|
+
return renderListModal(modalState.themePicker, "Apply Theme", renderTouchScaleToggle());
|
|
7101
7064
|
}
|
|
7102
7065
|
// In switch-model mode the model/reasoning pickers retarget an existing
|
|
7103
7066
|
// session, so the confirm action is "Switch Model", not "Create Session".
|
|
@@ -7112,7 +7075,14 @@ function ModalLayer({ controller }) {
|
|
|
7112
7075
|
return renderListModal(modalState.contextTierPicker, pickerConfirmLabel);
|
|
7113
7076
|
}
|
|
7114
7077
|
if (modal.type === "sessionAgentPicker" && modalState.sessionAgentPicker) {
|
|
7115
|
-
|
|
7078
|
+
// The primary button follows the selection: on a section it is the
|
|
7079
|
+
// disclosure, not a create, and labelling it "Create Session" would
|
|
7080
|
+
// promise something Enter does not do there.
|
|
7081
|
+
const picked = modal.items?.[modal.selectedIndex || 0];
|
|
7082
|
+
const confirmLabel = picked?.kind === "section"
|
|
7083
|
+
? (picked.collapsed ? "Open" : "Close")
|
|
7084
|
+
: "Create Session";
|
|
7085
|
+
return renderListModal(modalState.sessionAgentPicker, confirmLabel);
|
|
7116
7086
|
}
|
|
7117
7087
|
if (modal.type === "sessionGroupPicker" && modalState.sessionGroupPicker) {
|
|
7118
7088
|
return renderListModal(modalState.sessionGroupPicker, "Move");
|
|
@@ -7500,6 +7470,18 @@ function useKeyboardShortcuts(controller, mobile) {
|
|
|
7500
7470
|
controller.handleCommand(UI_COMMANDS.MODAL_PANE_NEXT).catch(() => {});
|
|
7501
7471
|
return;
|
|
7502
7472
|
}
|
|
7473
|
+
// Disclosure keys for the agent picker. Scoped to that modal:
|
|
7474
|
+
// every other list modal is flat, and left/right there would
|
|
7475
|
+
// either do nothing or fight the Tab pane-cycling. Without
|
|
7476
|
+
// this the picker's own hint ("Enter or → to open") named a
|
|
7477
|
+
// key nothing listened for.
|
|
7478
|
+
if (modal.type === "sessionAgentPicker" && (event.key === "ArrowRight" || event.key === "ArrowLeft")) {
|
|
7479
|
+
event.preventDefault();
|
|
7480
|
+
controller.handleCommand(
|
|
7481
|
+
event.key === "ArrowRight" ? UI_COMMANDS.MODAL_PANE_NEXT : UI_COMMANDS.MODAL_PANE_PREV,
|
|
7482
|
+
).catch(() => {});
|
|
7483
|
+
return;
|
|
7484
|
+
}
|
|
7503
7485
|
if (event.key === "ArrowUp" || event.key === "k") {
|
|
7504
7486
|
event.preventDefault();
|
|
7505
7487
|
controller.handleCommand(UI_COMMANDS.MODAL_PREV).catch(() => {});
|
|
@@ -8499,16 +8481,6 @@ export function createWebPilotSwarmController({ transport, mode = "remote", bran
|
|
|
8499
8481
|
}
|
|
8500
8482
|
|
|
8501
8483
|
export function PilotSwarmWebApp({ controller }) {
|
|
8502
|
-
// The rich UI restyles chrome that lives OUTSIDE this tree (the portal
|
|
8503
|
-
// header in App.jsx), so the flag rides on <body> and every surface
|
|
8504
|
-
// keys off `body.ps-rich-ui` in CSS rather than threading a prop.
|
|
8505
|
-
const richUi = useControllerSelector(controller, (state) => Boolean(getTheme(state.ui.themeId)?.richChat));
|
|
8506
|
-
React.useEffect(() => {
|
|
8507
|
-
if (typeof document === "undefined") return undefined;
|
|
8508
|
-
document.body.classList.toggle("ps-rich-ui", richUi);
|
|
8509
|
-
return () => document.body.classList.remove("ps-rich-ui");
|
|
8510
|
-
}, [richUi]);
|
|
8511
|
-
|
|
8512
8484
|
const viewportRef = React.useRef(null);
|
|
8513
8485
|
const mainGridRef = React.useRef(null);
|
|
8514
8486
|
const viewport = useMeasuredViewport(viewportRef);
|
|
@@ -8553,6 +8525,7 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
8553
8525
|
const [chatFocusPane, setChatFocusPane] = React.useState(null);
|
|
8554
8526
|
const state = useControllerSelector(controller, (rootState) => ({
|
|
8555
8527
|
themeId: rootState.ui.themeId,
|
|
8528
|
+
touchScale: Boolean(rootState.ui.touchScale),
|
|
8556
8529
|
ownerFilter: rootState.sessions.ownerFilter,
|
|
8557
8530
|
pinnedIds: rootState.sessions.pinnedIds,
|
|
8558
8531
|
// Without this the profile-save effect reads undefined and a reorder
|
|
@@ -8727,11 +8700,12 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
8727
8700
|
});
|
|
8728
8701
|
}, 400);
|
|
8729
8702
|
return undefined;
|
|
8730
|
-
}, [controller, state.activeSessionId, state.activityPaneAdjust, state.chatViewMode, state.collapsedSessionIds, state.ownerFilter, state.paneAdjust, state.manualOrder, state.pinnedIds, state.portalSessionColumnAdjust, state.sessionPaneAdjust, state.themeId]);
|
|
8703
|
+
}, [controller, state.activeSessionId, state.activityPaneAdjust, state.chatViewMode, state.collapsedSessionIds, state.ownerFilter, state.paneAdjust, state.manualOrder, state.pinnedIds, state.portalSessionColumnAdjust, state.sessionPaneAdjust, state.themeId, state.touchScale]);
|
|
8731
8704
|
|
|
8732
8705
|
React.useEffect(() => {
|
|
8733
8706
|
applyDocumentTheme(state.themeId);
|
|
8734
|
-
|
|
8707
|
+
applyTouchScale(state.touchScale);
|
|
8708
|
+
}, [state.themeId, state.touchScale]);
|
|
8735
8709
|
|
|
8736
8710
|
// Follow focus only on an actual TRANSITION, never on the first run.
|
|
8737
8711
|
//
|