pilotswarm 0.5.57 → 0.5.59

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.
Files changed (93) hide show
  1. package/mcp/dist/src/context.d.ts +2 -0
  2. package/mcp/dist/src/context.d.ts.map +1 -1
  3. package/mcp/dist/src/context.js +3 -1
  4. package/mcp/dist/src/context.js.map +1 -1
  5. package/mcp/dist/src/tools/agent-packages.js +1 -1
  6. package/mcp/dist/src/tools/agent-packages.js.map +1 -1
  7. package/mcp/dist/src/tools/capabilities.d.ts +4 -0
  8. package/mcp/dist/src/tools/capabilities.d.ts.map +1 -1
  9. package/mcp/dist/src/tools/capabilities.js +4 -0
  10. package/mcp/dist/src/tools/capabilities.js.map +1 -1
  11. package/package.json +2 -2
  12. package/tui/src/node-sdk-transport.js +5 -0
  13. package/ui/core/src/controller.js +136 -11
  14. package/ui/core/src/formatting.js +18 -9
  15. package/ui/core/src/history.js +162 -9
  16. package/ui/core/src/reducer.js +32 -5
  17. package/ui/core/src/selectors.js +160 -16
  18. package/ui/core/src/session-errors.js +16 -11
  19. package/ui/react/src/components.js +1 -0
  20. package/ui/react/src/web-app.js +230 -80
  21. package/web/api/live-plane.js +193 -0
  22. package/web/api/router.js +1 -1
  23. package/web/api/ws.js +160 -4
  24. package/web/authz.js +6 -1
  25. package/web/dist/assets/{abnfDiagram-VRR7QNED-CJOR2hPi.js → abnfDiagram-VRR7QNED-D9Rv2VpV.js} +1 -1
  26. package/web/dist/assets/{arc-Yh3kc_KB.js → arc-BATiucnq.js} +1 -1
  27. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-DLRlUeyk.js → architectureDiagram-ZJ3FMSHR-CSTMt0si.js} +1 -1
  28. package/web/dist/assets/{blockDiagram-677ZJIJ3-x_TGH_fp.js → blockDiagram-677ZJIJ3-C_RGqfeY.js} +1 -1
  29. package/web/dist/assets/{c4Diagram-LMCZKHZV-C-j_bxeF.js → c4Diagram-LMCZKHZV-DH__s__0.js} +1 -1
  30. package/web/dist/assets/channel-CqgY9oZh.js +1 -0
  31. package/web/dist/assets/{chunk-2Q5K7J3B-CaI5Xqwo.js → chunk-2Q5K7J3B-C197fD3B.js} +1 -1
  32. package/web/dist/assets/{chunk-32BRIVSS-Bh_Crac6.js → chunk-32BRIVSS-BjpuBroU.js} +1 -1
  33. package/web/dist/assets/{chunk-5VM5RSS4-XM-3ajKG.js → chunk-5VM5RSS4-W_jL8euj.js} +1 -1
  34. package/web/dist/assets/{chunk-EX3LRPZG-ZwkulfFp.js → chunk-EX3LRPZG-a9h0dtlp.js} +1 -1
  35. package/web/dist/assets/{chunk-JWPE2WC7-CT3Q7T2A.js → chunk-JWPE2WC7-CQN0qE7F.js} +1 -1
  36. package/web/dist/assets/{chunk-MOJQB5TN-_Uc0ksg3.js → chunk-MOJQB5TN-D-Y6KQ7t.js} +1 -1
  37. package/web/dist/assets/{chunk-RYQCIY6F-DosoId7u.js → chunk-RYQCIY6F-jPwseC5f.js} +1 -1
  38. package/web/dist/assets/{chunk-V7JOEXUC-KRjq1cwM.js → chunk-V7JOEXUC-DRNCEcQh.js} +1 -1
  39. package/web/dist/assets/{chunk-VR4S4FIN-B6Ul1z3S.js → chunk-VR4S4FIN-DzhUcoZ7.js} +1 -1
  40. package/web/dist/assets/{chunk-XXDRQBXY-AikdVXm4.js → chunk-XXDRQBXY-ldJfyPXK.js} +1 -1
  41. package/web/dist/assets/classDiagram-OUVF2IWQ-Uz4DYZs2.js +1 -0
  42. package/web/dist/assets/classDiagram-v2-EOCWNBFH-Uz4DYZs2.js +1 -0
  43. package/web/dist/assets/{cose-bilkent-JH36ORCC-DTYwgEAa.js → cose-bilkent-JH36ORCC-BBVo21wR.js} +1 -1
  44. package/web/dist/assets/{cynefin-VYW2F7L2-C_8v2wIz.js → cynefin-VYW2F7L2-CgRQfdW1.js} +1 -1
  45. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-1-nw1zbQ.js → cynefinDiagram-TSTJHNR4-Ckz2X0t3.js} +1 -1
  46. package/web/dist/assets/{dagre-VKFMJZFB-CAjwRpvT.js → dagre-VKFMJZFB-C1SanJiY.js} +1 -1
  47. package/web/dist/assets/{diagram-FQU43EPY-Bg41nQw_.js → diagram-FQU43EPY-dBdEifVy.js} +1 -1
  48. package/web/dist/assets/{diagram-G47NLZAW-Cp6P-qMn.js → diagram-G47NLZAW-Byqc1VCB.js} +1 -1
  49. package/web/dist/assets/{diagram-NH7WQ7WH-DNdFuedg.js → diagram-NH7WQ7WH-9aeTaCsX.js} +1 -1
  50. package/web/dist/assets/{diagram-OA4YK3LP-DcHpELG2.js → diagram-OA4YK3LP-CL0a0o0k.js} +1 -1
  51. package/web/dist/assets/{diagram-WEI45ONY-B2_kyMw3.js → diagram-WEI45ONY-YlMQOJfY.js} +1 -1
  52. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-DztuAo3o.js → ebnfDiagram-CCIWWBDH-qMZQEnws.js} +1 -1
  53. package/web/dist/assets/{erDiagram-Q63AITRT-DNjKTOzp.js → erDiagram-Q63AITRT-C3CWBCtq.js} +1 -1
  54. package/web/dist/assets/{flowDiagram-23GEKE2U-DWjfeUM1.js → flowDiagram-23GEKE2U-BN79sybL.js} +1 -1
  55. package/web/dist/assets/{ganttDiagram-NO4QXBWP-Y0MgGVmy.js → ganttDiagram-NO4QXBWP-BOWhiAgB.js} +1 -1
  56. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX--lDKAKVz.js → gitGraphDiagram-IHSO6WYX-CKX9OACp.js} +1 -1
  57. package/web/dist/assets/index-B4s2lTsK.js +24 -0
  58. package/web/dist/assets/{index-BxPfp0AO.css → index-DhGdE2Nd.css} +1 -1
  59. package/web/dist/assets/{infoDiagram-FWYZ7A6U-CKmH8_YO.js → infoDiagram-FWYZ7A6U-QQEunPNs.js} +1 -1
  60. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-D88R7zUc.js → ishikawaDiagram-FXEZZL3T-CvMwrnLQ.js} +1 -1
  61. package/web/dist/assets/{journeyDiagram-5HDEW3XC-Ck7EMrsY.js → journeyDiagram-5HDEW3XC-giNpEVpj.js} +1 -1
  62. package/web/dist/assets/{kanban-definition-HUTT4EX6-hc-mOfSG.js → kanban-definition-HUTT4EX6-CUPPpo27.js} +1 -1
  63. package/web/dist/assets/{linear-BVhU4csP.js → linear-AXqadfOR.js} +1 -1
  64. package/web/dist/assets/{mermaid.core-C9u4FOCX.js → mermaid.core-C78wdk8Y.js} +4 -4
  65. package/web/dist/assets/{mindmap-definition-LN4V7U3C-DatQu9Xp.js → mindmap-definition-LN4V7U3C-BGUmh9bK.js} +1 -1
  66. package/web/dist/assets/{papaparse.min-BdnoXc8M.js → papaparse.min-Bh8RLCk_.js} +1 -1
  67. package/web/dist/assets/{pegDiagram-2B236MQR-Cr_nZ_jR.js → pegDiagram-2B236MQR-BHsKXKhK.js} +1 -1
  68. package/web/dist/assets/{pieDiagram-ENE6RG2P-CcAbSIcY.js → pieDiagram-ENE6RG2P-D3EvWTdH.js} +1 -1
  69. package/web/dist/assets/pilotswarm-UV6T1_QI.js +98 -0
  70. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-CgJw5laM.js → quadrantDiagram-ABIIQ3AL-CbkxfLkV.js} +1 -1
  71. package/web/dist/assets/{railroadDiagram-RFXS5EU6-CHeun9SN.js → railroadDiagram-RFXS5EU6-H4M6JSDW.js} +1 -1
  72. package/web/dist/assets/{requirementDiagram-TGXJPOKE-D1kpDj8W.js → requirementDiagram-TGXJPOKE-BJo7JW1v.js} +1 -1
  73. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-C5vUSUaX.js → sankeyDiagram-HTMAVEWB-DL19MeOP.js} +1 -1
  74. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-T1aPSxk1.js → sequenceDiagram-DBY2YBRQ-DyVFc5pQ.js} +1 -1
  75. package/web/dist/assets/{sizeCapture-X5ZJPWSS-j-wh2LVX.js → sizeCapture-X5ZJPWSS-CKbcKIEQ.js} +1 -1
  76. package/web/dist/assets/{stateDiagram-2N3HPSRC-DfUZZR8N.js → stateDiagram-2N3HPSRC-BSukSXzS.js} +1 -1
  77. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-BhKG3pJB.js +1 -0
  78. package/web/dist/assets/{swimlanes-5IMT3BWC-OpASJiRw.js → swimlanes-5IMT3BWC-BUd7UhsF.js} +2 -2
  79. package/web/dist/assets/swimlanesDiagram-G3AALYLV-CgoFkXJE.js +8 -0
  80. package/web/dist/assets/{timeline-definition-FHXFAJF6-BBnc_z1U.js → timeline-definition-FHXFAJF6-Dt1gludn.js} +1 -1
  81. package/web/dist/assets/{vennDiagram-L72KCM5P-BlPQe9p4.js → vennDiagram-L72KCM5P-BHxt00GV.js} +1 -1
  82. package/web/dist/assets/{wardleyDiagram-EHGQE667-DBcBFUx2.js → wardleyDiagram-EHGQE667-BT3fIDbe.js} +1 -1
  83. package/web/dist/assets/{xychartDiagram-FW5EYKEG-BQUSGL6g.js → xychartDiagram-FW5EYKEG-ChoYDS2b.js} +1 -1
  84. package/web/dist/index.html +3 -3
  85. package/web/runtime.js +89 -44
  86. package/web/server.js +9 -0
  87. package/web/dist/assets/channel-BIn92AzB.js +0 -1
  88. package/web/dist/assets/classDiagram-OUVF2IWQ-B7ktW6XM.js +0 -1
  89. package/web/dist/assets/classDiagram-v2-EOCWNBFH-B7ktW6XM.js +0 -1
  90. package/web/dist/assets/index-rPji8Uud.js +0 -24
  91. package/web/dist/assets/pilotswarm--kvfB_6q.js +0 -99
  92. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-ilKtTdHG.js +0 -1
  93. package/web/dist/assets/swimlanesDiagram-G3AALYLV-C1PthnB4.js +0 -8
