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
@@ -35,7 +35,6 @@ import {
35
35
  normalizeArtifactEntries,
36
36
  normalizeSessionPause,
37
37
  } from "./state.js";
38
- import { isRecoverableTransportErrorText } from "./session-errors.js";
39
38
 
40
39
  export const ACTIVE_HIGHLIGHT_BACKGROUND = "activeHighlightBackground";
41
40
  export const ACTIVE_HIGHLIGHT_FOREGROUND = "activeHighlightForeground";
@@ -297,9 +296,15 @@ function isTerminalOrchestrationStatus(status) {
297
296
 
298
297
  function getSessionErrorVisualKind(session) {
299
298
  const status = getSessionVisualStatus(session);
300
- if (session?.orchestrationStatus === "Failed" || status === "failed") return "failed";
299
+ if (session?.orchestrationStatus === "Failed") return "failed";
300
+ if (isTerminalOrchestrationStatus(session?.orchestrationStatus)) return null;
301
+ if (status === "failed") return "failed";
301
302
  if (status === "error") return "warning";
302
- if (status === "running" && isRecoverableTransportErrorText(session?.error) && !isTerminalOrchestrationStatus(session?.orchestrationStatus)) return "warning";
303
+ // The CMS row may already say idle/waiting while the live detail still
304
+ // carries the retry. Keep its card visible until detail sync clears it.
305
+ if (!["completed", "cancelled", "terminated", "input_required"].includes(session?.status)
306
+ && String(session?.error || "").trim()
307
+ && !isTerminalOrchestrationStatus(session?.orchestrationStatus)) return "warning";
303
308
  return null;
304
309
  }
305
310
 
@@ -457,17 +462,65 @@ export function manualOrderContainerKey(session, pinned = false) {
457
462
  * The one owner-badge descriptor both lists render from. Same person ⇒ same
458
463
  * initials and same colour, wherever they appear.
459
464
  */
460
- export function ownerBadgeFor(owner, { isMine = false } = {}) {
465
+ export function ownerBadgeFor(owner, { isMine = false, hueByKey = null } = {}) {
461
466
  if (!owner) return null;
462
467
  const name = ownerDisplayName(owner, owner?.email || "");
468
+ const key = ownerBadgeKey(owner, owner?.email);
463
469
  return {
464
470
  initials: ownerInitials(owner),
465
- hue: ownerBadgeHue(ownerBadgeKey(owner, owner?.email)),
471
+ // The list-wide assignment when the caller has one (no two people in
472
+ // the same list share a colour); the bare hash otherwise.
473
+ hue: hueByKey?.get?.(key) ?? ownerBadgeHue(key),
466
474
  name: name || "unknown user",
467
475
  isMine: Boolean(isMine),
468
476
  };
469
477
  }
470
478
 
479
+ /**
480
+ * One colour per PERSON in a list, no two alike while the palette lasts.
481
+ *
482
+ * The hash alone puts different people on the same colour far too often
483
+ * (three people, twelve colours: one collision in four), and a badge whose
484
+ * colour does not tell people apart is not doing its job. So: everyone
485
+ * gets their hash hue first; anyone landing on a taken hue walks to the next
486
+ * free one. Walk order is the sorted identity key, so the outcome depends
487
+ * only on WHO is in the list — the same people get the same colours across
488
+ * reloads and in both panes, and a newcomer only ever takes a free colour.
489
+ */
490
+ export function assignOwnerBadgeHues(keys) {
491
+ const sorted = [...new Set([...keys].map((k) => String(k || "").trim().toLowerCase()).filter(Boolean))].sort();
492
+ const taken = new Set();
493
+ const out = new Map();
494
+ for (const key of sorted) {
495
+ let hue = ownerBadgeHue(key);
496
+ if (taken.size < OWNER_BADGE_HUES) {
497
+ let steps = 0;
498
+ while (taken.has(hue) && steps < OWNER_BADGE_HUES) { hue = (hue + 1) % OWNER_BADGE_HUES; steps += 1; }
499
+ }
500
+ taken.add(hue);
501
+ out.set(key, hue);
502
+ }
503
+ return out;
504
+ }
505
+
506
+ // The assignment for every human owner in the session catalog, memoised on
507
+ // the catalog's identity: the row selector runs per row and per dispatch.
508
+ let ownerHueMapCache = null;
509
+ export function ownerHueMapForSessions(byId) {
510
+ if (ownerHueMapCache && ownerHueMapCache.byId === byId) return ownerHueMapCache.map;
511
+ const keys = [];
512
+ for (const session of Object.values(byId || {})) {
513
+ if (!session || session.isSystem || session.isGroup) continue;
514
+ const owner = session.owner;
515
+ const ownerKey = ownerKeyForOwner(owner);
516
+ if (!ownerKey || ownerKey === SYSTEM_OWNER_KEY) continue;
517
+ keys.push(ownerBadgeKey(owner, owner?.email));
518
+ }
519
+ const map = assignOwnerBadgeHues(keys);
520
+ ownerHueMapCache = { byId, map };
521
+ return map;
522
+ }
523
+
471
524
  function ownerDirectoryFromSessions(byId) {
472
525
  const directory = new Map();
473
526
  for (const session of Object.values(byId || {})) {
@@ -507,11 +560,11 @@ function resolvePackageOwner(pkg, directory) {
507
560
  * the badge is telling owners APART at a glance, which two-letter initials
508
561
  * alone do poorly (and did not do at all while they were all "?").
509
562
  *
510
- * Eight hues, all legible on both the light and dark portal grounds; the
563
+ * Twelve hues, all legible on both the light and dark portal grounds; the
511
564
  * viewer's own rows are styled separately by the caller, so this palette
512
565
  * never has to encode "mine".
513
566
  */
514
- const OWNER_BADGE_HUES = 8;
567
+ export const OWNER_BADGE_HUES = 12;
515
568
 
516
569
  export function ownerBadgeHue(key) {
517
570
  const text = String(key || "").trim().toLowerCase();
@@ -1358,6 +1411,7 @@ export function selectSessionRows(state) {
1358
1411
  ? ownerBadgeFor(effectiveSessionOwner(session, state.sessions.byId), {
1359
1412
  isMine: Boolean(ownerKeyForOwner(state?.auth?.principal)
1360
1413
  && ownerKeyForOwner(effectiveSessionOwner(session, state.sessions.byId)) === ownerKeyForOwner(state?.auth?.principal)),
1414
+ hueByKey: ownerHueMapForSessions(state.sessions.byId),
1361
1415
  })
1362
1416
  : null,
1363
1417
  orderable: isManuallyOrderableSession(session),
@@ -1601,7 +1655,7 @@ function buildSessionErrorMessage(session, events = []) {
1601
1655
  : `${errorText}\n\nThe orchestration is still running, so this may be transient.`;
1602
1656
 
1603
1657
  return {
1604
- id: `session-error:${session.sessionId}:${errorKind}:${errorText}`,
1658
+ id: `session-error:${session.sessionId}`,
1605
1659
  role: "system",
1606
1660
  text: body,
1607
1661
  time: "",
@@ -1904,7 +1958,11 @@ function describeChatMessageHeader(message, options = {}) {
1904
1958
  const sharedContext = options?.sharedContext === true;
1905
1959
  const isUser = message?.role === "user";
1906
1960
  const isSelf = isUser && senderKey && viewerKey && senderKey === viewerKey;
1907
- const fromOtherPerson = sharedContext && isUser && senderKey && !isSelf;
1961
+ // A user-stamped message whose key is not the viewer's IS from someone
1962
+ // else, whatever the session's visibility says: a targeted share leaves
1963
+ // the session "private" but still puts another person's messages in the
1964
+ // transcript. Naming them never needs the shared-context gate.
1965
+ const fromOtherPerson = isUser && Boolean(senderKey) && Boolean(viewerKey) && !isSelf;
1908
1966
  // Owner tag: the sender is the session owner (or, for a sender-less own
1909
1967
  // message, the viewer themselves is the owner).
1910
1968
  const senderIsOwner = sharedContext && isUser && ownerKey && (
@@ -2427,7 +2485,22 @@ function splashArtWidth(text) {
2427
2485
  .reduce((widest, line) => Math.max(widest, stripTerminalMarkupTags(line).length), 0);
2428
2486
  }
2429
2487
 
2488
+ // Immutable history items survive live ticks. Do not re-parse thousands of
2489
+ // committed lines because the last message gained a handful of characters.
2490
+ const chatMessageLinesCache = new WeakMap();
2430
2491
  function buildChatMessageLines(message, maxWidth, options = {}) {
2492
+ if (!message || typeof message !== "object") return buildChatMessageLinesUncached(message, maxWidth, options);
2493
+ const key = JSON.stringify([maxWidth, options]);
2494
+ let entries = chatMessageLinesCache.get(message);
2495
+ if (entries?.has(key)) return entries.get(key);
2496
+ const lines = buildChatMessageLinesUncached(message, maxWidth, options);
2497
+ if (!entries) { entries = new Map(); chatMessageLinesCache.set(message, entries); }
2498
+ if (entries.size >= 4) entries.delete(entries.keys().next().value);
2499
+ entries.set(key, lines);
2500
+ return lines;
2501
+ }
2502
+
2503
+ function buildChatMessageLinesUncached(message, maxWidth, options = {}) {
2431
2504
  if (message?.splash) {
2432
2505
  // Swap in the narrow-viewport variant when the main art would
2433
2506
  // overflow the pane (mobile portal, narrow terminals). When the
@@ -2447,6 +2520,36 @@ function buildChatMessageLines(message, maxWidth, options = {}) {
2447
2520
  return buildThinkingCardLines(message, maxWidth);
2448
2521
  }
2449
2522
 
2523
+ // The browser gives an in-flight assistant answer its own stable surface.
2524
+ // Previously it was flattened into the ordinary transcript on every
2525
+ // snapshot. As partial markdown changed shape (plain lines -> table/code
2526
+ // block), React could replace several adjacent nodes and the whole answer
2527
+ // appeared to flash. One keyed card keeps the outer DOM and its shading in
2528
+ // place while only the body grows. The TUI deliberately keeps its existing
2529
+ // line-oriented rendering.
2530
+ if ((message?.liveTurn || message?.streamSettling || message?.assistantPreview) && options.tableMode === "sentinel") {
2531
+ // Carry raw markdown: collapsed previews never parse their hidden body.
2532
+ return [{
2533
+ kind: "assistantPreview",
2534
+ width: maxWidth,
2535
+ text: "Message preview",
2536
+ previewKey: `assistant:${message?.liveKey || message?.messageId || message?.id || "active-turn"}`,
2537
+ body: decorateArtifactLinksForChat(message?.text || ""),
2538
+ reasoningText: String(message?.reasoningText || message?.liveReasoningText || ""),
2539
+ isLive: message?.liveTurn === true,
2540
+ final: message?.responseFinal === true,
2541
+ truncated: message?.truncated === true,
2542
+ liveStartedAt: Number(message?.liveStartedAt || message?.createdAt) || 0,
2543
+ time: message?.time || "",
2544
+ headerRuns: buildChatMessagePrefix(message, options),
2545
+ }];
2546
+ }
2547
+
2548
+ if (message?.liveTurn && options.tableMode !== "sentinel" && message.reasoningText) {
2549
+ const reasoning = buildThinkingCardLines({ ...message, text: message.reasoningText }, maxWidth);
2550
+ return message.text ? [...reasoning, ...buildChatMessageLines({ ...message, reasoningText: "" }, maxWidth, options)] : reasoning;
2551
+ }
2552
+
2450
2553
  // Chrome-less message: render the text as plain markdown directly into
2451
2554
  // the chat pane (no card border, no speaker prefix, no timestamp). Used
2452
2555
  // for the session summary view so the summary feels like the pane's
@@ -2559,6 +2662,7 @@ function buildChatMessageLines(message, maxWidth, options = {}) {
2559
2662
  && (!message?.cardTitle || String(message.cardTitle).toLowerCase() === "system");
2560
2663
  return buildMessageCardLines({
2561
2664
  title: message?.cardTitle || (message?.role === "system" ? "System" : "PilotSwarm"),
2665
+ cardKey: message?.id,
2562
2666
  timestamp: formatTimestamp(message?.createdAt || message?.time),
2563
2667
  body: decorateArtifactLinksForChat(message?.text || ""),
2564
2668
  width: Math.max(20, maxWidth),
@@ -2589,6 +2693,7 @@ function buildChatMessageLines(message, maxWidth, options = {}) {
2589
2693
 
2590
2694
  if (startsWithStructuredBlock(tintedMarkdownLines)) {
2591
2695
  const structured = prefix.length > 0 ? [prefix, ...tintedMarkdownLines] : tintedMarkdownLines;
2696
+ if (message?.streaming) appendStreamingCaret(structured);
2592
2697
  return attachmentChipLines.length > 0 ? [...structured, ...attachmentChipLines] : structured;
2593
2698
  }
2594
2699
 
@@ -2610,10 +2715,23 @@ function buildChatMessageLines(message, maxWidth, options = {}) {
2610
2715
  renderedLines.push(lineRuns);
2611
2716
  }
2612
2717
  }
2718
+ if (message?.streaming) {
2719
+ appendStreamingCaret(renderedLines);
2720
+ }
2613
2721
  renderedLines.push(...attachmentChipLines);
2614
2722
  return renderedLines;
2615
2723
  }
2616
2724
 
2725
+ function appendStreamingCaret(lines) {
2726
+ const lastRuns = [...lines].reverse().find((line) => Array.isArray(line)
2727
+ && line.some((run) => String(run?.text || "").trim()));
2728
+ if (lastRuns) {
2729
+ lastRuns.push({ text: " ▍", color: "cyan", role: "streamingCaret" });
2730
+ } else {
2731
+ lines.push([{ text: "▍", color: "cyan", role: "streamingCaret" }]);
2732
+ }
2733
+ }
2734
+
2617
2735
  // Image attachment chips under a message. In sentinel mode (browser portal)
2618
2736
  // a single structured line carries the refs so the web renderer can show
2619
2737
  // authenticated thumbnails; everywhere else (TUI) each file renders as a
@@ -2730,6 +2848,10 @@ function liveActivityPhaseText(item) {
2730
2848
  case "assistant.intent": return "planning\u2026";
2731
2849
  case "assistant.message_start":
2732
2850
  case "assistant.message": return "writing reply\u2026";
2851
+ case "model.call_start": {
2852
+ const model = String(item?.text || "").replace(/^.*\bcalling\s+/i, "").trim();
2853
+ return `calling ${model || "model"}\u2026`;
2854
+ }
2733
2855
  case "session.compaction_start": return "compacting context\u2026";
2734
2856
  case "session.compaction_complete": return "context compacted";
2735
2857
  case "session.dehydrated": return "dehydrating\u2026";
@@ -2891,12 +3013,19 @@ export function selectChatLines(state, maxWidth = 80, options = {}) {
2891
3013
  const activeSessionId = state?.sessions?.activeSessionId;
2892
3014
  const activeSession = activeSessionId ? state?.sessions?.byId?.[activeSessionId] : null;
2893
3015
  const ownerKey = ownerKeyForOwner(activeSession?.owner);
2894
- // A shared context = the session is shared deployment-wide, or the viewer
2895
- // is not its owner (someone shared it with them). Only then does the
2896
- // transcript name speakers and tag the owner.
3016
+ // A shared context = the session is shared deployment-wide, the viewer is
3017
+ // not its owner (someone shared it with them), or someone other than the
3018
+ // viewer has written to it (the owner shared it with a writer; the session
3019
+ // row still says "private" — targeted shares are not on the row, but the
3020
+ // other person's stamped messages are in the transcript). Only then does
3021
+ // the transcript name speakers and tag the owner.
2897
3022
  const sharedContext = Boolean(activeSession && (
2898
3023
  (activeSession.visibility && activeSession.visibility !== "private")
2899
3024
  || (ownerKey && viewerKey && ownerKey !== viewerKey)
3025
+ || (viewerKey && messages.some((message) => message?.role === "user"
3026
+ && message?.sender?.kind === "user"
3027
+ && ownerKeyForOwner(message.sender)
3028
+ && ownerKeyForOwner(message.sender) !== viewerKey))
2900
3029
  ));
2901
3030
  const buildOptions = {
2902
3031
  ...(options?.tableMode ? { tableMode: options.tableMode } : {}),
@@ -4001,6 +4130,8 @@ export function selectAdminConsole(state) {
4001
4130
 
4002
4131
  const systemGhcpKey = admin.systemGhcpKey || { supported: false, loading: false, configured: false, changedBy: null, changedAt: null, error: null };
4003
4132
  const isAdmin = Boolean(profile?.isAdmin);
4133
+ const adminScope = state.auth?.adminScope ?? profile?.adminScope ?? "unrestricted";
4134
+ const resourceAdmin = isAdmin && adminScope === "unrestricted";
4004
4135
  const modelProviderState = admin.modelProviders || {};
4005
4136
  const providerRows = Array.isArray(modelProviderState.providers) ? modelProviderState.providers : [];
4006
4137
  const modelRows = Array.isArray(modelProviderState.models) ? modelProviderState.models : [];
@@ -4153,7 +4284,7 @@ export function selectAdminConsole(state) {
4153
4284
  const pkgState = admin.packages || {};
4154
4285
  const pkgList = Array.isArray(pkgState.list) ? pkgState.list : [];
4155
4286
  const ownsPackage = (pkg) => Boolean(
4156
- isAdmin
4287
+ resourceAdmin
4157
4288
  || (pkg?.owner && principal
4158
4289
  && pkg.owner.provider === principal.provider
4159
4290
  && pkg.owner.subject === principal.subject),
@@ -4165,6 +4296,8 @@ export function selectAdminConsole(state) {
4165
4296
  const canEditPackage = (pkg) => ownsPackage(pkg) || Boolean(pkg?.canEdit);
4166
4297
  // Built once per render, not per row.
4167
4298
  const ownerDirectory = ownerDirectoryFromSessions(state?.sessions?.byId);
4299
+ // Same assignment as the session list, so the same person is the same colour in both panes.
4300
+ const packageHueByKey = ownerHueMapForSessions(state?.sessions?.byId);
4168
4301
  // Same rule as the session list: the owner chip only earns its place when
4169
4302
  // this list actually holds more than one owner. Shared packages belong to
4170
4303
  // the deployment, not a person, so they neither carry a chip nor count
@@ -4212,7 +4345,7 @@ export function selectAdminConsole(state) {
4212
4345
  // PERSON's, matching the session list exactly.
4213
4346
  ownerBadge: (pkg.scope === "shared" || !decoratePackageOwners)
4214
4347
  ? null
4215
- : ownerBadgeFor(resolvePackageOwner(pkg, ownerDirectory)),
4348
+ : ownerBadgeFor(resolvePackageOwner(pkg, ownerDirectory), { hueByKey: packageHueByKey }),
4216
4349
  // Highlight only while the Agents section is active — otherwise the
4217
4350
  // GitHub Keys screen shows a double selection.
4218
4351
  selected: rowMatchesSelection(pkg),
@@ -4314,8 +4447,11 @@ export function selectAdminConsole(state) {
4314
4447
  ? workerRows.filter((worker) => installedRowFor(worker)?.semver === activeVersion.semver
4315
4448
  && installedRowFor(worker)?.status === "ok").length
4316
4449
  : 0;
4317
- const canManage = detail ? ownsPackage(detail) : (summary ? ownsPackage(summary) : false);
4318
- const canEdit = detail ? canEditPackage(detail) : (summary ? canEditPackage(summary) : false);
4450
+ // The periodically refreshed list carries current editor grants; an
4451
+ // older open detail must not preserve a revoked edit affordance.
4452
+ const permissionSource = summary ?? detail;
4453
+ const canManage = permissionSource ? ownsPackage(permissionSource) : false;
4454
+ const canEdit = permissionSource ? canEditPackage(permissionSource) : false;
4319
4455
  packageDetail = {
4320
4456
  name: pkgState.selectedName,
4321
4457
  loading: Boolean(pkgState.detailLoading),
@@ -4542,6 +4678,8 @@ export function selectAdminConsole(state) {
4542
4678
  loadError: admin.loadError || null,
4543
4679
  principal,
4544
4680
  isAdmin,
4681
+ adminScope,
4682
+ adminPolicyLabel: adminScope === "cluster" ? "Cluster-scoped admin · system-session access retained" : "Unrestricted admin access",
4545
4683
  section,
4546
4684
  settingsTree,
4547
4685
  packages: packagesView,
@@ -4581,6 +4719,8 @@ export function selectAdminConsole(state) {
4581
4719
  loadError: admin.loadError || null,
4582
4720
  principal,
4583
4721
  isAdmin,
4722
+ adminScope,
4723
+ adminPolicyLabel: adminScope === "cluster" ? "Cluster-scoped admin · system-session access retained" : "Unrestricted admin access",
4584
4724
  section,
4585
4725
  settingsTree,
4586
4726
  packages: packagesView,
@@ -4626,6 +4766,8 @@ export function selectAdminConsole(state) {
4626
4766
  loadError: admin.loadError || null,
4627
4767
  principal,
4628
4768
  isAdmin,
4769
+ adminScope,
4770
+ adminPolicyLabel: adminScope === "cluster" ? "Cluster-scoped admin · system-session access retained" : "Unrestricted admin access",
4629
4771
  section,
4630
4772
  settingsTree,
4631
4773
  packages: packagesView,
@@ -4674,6 +4816,8 @@ export function selectAdminConsole(state) {
4674
4816
  loadError: admin.loadError || null,
4675
4817
  principal,
4676
4818
  isAdmin,
4819
+ adminScope,
4820
+ adminPolicyLabel: adminScope === "cluster" ? "Cluster-scoped admin · system-session access retained" : "Unrestricted admin access",
4677
4821
  section,
4678
4822
  settingsTree,
4679
4823
  packages: packagesView,
@@ -1,14 +1,19 @@
1
- export function isRecoverableTransportErrorText(text) {
2
- const value = String(text || "");
3
- return /\bConnection is closed\b/i.test(value)
4
- || /\bLive Copilot connection lost\b/i.test(value);
5
- }
6
-
7
- export function shouldKeepRecoverableTransportWarning(previousSession, nextSession) {
8
- if (!isRecoverableTransportErrorText(previousSession?.error)) return false;
1
+ export function shouldKeepSessionWarning(previousSession, nextSession) {
2
+ if (!String(previousSession?.error || "").trim()) return false;
9
3
  if (nextSession?.error !== undefined) return false;
10
- const nextStatus = String(nextSession?.status || "");
11
- if (nextStatus !== "running") return false;
12
4
  if (["Completed", "Failed", "Terminated"].includes(String(nextSession?.orchestrationStatus || ""))) return false;
13
- return true;
5
+ if (["completed", "failed", "cancelled", "terminated"].includes(previousSession?.status)) return false;
6
+ const nextStatus = String(nextSession?.status || "");
7
+ if (["running", "error"].includes(nextStatus)) return true;
8
+ if (!["idle", "waiting", "unknown", "pending", ""].includes(nextStatus)) return false;
9
+ // Catalog/detail snapshots can lag a retry. A genuinely newer idle detail
10
+ // (or a new durable wait) clears it; a same-age snapshot is not evidence
11
+ // of recovery. Do not infer retry semantics from provider error wording.
12
+ const previousVersion = Number(previousSession?.statusVersion);
13
+ const nextVersion = Number(nextSession?.statusVersion);
14
+ if (previousVersion > 0 && nextVersion > 0) return nextVersion <= previousVersion;
15
+ const time = (value) => typeof value === "number" ? value : Date.parse(value || "");
16
+ const previousAt = time(previousSession?.updatedAt);
17
+ const nextAt = time(nextSession?.updatedAt);
18
+ return previousAt > 0 && nextAt > 0 && nextAt <= previousAt;
14
19
  }
@@ -2180,6 +2180,7 @@ function buildAdminModelProviderLines(view) {
2180
2180
 
2181
2181
  function buildAdminConsoleLines(view) {
2182
2182
  const lines = [];
2183
+ if (view.isAdmin && view.adminPolicyLabel) lines.push([{ text: view.adminPolicyLabel, color: "gray" }]);
2183
2184
  lines.push([
2184
2185
  { text: "Signed in as ", color: "gray" },
2185
2186
  { text: formatAdminPrincipalLabelTui(view.principal), color: "white", bold: true },