@@ -1,7 +1,8 @@
1
1
  import React from "react";
2
- // createPortal is only invoked when an IconButton tooltip renders (portal only,
3
- // never in the TUI); the import itself is side-effect-free and react-dom is a
4
- // dependency wherever this file loads, so it is safe in the shared module.
2
+ // createPortal is only invoked by browser-only surfaces (tooltips, toolbar
3
+ // slots, and viewport-level dialogs); the import itself is side-effect-free
4
+ // and react-dom is a dependency wherever this file loads, so it is safe in the
5
+ // shared module.
5
6
  import { createPortal } from "react-dom";
6
7
  import { appendAnimatedDotsToRuns, useAnimatedDots, useSpinnerFrame } from "./chat-status.js";
7
8
  import {
@@ -25,6 +26,7 @@ import {
25
26
  isThemeLight,
26
27
  normalizeStoredCanvasPrefs,
27
28
  parseTerminalMarkupRuns,
29
+ parseMarkdownLines,
28
30
  PilotSwarmUiController,
29
31
  tokenizeInlineMarkdown,
30
32
  selectActivityPane,
@@ -818,6 +820,12 @@ function normalizeLines(lines) {
818
820
  };
819
821
 
820
822
  for (const line of lines || []) {
823
+ if (line?.kind === "assistantPreview") {
824
+ // Preserve selector-cache identity so another stream's ticks don't
825
+ // rerender every completed response or recreate its scroll observer.
826
+ normalized.push(line);
827
+ continue;
828
+ }
821
829
  if (line?.kind === "markup") {
822
830
  // Markup lines carry preformatted art (splash screens). Tag them
823
831
  // so the chat renderer keeps each line intact: the pane's
@@ -1255,7 +1263,7 @@ function Runs({ runs, theme }) {
1255
1263
 
1256
1264
  if (artifactRef) {
1257
1265
  return React.createElement(ArtifactLink, {
1258
- key: `${index}:${run.text || ""}`,
1266
+ key: index,
1259
1267
  artifactRef,
1260
1268
  style,
1261
1269
  });
@@ -1268,7 +1276,7 @@ function Runs({ runs, theme }) {
1268
1276
 
1269
1277
  return isExternalHref
1270
1278
  ? React.createElement("a", {
1271
- key: `${index}:${run.text || ""}`,
1279
+ key: index,
1272
1280
  className: "ps-md-link",
1273
1281
  href,
1274
1282
  target: "_blank",
@@ -1276,7 +1284,8 @@ function Runs({ runs, theme }) {
1276
1284
  style,
1277
1285
  }, text)
1278
1286
  : React.createElement("span", {
1279
- key: `${index}:${run.text || ""}`,
1287
+ key: index,
1288
+ className: run?.role === "streamingCaret" ? "ps-streaming-caret" : undefined,
1280
1289
  style,
1281
1290
  }, text);
1282
1291
  }),
@@ -1284,14 +1293,16 @@ function Runs({ runs, theme }) {
1284
1293
  }
1285
1294
 
1286
1295
  function SystemNoticeLine({ line, theme }) {
1296
+ const [open, setOpen] = React.useState(false);
1287
1297
  const body = String(line?.body || "").trim();
1288
- return React.createElement("details", { className: "ps-system-notice" },
1298
+ const className = `ps-system-notice${line?.liveReasoning || line?.streamReasoning ? " is-live-reasoning" : ""}`;
1299
+ return React.createElement("details", { className, onToggle: (event) => setOpen(event.currentTarget.open) },
1289
1300
  React.createElement("summary", {
1290
1301
  className: "ps-system-notice-summary",
1291
1302
  style: { color: resolveColor(theme, line?.color) || "var(--ps-muted)" },
1292
1303
  },
1293
1304
  React.createElement("span", { className: "ps-system-notice-summary-text" }, line?.text || "System notice")),
1294
- body
1305
+ body && open
1295
1306
  ? React.createElement("div", { className: "ps-system-notice-body" },
1296
1307
  React.createElement(MarkdownPreviewContent, { content: body, theme }))
1297
1308
  : null);
@@ -3185,6 +3196,12 @@ function parseStructuredChatBlocks(lines = []) {
3185
3196
  for (let index = 0; index < lines.length;) {
3186
3197
  const currentLine = lines[index];
3187
3198
 
3199
+ if (currentLine?.kind === "assistantPreview") {
3200
+ blocks.push({ type: "assistantPreview", line: currentLine });
3201
+ index += 1;
3202
+ continue;
3203
+ }
3204
+
3188
3205
  // Preformatted markup lines (splash art) bypass box/table/card
3189
3206
  // detection entirely and render as one preserve block, so no line
3190
3207
  // ever rewraps or gets reinterpreted as a structured element.
@@ -3216,6 +3233,7 @@ function parseStructuredChatBlocks(lines = []) {
3216
3233
  }
3217
3234
  blocks.push({
3218
3235
  type: "card",
3236
+ cardKey: currentLine.cardKey || null,
3219
3237
  headerRuns: Array.isArray(currentLine.runs) ? currentLine.runs : [],
3220
3238
  borderColor: currentLine.borderColor || "gray",
3221
3239
  blocks: parseStructuredChatBlocks(innerLines),
@@ -3489,12 +3507,129 @@ function ArtifactImageStrip({ controller, sessionId, attachments }) {
3489
3507
  }));
3490
3508
  }
3491
3509
 
3510
+ const PreviewMessageContent = React.memo(function PreviewMessageContent({ content, width, theme, controller }) {
3511
+ const lines = React.useMemo(
3512
+ () => normalizeLines(parseMarkdownLines(content, { width, tableMode: "sentinel" })),
3513
+ [content, width],
3514
+ );
3515
+ return React.createElement(StructuredChatBlocks, { lines, theme, controller });
3516
+ });
3517
+
3518
+ const AssistantPreviewCard = React.memo(function AssistantPreviewCard({ line, theme, controller }) {
3519
+ const [open, setOpen] = React.useState(false);
3520
+ const [revealed, setRevealed] = React.useState(
3521
+ !line.liveStartedAt || Date.now() - line.liveStartedAt >= 200,
3522
+ );
3523
+ const viewportRef = React.useRef(null);
3524
+ const contentRef = React.useRef(null);
3525
+ const followRef = React.useRef(true);
3526
+ const snapshotRef = React.useRef(null);
3527
+ const expanded = open || line.final;
3528
+ // Keep already-mounted markdown/disclosures when closed, but don't parse
3529
+ // new hidden deltas. Reopening catches up in a single render.
3530
+ if (expanded) snapshotRef.current = line;
3531
+ const snapshot = snapshotRef.current;
3532
+
3533
+ React.useEffect(() => {
3534
+ if (line.final || revealed) return undefined;
3535
+ const timer = setTimeout(() => setRevealed(true), Math.max(0, Math.min(200, 200 - (Date.now() - line.liveStartedAt))));
3536
+ return () => clearTimeout(timer);
3537
+ }, [line.final, line.liveStartedAt, revealed]);
3538
+
3539
+ React.useLayoutEffect(() => {
3540
+ if (!expanded || line.final) return undefined;
3541
+ const follow = () => {
3542
+ const viewport = viewportRef.current;
3543
+ if (viewport && line.isLive && followRef.current) viewport.scrollTop = viewport.scrollHeight;
3544
+ };
3545
+ follow();
3546
+ // Images, tables and an opened reasoning disclosure may resize after
3547
+ // the delta render. Only the inner viewport follows their growth.
3548
+ const observer = new ResizeObserver(follow);
3549
+ if (contentRef.current) observer.observe(contentRef.current);
3550
+ return () => observer.disconnect();
3551
+ }, [expanded, line.final, line.isLive, snapshot]);
3552
+
3553
+ // A fast durable answer can beat the preview reveal too. Never flash a
3554
+ // one-frame "Saved" card between its last delta and turn completion.
3555
+ if (!line.final && !revealed) return null;
3556
+
3557
+ return React.createElement("details", {
3558
+ className: `ps-system-notice ps-assistant-preview${line.final ? " is-final" : ""}`,
3559
+ open: expanded,
3560
+ onToggle: (event) => {
3561
+ if (event.target === event.currentTarget && !line.final) setOpen(event.currentTarget.open);
3562
+ },
3563
+ },
3564
+ React.createElement("summary", {
3565
+ className: "ps-system-notice-summary ps-canvas-action-summary",
3566
+ // The completed answer is ordinary transcript, not a collapsible
3567
+ // preview. Keep this same shell/body mounted through promotion.
3568
+ tabIndex: line.final ? -1 : undefined,
3569
+ onClick: (event) => { if (line.final) event.preventDefault(); },
3570
+ }, line.final
3571
+ ? React.createElement(Runs, { runs: line.headerRuns, theme })
3572
+ : React.createElement(React.Fragment, null,
3573
+ React.createElement("span", { className: "ps-canvas-action-tag" }, "Agent"),
3574
+ React.createElement("span", { className: "ps-system-notice-summary-text" }, "Message preview"),
3575
+ React.createElement("span", { className: "ps-preview-status" },
3576
+ line.isLive ? (line.body ? "Responding" : "Thinking") : "Saved"))),
3577
+ React.createElement("div", {
3578
+ ref: viewportRef,
3579
+ className: "ps-assistant-preview-viewport",
3580
+ tabIndex: expanded && !line.final ? 0 : undefined,
3581
+ role: line.final ? undefined : "region",
3582
+ "aria-label": line.final ? undefined : "Agent message preview",
3583
+ onScroll: (event) => {
3584
+ if (!line.final) {
3585
+ event.stopPropagation();
3586
+ followRef.current = getScrollDistanceToBottom(event.currentTarget) <= 24;
3587
+ }
3588
+ },
3589
+ onWheel: (event) => {
3590
+ if (!line.final) {
3591
+ event.stopPropagation();
3592
+ if (event.deltaY < 0) followRef.current = false;
3593
+ }
3594
+ },
3595
+ onTouchStart: (event) => {
3596
+ if (!line.final) { event.stopPropagation(); followRef.current = false; }
3597
+ },
3598
+ onTouchMove: (event) => { if (!line.final) event.stopPropagation(); },
3599
+ onTouchEnd: (event) => { if (!line.final) event.stopPropagation(); },
3600
+ onKeyDown: (event) => {
3601
+ if (!line.final && ["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "].includes(event.key)) {
3602
+ event.stopPropagation();
3603
+ if (["ArrowUp", "PageUp", "Home"].includes(event.key)) followRef.current = false;
3604
+ }
3605
+ },
3606
+ }, React.createElement("div", { ref: contentRef, className: "ps-assistant-preview-content" },
3607
+ snapshot?.reasoningText ? React.createElement(SystemNoticeLine, {
3608
+ key: "reasoning",
3609
+ line: { text: line.isLive ? "Thinking" : "Thought", body: snapshot.reasoningText, streamReasoning: true },
3610
+ theme,
3611
+ }) : null,
3612
+ snapshot?.body ? React.createElement(PreviewMessageContent, {
3613
+ key: "message", content: snapshot.body, width: snapshot.width, theme, controller,
3614
+ }) : null,
3615
+ snapshot?.truncated && line.isLive
3616
+ ? React.createElement("div", { className: "ps-preview-status" }, "Preview paused — the full answer will appear when complete.")
3617
+ : null)));
3618
+ });
3492
3619
  // Renders parsed chat blocks; sentinel card blocks recurse through this list
3493
3620
  // so structured content (box/markdown tables, code fences) inside a card
3494
3621
  // renders exactly the same as it does at top level.
3495
3622
  function StructuredBlockList({ blocks, theme, controller = null }) {
3496
3623
  return React.createElement(React.Fragment, null,
3497
3624
  (blocks || []).map((block, index) => {
3625
+ if (block.type === "assistantPreview") {
3626
+ return React.createElement(AssistantPreviewCard, {
3627
+ key: block.line.previewKey,
3628
+ line: block.line,
3629
+ theme,
3630
+ controller,
3631
+ });
3632
+ }
3498
3633
  if (block.type === "imageAttachments") {
3499
3634
  return React.createElement(ArtifactImageStrip, {
3500
3635
  key: `imageAttachments:${index}`,
@@ -3538,7 +3673,7 @@ function StructuredBlockList({ blocks, theme, controller = null }) {
3538
3673
 
3539
3674
  if (block.type === "card") {
3540
3675
  return React.createElement("section", {
3541
- key: `card:${index}`,
3676
+ key: `card:${block.cardKey || index}`,
3542
3677
  className: "ps-chat-card",
3543
3678
  style: { "--ps-chat-card-accent": resolveColor(theme, block.borderColor) || "var(--ps-border)" },
3544
3679
  },
@@ -3812,7 +3947,7 @@ function focusRegionForPaneKey(paneKey, override = null) {
3812
3947
  return PANE_KEY_FOCUS_REGIONS[String(paneKey || "")] || null;
3813
3948
  }
3814
3949
 
3815
- function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, lines, stickyLines = [], bottomStickyLines = [], scrollOffset = 0, scrollMode = "top", paneKey, controller, className = "", panelClassName = "", topContent = null, bottomContent = null, structuredBlocks = false, stickyBottom = false, renderBody = null, focusRegion = null, panelRef = null }) {
3950
+ function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, lines, stickyLines = [], bottomStickyLines = [], scrollOffset = 0, scrollMode = "top", paneKey, controller, className = "", panelClassName = "", topContent = null, bottomContent = null, structuredBlocks = false, stickyBottom = false, renderBody = null, focusRegion = null, panelRef = null, ariaLive = null }) {
3816
3951
  const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
3817
3952
  const theme = getTheme(themeId);
3818
3953
  const ref = React.useRef(null);
@@ -3924,7 +4059,7 @@ function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, l
3924
4059
  normalizedSticky.map((line, index) => React.createElement(Line, { key: `sticky:${index}`, line, theme })),
3925
4060
  )
3926
4061
  : null,
3927
- React.createElement("div", { ref: setPanelNode, className: `ps-scroll-panel ${className}${scrollShadow.down ? " is-scrolled-down" : ""}${scrollShadow.up ? " is-scrolled-up" : ""}`.trim(), "data-session-scroll": focusRegion === "sessions" ? "1" : undefined, onScroll: handleBodyScroll, onMouseDown: claimFocus, onTouchStart, onWheel, onTouchMove, onTouchEnd, onTouchCancel: onTouchEnd },
4062
+ React.createElement("div", { ref: setPanelNode, className: `ps-scroll-panel ${className}${scrollShadow.down ? " is-scrolled-down" : ""}${scrollShadow.up ? " is-scrolled-up" : ""}`.trim(), "data-session-scroll": focusRegion === "sessions" ? "1" : undefined, "aria-live": ariaLive || undefined, "aria-atomic": ariaLive ? "false" : undefined, onScroll: handleBodyScroll, onMouseDown: claimFocus, onTouchStart, onWheel, onTouchMove, onTouchEnd, onTouchCancel: onTouchEnd },
3928
4063
  typeof renderBody === "function"
3929
4064
  ? renderBody(normalizedLines, theme)
3930
4065
  : structuredBlocks
@@ -4422,13 +4557,48 @@ function StatusDot({ color, node = false }) {
4422
4557
  * person cannot look like two different people in two panes.
4423
4558
  */
4424
4559
  function OwnerAvatar({ badge, size = "sm" }) {
4560
+ // The full name on hover, drawn the way the toolbar tooltips are (a
4561
+ // fixed-position label portalled to the body) rather than the browser's
4562
+ // native title: that one is slow to appear, easy to miss, and styled by
4563
+ // the OS rather than the app.
4564
+ const [tip, setTip] = React.useState(null);
4565
+ const ref = React.useRef(null);
4566
+ const tipRef = React.useRef(null);
4567
+ React.useLayoutEffect(() => {
4568
+ if (!tip || !tipRef.current || typeof window === "undefined") return;
4569
+ const half = tipRef.current.offsetWidth / 2;
4570
+ const margin = 6;
4571
+ tipRef.current.style.left = `${Math.max(half + margin, Math.min(tip.x, window.innerWidth - half - margin))}px`;
4572
+ }, [tip]);
4425
4573
  if (!badge) return null;
4426
- return React.createElement("span", {
4427
- className: `ps-owner-avatar is-${size}${badge.isMine ? " is-mine" : ""}`,
4428
- "data-owner-hue": String(badge.hue ?? 0),
4429
- title: badge.isMine ? `${badge.name} (you)` : badge.name,
4430
- "aria-label": badge.name,
4431
- }, badge.initials);
4574
+ const label = badge.isMine ? `${badge.name} (you)` : badge.name;
4575
+ const show = () => {
4576
+ const el = ref.current;
4577
+ if (!el || typeof window === "undefined") return;
4578
+ const r = el.getBoundingClientRect();
4579
+ const below = r.bottom + 44 < window.innerHeight;
4580
+ setTip({ x: r.left + r.width / 2, y: below ? r.bottom + 6 : r.top - 6, placement: below ? "below" : "above" });
4581
+ };
4582
+ const hide = () => setTip(null);
4583
+ return React.createElement(React.Fragment, null,
4584
+ React.createElement("span", {
4585
+ ref,
4586
+ className: `ps-owner-avatar is-${size}${badge.isMine ? " is-mine" : ""}`,
4587
+ "data-owner-hue": String(badge.hue ?? 0),
4588
+ "aria-label": label,
4589
+ onMouseEnter: show,
4590
+ onMouseLeave: hide,
4591
+ onFocus: show,
4592
+ onBlur: hide,
4593
+ }, badge.initials),
4594
+ tip && typeof document !== "undefined"
4595
+ ? createPortal(React.createElement("span", {
4596
+ ref: tipRef,
4597
+ className: `ps-icon-tooltip is-${tip.placement}`,
4598
+ role: "tooltip",
4599
+ style: { top: `${tip.y}px` },
4600
+ }, label), document.body)
4601
+ : null);
4432
4602
  }
4433
4603
 
4434
4604
  /** Drop the TUI-only text chip; the portal draws OwnerAvatar instead. */
@@ -4550,17 +4720,14 @@ function useStableValue(value) {
4550
4720
  * vote on the diagonal. Vertical stays the default: sideways has to be asked
4551
4721
  * for, by a drag that beats vertical by PAN_HORIZONTAL_RATIO.
4552
4722
  *
4553
- * Driving the scroll ourselves means we also own the part the browser was
4554
- * doing for free, so a flick decays under `releaseFling` instead of stopping
4555
- * dead under the finger. CSS keeps `touch-action: pan-y` as a floor: if these
4723
+ * The list tracks the finger and stops on release: no inertial fling. Chat
4724
+ * and canvas scrolling are independent and keep their native behavior.
4725
+ * CSS keeps `touch-action: pan-y` as a floor: if these
4556
4726
  * listeners ever fail to attach, the list still scrolls vertically the native
4557
4727
  * way rather than becoming inert.
4558
4728
  */
4559
4729
  const PAN_COMMIT_PX = 10; // ignore the first few px — every swipe starts noisy
4560
4730
  const PAN_HORIZONTAL_RATIO = 1.6; // |dx| must beat |dy| by this much to go sideways
4561
- const FLING_MIN_VELOCITY = 0.05; // px/ms below which a lift is a stop, not a flick
4562
- const FLING_FRICTION = 0.95; // per frame; ~0.35s of glide from a brisk flick
4563
- const FLING_MAX_VELOCITY = 4; // px/ms ceiling, so a fast flick stays catchable
4564
4731
 
4565
4732
  /**
4566
4733
  * Which axis does this gesture own? Pure, and exported so the rule can be
@@ -4594,38 +4761,20 @@ function useAxisLockedPan(ref, enabled = true) {
4594
4761
  let startY = 0;
4595
4762
  let lastX = 0;
4596
4763
  let lastY = 0;
4597
- let lastT = 0;
4598
- let velocity = 0; // px/ms along the committed axis, sign follows the finger
4599
4764
  let axis = null; // null = undecided, "x" | "y" = committed, "done" = not ours
4600
- let flingFrame = null;
4601
4765
  let swallowClick = false;
4602
-
4603
- const stopFling = () => {
4604
- if (flingFrame !== null) {
4605
- cancelAnimationFrame(flingFrame);
4606
- flingFrame = null;
4607
- }
4608
- };
4609
-
4610
- const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
4611
4766
  const canScroll = (which) => (which === "x"
4612
4767
  ? el.scrollWidth > el.clientWidth + 1
4613
4768
  : el.scrollHeight > el.clientHeight + 1);
4614
4769
 
4615
4770
  const onTouchStart = (event) => {
4616
- // A tap that lands while the list is still gliding is "stop", not
4617
- // "open whatever is passing under my finger". Native momentum never
4618
- // opened a row on the arresting tap; ours must not either.
4619
- swallowClick = flingFrame !== null;
4620
- stopFling();
4771
+ swallowClick = false;
4621
4772
  // targetTouches, not touches: a thumb resting elsewhere on the page
4622
4773
  // is not part of this gesture. Two fingers HERE is a pinch.
4623
4774
  if (event.targetTouches.length !== 1) { axis = "done"; return; }
4624
4775
  const touch = event.targetTouches[0];
4625
4776
  startX = lastX = touch.clientX;
4626
4777
  startY = lastY = touch.clientY;
4627
- lastT = now();
4628
- velocity = 0;
4629
4778
  axis = null;
4630
4779
  };
4631
4780
 
@@ -4668,7 +4817,8 @@ function useAxisLockedPan(ref, enabled = true) {
4668
4817
  // Measure from here, so the slop we ignored is not also applied.
4669
4818
  lastX = touch.clientX;
4670
4819
  lastY = touch.clientY;
4671
- lastT = now();
4820
+ // A drag must not select whichever row ends up under the thumb.
4821
+ swallowClick = true;
4672
4822
  }
4673
4823
 
4674
4824
  // Ours: stop the browser doing it as well. A repeat of the
@@ -4678,39 +4828,12 @@ function useAxisLockedPan(ref, enabled = true) {
4678
4828
 
4679
4829
  const stepX = touch.clientX - lastX;
4680
4830
  const stepY = touch.clientY - lastY;
4681
- const t = now();
4682
- const elapsed = Math.max(1, t - lastT);
4683
- const step = axis === "x" ? stepX : stepY;
4684
4831
 
4685
4832
  if (axis === "x") el.scrollLeft -= stepX;
4686
4833
  else el.scrollTop -= stepY;
4687
4834
 
4688
- // Exponential smoothing: one jittery sample should not decide the
4689
- // whole flick, and a finger that stops before lifting should decay
4690
- // to zero rather than fling on a stale reading.
4691
- const sample = Math.max(-FLING_MAX_VELOCITY, Math.min(FLING_MAX_VELOCITY, step / elapsed));
4692
- velocity = velocity * 0.7 + sample * 0.3;
4693
-
4694
4835
  lastX = touch.clientX;
4695
4836
  lastY = touch.clientY;
4696
- lastT = t;
4697
- };
4698
-
4699
- const releaseFling = (committed) => {
4700
- if (Math.abs(velocity) < FLING_MIN_VELOCITY) return;
4701
- let v = velocity;
4702
- const tick = () => {
4703
- v *= FLING_FRICTION;
4704
- if (Math.abs(v) < FLING_MIN_VELOCITY) { flingFrame = null; return; }
4705
- const before = committed === "x" ? el.scrollLeft : el.scrollTop;
4706
- if (committed === "x") el.scrollLeft -= v * 16;
4707
- else el.scrollTop -= v * 16;
4708
- const after = committed === "x" ? el.scrollLeft : el.scrollTop;
4709
- // Hit an end stop — no point animating against it.
4710
- if (after === before) { flingFrame = null; return; }
4711
- flingFrame = requestAnimationFrame(tick);
4712
- };
4713
- flingFrame = requestAnimationFrame(tick);
4714
4837
  };
4715
4838
 
4716
4839
  const onTouchEnd = (event) => {
@@ -4718,16 +4841,13 @@ function useAxisLockedPan(ref, enabled = true) {
4718
4841
  // its start point belongs to a different gesture, and the commit
4719
4842
  // would land on a large bogus delta. Stay out until all lift.
4720
4843
  if (event.targetTouches.length > 0) { axis = "done"; return; }
4721
- const committed = axis;
4722
4844
  axis = null;
4723
- if (committed === "x" || committed === "y") releaseFling(committed);
4724
- velocity = 0;
4725
4845
  };
4726
4846
 
4727
- const onTouchCancel = () => { axis = null; velocity = 0; stopFling(); };
4847
+ const onTouchCancel = () => { axis = "done"; };
4728
4848
 
4729
4849
  const onClickCapture = (event) => {
4730
- if (!swallowClick) return;
4850
+ if (!swallowClick || event.detail === 0) return;
4731
4851
  swallowClick = false;
4732
4852
  event.preventDefault();
4733
4853
  event.stopPropagation();
@@ -4739,7 +4859,6 @@ function useAxisLockedPan(ref, enabled = true) {
4739
4859
  el.addEventListener("touchcancel", onTouchCancel, { passive: true });
4740
4860
  el.addEventListener("click", onClickCapture, true);
4741
4861
  return () => {
4742
- stopFling();
4743
4862
  el.removeEventListener("touchstart", onTouchStart);
4744
4863
  el.removeEventListener("touchmove", onTouchMove);
4745
4864
  el.removeEventListener("touchend", onTouchEnd);
@@ -4886,6 +5005,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
4886
5005
  const authPrincipal = viewState.auth?.principal || null;
4887
5006
  const viewerRole = viewState.auth?.authorization?.role;
4888
5007
  const isAdminViewer = viewerRole === "admin" || viewerRole === "anonymous";
5008
+ const resourceAdminViewer = isAdminViewer && viewState.auth?.adminScope !== "cluster";
4889
5009
  const ownsActiveSession = Boolean(
4890
5010
  activeSession?.owner
4891
5011
  && authPrincipal
@@ -4906,7 +5026,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
4906
5026
  activeSession && !activeSession.isGroup
4907
5027
  && (activeSession.isSystem
4908
5028
  ? isAdminViewer
4909
- : (isAdminViewer || ownsActiveSession)),
5029
+ : (resourceAdminViewer || ownsActiveSession)),
4910
5030
  );
4911
5031
  const selectedCount = Array.isArray(viewState.selectedIds) ? viewState.selectedIds.length : 0;
4912
5032
  const isBulkSelection = selectedCount > 1;
@@ -6398,6 +6518,7 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
6398
6518
  scrollOffset: viewState.scroll,
6399
6519
  scrollMode: "bottom",
6400
6520
  paneKey: "chat",
6521
+ ariaLive: "polite",
6401
6522
  className: "is-wrapped",
6402
6523
  panelClassName: "ps-chat-panel",
6403
6524
  panelRef,
@@ -6877,8 +6998,19 @@ function CanvasShareDialog({ controller, sessionId, slot, onClose }) {
6877
6998
  // the policy, so the control is disabled for everyone else rather than
6878
6999
  // letting them pick a value and then showing a refusal.
6879
7000
  const [kvMe, setKvMe] = React.useState(null);
7001
+ // Off by default: every link this dialog has ever produced opened the full
7002
+ // workspace, and a share option that silently changed that for existing
7003
+ // habits would be a surprise. Checked, the link carries `show_chrome=false`
7004
+ // and lands on the canvas alone.
7005
+ //
7006
+ // Presentation only. It hides the portal header; it does NOT make the
7007
+ // canvas read-only, because a query parameter is not a permission — the
7008
+ // recipient can simply delete it. What a viewer may change inside the app
7009
+ // stays governed by the KV policy below, and the strip is worded so it
7010
+ // never implies otherwise.
7011
+ const [hideChrome, setHideChrome] = React.useState(false);
6880
7012
  const transport = controller.transport;
6881
- const sessionLink = `${window.location.origin}/?session=${encodeURIComponent(sessionId)}&view=canvas&slot=${slot}&max=1`;
7013
+ const sessionLink = `${window.location.origin}/?session=${encodeURIComponent(sessionId)}&view=canvas&slot=${slot}&max=1${hideChrome ? "&show_chrome=false" : ""}`;
6882
7014
  // The token variants all resolve the same hashed row: one token, N doors,
6883
7015
  // single-switch revocation whichever door the bearer uses.
6884
7016
  const linkRows = (url, keyPrefix) => React.createElement(MultiOriginLinkRows, {
@@ -6933,7 +7065,7 @@ function CanvasShareDialog({ controller, sessionId, slot, onClose }) {
6933
7065
  .finally(() => setBusy(false));
6934
7066
  };
6935
7067
 
6936
- return React.createElement("div", { className: "ps-canvas-share-overlay", onClick: onClose },
7068
+ const overlay = React.createElement("div", { className: "ps-canvas-share-overlay", onClick: onClose },
6937
7069
  React.createElement("div", { className: "ps-canvas-share-dialog", onClick: (e) => e.stopPropagation(), role: "dialog", "aria-label": "Share canvas" },
6938
7070
  React.createElement("div", { className: "ps-canvas-share-title" },
6939
7071
  "Share this canvas",
@@ -6965,6 +7097,13 @@ function CanvasShareDialog({ controller, sessionId, slot, onClose }) {
6965
7097
  },
6966
7098
  React.createElement("div", { className: "ps-canvas-share-sub" }, "Opens the portal signed in, canvas full screen. Session visibility rules apply."),
6967
7099
  linkRows(sessionLink, "session"),
7100
+ React.createElement("label", { className: "ps-canvas-share-check" },
7101
+ React.createElement("input", {
7102
+ type: "checkbox",
7103
+ checked: hideChrome,
7104
+ onChange: (event) => setHideChrome(event.target.checked),
7105
+ }),
7106
+ "Hide page chrome"),
6968
7107
  // Who may WRITE the app's shared state. Stated in words:
6969
7108
  // "readers" reaches everyone the session is read-shared
6970
7109
  // with, which a dialog that only says "link" hides.
@@ -7016,6 +7155,16 @@ function CanvasShareDialog({ controller, sessionId, slot, onClose }) {
7016
7155
  ? React.createElement("button", { type: "button", className: "ps-mini-button", disabled: busy, onClick: remove }, "Remove link")
7017
7156
  : null))),
7018
7157
  error ? React.createElement("div", { className: "ps-canvas-share-error" }, error) : null));
7158
+
7159
+ // CanvasPane lives inside `.ps-canvas-layer`, which is a z-indexed
7160
+ // stacking context below the pane resizers. A fixed overlay left inside
7161
+ // that tree cannot out-rank a sibling resizer regardless of its own
7162
+ // z-index, so the divider used to paint straight through this dialog.
7163
+ // Put viewport chrome at the document root; React keeps context and event
7164
+ // bubbling intact across the portal.
7165
+ return typeof document !== "undefined" && document.body
7166
+ ? createPortal(overlay, document.body)
7167
+ : overlay;
7019
7168
  }
7020
7169
 
7021
7170
  /**
@@ -12888,7 +13037,8 @@ function AdminConsolePanel({ controller, mobile = false }) {
12888
13037
  // the way out is the toolbar's Mode cluster (Workspace) \u2014 a \u2715 here was a
12889
13038
  // second, unlabelled way to do the same thing.
12890
13039
  const header = React.createElement("header", { className: "ps-admin-console__header" },
12891
- React.createElement("h2", null, view.isAdmin ? "Admin Console" : "Settings"));
13040
+ React.createElement("h2", null, view.isAdmin ? "Admin Console" : "Settings"),
13041
+ view.isAdmin ? React.createElement("small", null, view.adminPolicyLabel) : null);
12892
13042
 
12893
13043
  const providerSection = React.createElement(AdminModelProvidersSection, {
12894
13044
  controller,