pilotswarm 0.5.60 → 0.5.62

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 (90) hide show
  1. package/README.md +3 -2
  2. package/mcp/dist/src/resources/models.d.ts.map +1 -1
  3. package/mcp/dist/src/resources/models.js +6 -1
  4. package/mcp/dist/src/resources/models.js.map +1 -1
  5. package/mcp/dist/src/tools/models.d.ts.map +1 -1
  6. package/mcp/dist/src/tools/models.js +11 -1
  7. package/mcp/dist/src/tools/models.js.map +1 -1
  8. package/mcp/dist/src/tools/sessions.d.ts.map +1 -1
  9. package/mcp/dist/src/tools/sessions.js +8 -2
  10. package/mcp/dist/src/tools/sessions.js.map +1 -1
  11. package/package.json +2 -2
  12. package/tui/src/node-sdk-transport.js +1 -1
  13. package/ui/core/src/chat-activity.js +94 -0
  14. package/ui/core/src/controller.js +50 -5
  15. package/ui/core/src/history.js +15 -0
  16. package/ui/core/src/moa.js +32 -11
  17. package/ui/core/src/reducer.js +63 -51
  18. package/ui/core/src/selectors.js +25 -5
  19. package/ui/core/src/session-errors.js +10 -2
  20. package/ui/core/src/system-titles.js +2 -0
  21. package/ui/react/src/web-app.js +155 -77
  22. package/web/dist/assets/{abnfDiagram-VRR7QNED-CXEHJkTQ.js → abnfDiagram-VRR7QNED-DAiG41W-.js} +1 -1
  23. package/web/dist/assets/{arc-lInHlEVm.js → arc-CN_GY3DG.js} +1 -1
  24. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-vrocx6re.js → architectureDiagram-ZJ3FMSHR-DD9qiBOj.js} +1 -1
  25. package/web/dist/assets/{blockDiagram-677ZJIJ3-B1KA5QIB.js → blockDiagram-677ZJIJ3-CZg2R8i6.js} +1 -1
  26. package/web/dist/assets/{c4Diagram-LMCZKHZV-Du9xJMjz.js → c4Diagram-LMCZKHZV-DuZxUZYd.js} +1 -1
  27. package/web/dist/assets/channel-B50-C7oJ.js +1 -0
  28. package/web/dist/assets/{chunk-2Q5K7J3B-BuHucQOQ.js → chunk-2Q5K7J3B-BX7lDvmf.js} +1 -1
  29. package/web/dist/assets/{chunk-32BRIVSS-T-AE5Yz0.js → chunk-32BRIVSS-COupwzAw.js} +1 -1
  30. package/web/dist/assets/{chunk-5VM5RSS4-B_kTsNzt.js → chunk-5VM5RSS4-C_IyERib.js} +1 -1
  31. package/web/dist/assets/{chunk-EX3LRPZG-CtqgceBQ.js → chunk-EX3LRPZG-CyfIuYKn.js} +1 -1
  32. package/web/dist/assets/{chunk-JWPE2WC7-C_Fv9QUd.js → chunk-JWPE2WC7-Cj5lvEfN.js} +1 -1
  33. package/web/dist/assets/{chunk-MOJQB5TN-D5GeF8ZZ.js → chunk-MOJQB5TN-l907LpMA.js} +1 -1
  34. package/web/dist/assets/{chunk-RYQCIY6F-Cg63wDst.js → chunk-RYQCIY6F-PLru1fOZ.js} +1 -1
  35. package/web/dist/assets/{chunk-V7JOEXUC-Bz6G3lev.js → chunk-V7JOEXUC-C6GCxiE5.js} +1 -1
  36. package/web/dist/assets/{chunk-VR4S4FIN-DKs_hvUD.js → chunk-VR4S4FIN-BGelrIxQ.js} +1 -1
  37. package/web/dist/assets/{chunk-XXDRQBXY-B9AtUlbn.js → chunk-XXDRQBXY-BmJstjEv.js} +1 -1
  38. package/web/dist/assets/classDiagram-OUVF2IWQ-CIJlKS0m.js +1 -0
  39. package/web/dist/assets/classDiagram-v2-EOCWNBFH-CIJlKS0m.js +1 -0
  40. package/web/dist/assets/{cose-bilkent-JH36ORCC-rAG6oS0i.js → cose-bilkent-JH36ORCC-CRabxOOK.js} +1 -1
  41. package/web/dist/assets/{cynefin-VYW2F7L2-CUy2C8UT.js → cynefin-VYW2F7L2-CminxBoG.js} +1 -1
  42. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-C1W8blXH.js → cynefinDiagram-TSTJHNR4-yBazsQf4.js} +1 -1
  43. package/web/dist/assets/{dagre-VKFMJZFB-DC0XqwcC.js → dagre-VKFMJZFB-DcZuZV2n.js} +1 -1
  44. package/web/dist/assets/{diagram-FQU43EPY-COAY9zCP.js → diagram-FQU43EPY-CHoFzsw6.js} +1 -1
  45. package/web/dist/assets/{diagram-G47NLZAW-DLPr8eWG.js → diagram-G47NLZAW-B8leqMRl.js} +1 -1
  46. package/web/dist/assets/{diagram-NH7WQ7WH-DoNgwoDz.js → diagram-NH7WQ7WH-DNoVxpa3.js} +1 -1
  47. package/web/dist/assets/{diagram-OA4YK3LP-D3-5TINE.js → diagram-OA4YK3LP-gC3Q2X6-.js} +1 -1
  48. package/web/dist/assets/{diagram-WEI45ONY-DplCkBJC.js → diagram-WEI45ONY-CABqyjDl.js} +1 -1
  49. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-dvcVh7yr.js → ebnfDiagram-CCIWWBDH-cSguXaDC.js} +1 -1
  50. package/web/dist/assets/{erDiagram-Q63AITRT-Cde8cTY6.js → erDiagram-Q63AITRT-CoWX37V3.js} +1 -1
  51. package/web/dist/assets/{flowDiagram-23GEKE2U-MGMNsh2E.js → flowDiagram-23GEKE2U-I9DHYjhG.js} +1 -1
  52. package/web/dist/assets/{ganttDiagram-NO4QXBWP-BiKw7AfS.js → ganttDiagram-NO4QXBWP-DaFnfYTt.js} +1 -1
  53. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-vC7fsylq.js → gitGraphDiagram-IHSO6WYX-CwiXVM_O.js} +1 -1
  54. package/web/dist/assets/index-CHlrgp0s.css +1 -0
  55. package/web/dist/assets/index-bf7RCrAf.js +24 -0
  56. package/web/dist/assets/{infoDiagram-FWYZ7A6U-Du1uirB0.js → infoDiagram-FWYZ7A6U-DtzIrIpW.js} +1 -1
  57. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-Dk--Qshr.js → ishikawaDiagram-FXEZZL3T-B9nk829n.js} +1 -1
  58. package/web/dist/assets/{journeyDiagram-5HDEW3XC-iDE4zf_N.js → journeyDiagram-5HDEW3XC-xLaPm-ow.js} +1 -1
  59. package/web/dist/assets/{kanban-definition-HUTT4EX6-D7WI8iRR.js → kanban-definition-HUTT4EX6-BnDqLYLC.js} +1 -1
  60. package/web/dist/assets/{linear-B1_kxCBK.js → linear-Djc7nVo8.js} +1 -1
  61. package/web/dist/assets/{mermaid.core-DNmC5O4Y.js → mermaid.core-B5elhUtV.js} +4 -4
  62. package/web/dist/assets/{mindmap-definition-LN4V7U3C-DIbLKLMA.js → mindmap-definition-LN4V7U3C-N-B7EJGs.js} +1 -1
  63. package/web/dist/assets/{papaparse.min-BdWinjY-.js → papaparse.min-BqM-LJkq.js} +1 -1
  64. package/web/dist/assets/{pegDiagram-2B236MQR-CkxxR07G.js → pegDiagram-2B236MQR-BIollSMB.js} +1 -1
  65. package/web/dist/assets/{pieDiagram-ENE6RG2P-Dxvp1mn7.js → pieDiagram-ENE6RG2P-DkZY6iCd.js} +1 -1
  66. package/web/dist/assets/pilotswarm-Di5s6c69.js +105 -0
  67. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-CrL0hjZW.js → quadrantDiagram-ABIIQ3AL-iUDmQIyL.js} +1 -1
  68. package/web/dist/assets/{railroadDiagram-RFXS5EU6-CmVmFBPa.js → railroadDiagram-RFXS5EU6-BSCGJgV7.js} +1 -1
  69. package/web/dist/assets/{requirementDiagram-TGXJPOKE-DJc3NRXb.js → requirementDiagram-TGXJPOKE-9yqcGRUA.js} +1 -1
  70. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-BJNqWqBb.js → sankeyDiagram-HTMAVEWB-SPh1plT9.js} +1 -1
  71. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-C0wddECV.js → sequenceDiagram-DBY2YBRQ-C0nv5x71.js} +1 -1
  72. package/web/dist/assets/{sizeCapture-X5ZJPWSS-Cw0dqPWS.js → sizeCapture-X5ZJPWSS-DMXfUt1o.js} +1 -1
  73. package/web/dist/assets/{stateDiagram-2N3HPSRC-C5793RHZ.js → stateDiagram-2N3HPSRC-vjhtquua.js} +1 -1
  74. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-CYIeYmPN.js +1 -0
  75. package/web/dist/assets/{swimlanes-5IMT3BWC-Bhpm77Zu.js → swimlanes-5IMT3BWC-Bqewmkt5.js} +2 -2
  76. package/web/dist/assets/swimlanesDiagram-G3AALYLV-BeKhH9C9.js +8 -0
  77. package/web/dist/assets/{timeline-definition-FHXFAJF6-CwbE4NsH.js → timeline-definition-FHXFAJF6-DYHsdXUL.js} +1 -1
  78. package/web/dist/assets/{vennDiagram-L72KCM5P-DZnJEL_w.js → vennDiagram-L72KCM5P-Cb7cOHSa.js} +1 -1
  79. package/web/dist/assets/{wardleyDiagram-EHGQE667-DwTgBDev.js → wardleyDiagram-EHGQE667-BHpO1byL.js} +1 -1
  80. package/web/dist/assets/{xychartDiagram-FW5EYKEG-BiPjN3Qc.js → xychartDiagram-FW5EYKEG-DHDvGPtH.js} +1 -1
  81. package/web/dist/index.html +3 -3
  82. package/web/runtime.js +1 -0
  83. package/web/dist/assets/channel-6wPrR6sC.js +0 -1
  84. package/web/dist/assets/classDiagram-OUVF2IWQ-vvXyIfbC.js +0 -1
  85. package/web/dist/assets/classDiagram-v2-EOCWNBFH-vvXyIfbC.js +0 -1
  86. package/web/dist/assets/index-D9Ab1VTY.css +0 -1
  87. package/web/dist/assets/index-DDAyy127.js +0 -24
  88. package/web/dist/assets/pilotswarm-493nqGmy.js +0 -98
  89. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-qGaRayHj.js +0 -1
  90. package/web/dist/assets/swimlanesDiagram-G3AALYLV-BXjNM_Zw.js +0 -8
@@ -1,4 +1,5 @@
1
- // One personal desktop layout, containing references and geometry only.
1
+ // Personal dashboards contain references and geometry only, never session data.
2
+ export const MOA_MAX_DASHBOARDS = 5;
2
3
  export const MOA_MAX_PANELS = 16;
3
4
  export const MOA_BREAKPOINT = 920;
4
5
  const object = (v) => v && typeof v === "object" && !Array.isArray(v);
@@ -24,17 +25,37 @@ export function normalizeMoaLayout(value) {
24
25
  return { name: typeof value.name === "string" ? value.name.trim().slice(0, 64) || "Untitled MoA" : "Untitled MoA", tree: value.tree == null ? null : walk(value.tree) };
25
26
  }
26
27
  export function normalizeMoa(value) {
27
- const safeTree = layout => { try { return normalizeMoaLayout(layout).tree; } catch { return null; } };
28
- // Version 2 has one layout. A deliberately cleared layout must stay empty.
29
- if (value?.version === 2 || (object(value) && Object.hasOwn(value, "tree"))) {
30
- return { version: 2, tree: safeTree(value) };
28
+ const safeLayout = (layout, dashboardId = "moa-1", name = "MoA 1") => {
29
+ let tree = null;
30
+ try { tree = normalizeMoaLayout(layout).tree; } catch {}
31
+ const leaves = moaLeaves(tree);
32
+ return { id: dashboardId, name: typeof layout?.name === "string" ? layout.name.trim().slice(0, 64) || name : name, tree,
33
+ ...(Number.isFinite(layout?.aspectRatio) && layout.aspectRatio >= .2 && layout.aspectRatio <= 8 ? { aspectRatio: layout.aspectRatio } : {}),
34
+ focusedPanelId: leaves.some(p => p.id === layout?.focusedPanelId) ? layout.focusedPanelId : leaves[0]?.id || null };
35
+ };
36
+ if (value?.version === 3 && Array.isArray(value.dashboards)) {
37
+ const used = new Set();
38
+ const dashboards = value.dashboards.slice(0, MOA_MAX_DASHBOARDS).map((dashboard, index) => {
39
+ let key = id(dashboard?.id) && !used.has(dashboard.id) ? dashboard.id : `moa-${index + 1}`;
40
+ while (used.has(key)) key += "-copy";
41
+ used.add(key);
42
+ return safeLayout(dashboard, key, `MoA ${index + 1}`);
43
+ });
44
+ if (!dashboards.length) dashboards.push(safeLayout(null));
45
+ return { version: 3, activeDashboardId: dashboards.some(d => d.id === value.activeDashboardId) ? value.activeDashboardId : dashboards[0].id, dashboards };
31
46
  }
32
- // Migrate the selected legacy dashboard when populated; otherwise keep
33
- // the first populated dashboard rather than a previously selected blank tab.
34
- const slots = Array.isArray(value?.slots) ? value.slots.slice(0, 5) : [];
35
- const active = Number.isInteger(value?.activeSlot) ? Math.max(0, Math.min(4, value.activeSlot)) : 0;
36
- const tree = safeTree(slots[active]) || slots.map(safeTree).find(Boolean) || null;
37
- return { version: 2, tree };
47
+ let source = value;
48
+ if (!(value?.version === 2 || (object(value) && Object.hasOwn(value, "tree")))) {
49
+ // Match the old single-workspace migration; do not resurrect discarded tabs.
50
+ const slots = Array.isArray(value?.slots) ? value.slots.slice(0, 5) : [];
51
+ const active = Number.isInteger(value?.activeSlot) ? Math.max(0, Math.min(4, value.activeSlot)) : 0;
52
+ source = safeLayout(slots[active]).tree ? slots[active] : slots.find(slot => safeLayout(slot).tree);
53
+ }
54
+ return { version: 3, activeDashboardId: "moa-1", dashboards: [safeLayout(source)] };
55
+ }
56
+ export const activeMoaDashboard = value => value.dashboards.find(d => d.id === value.activeDashboardId) || value.dashboards[0];
57
+ export function updateMoaDashboard(value, dashboardId, patch) {
58
+ return normalizeMoa({ ...value, dashboards: value.dashboards.map(d => d.id === dashboardId ? { ...d, ...patch } : d) });
38
59
  }
39
60
  export function replaceMoaNode(tree, nodeId, next) {
40
61
  if (!tree) return null;
@@ -319,63 +319,67 @@ function isSameOrOlderSessionUpdate(previousSession, nextSession) {
319
319
  return previousAt > 0 && nextAt > 0 && nextAt <= previousAt;
320
320
  }
321
321
 
322
- // A run that has ended for good. These are authoritative and must always land,
323
- // even from an update that looks stale — stranding a finished session as
324
- // "running" is far worse than a brief wrong status.
322
+ // Completion can come from orchestration metadata without another custom
323
+ // status write. Allow it at the same version, but never at an older version.
325
324
  const TERMINAL_SESSION_STATUSES = new Set(["completed", "failed", "cancelled", "error"]);
326
325
 
327
- // Session state is written from several concurrent sources: the session-list
328
- // poll, live events, and a per-session detail fetch. They do not agree
329
- // instant-to-instant, and the list poll in particular can report a status the
330
- // orchestration has already moved on from — observed live as:
331
- //
332
- // sessions/loaded running + waiting -> waiting prevAt == incomingAt
333
- // sessions/merged waiting + running -> running (105ms later)
334
- //
335
- // while the server's own session row and orchestration both read "running"
336
- // throughout. Consumers gated on status === "running" — the live-activity
337
- // strip, the composer's Stop button — blink off for the width of that gap.
338
- //
339
- // Refuse ANY non-terminal downgrade out of "running" unless the incoming
340
- // update is genuinely newer. Deliberately not restricted to idle-like
341
- // statuses: the status actually observed clobbering a live run was "waiting",
342
- // and an earlier version of this guard missed the bug by excluding it.
343
- // Terminal statuses are exempt so a finished run can never be stranded.
344
- //
345
- // "Genuinely newer" is decided by the server's monotonic statusVersion when
346
- // both sides carry one — wall-clock updatedAt is only the fallback. The
347
- // timestamp heuristic alone can WEDGE a finished turn as "running" forever:
348
- // client-side event merges (turn_completed, context usage) inflate the held
349
- // session's updatedAt past the server row's idle-write timestamp, so every
350
- // subsequent idle poll compares as stale and only a terminal status could
351
- // ever land. Observed live (2026-07-21, local portal): server row idle at
352
- // statusVersion 5 while the client showed "Working.." indefinitely.
353
- function shouldPreserveRunningStatus(previousSession, nextSession) {
354
- const nextStatus = String(nextSession?.status ?? "").toLowerCase();
355
- if (previousSession?.status !== "running"
356
- || nextStatus === "running"
357
- || TERMINAL_SESSION_STATUSES.has(nextStatus)) {
358
- return false;
359
- }
360
- const previousVersion = Number(previousSession?.statusVersion);
361
- const nextVersion = Number(nextSession?.statusVersion);
362
- if (Number.isFinite(previousVersion) && previousVersion > 0
363
- && Number.isFinite(nextVersion) && nextVersion > 0) {
364
- // Monotonic server counter: a higher version is authoritative and
365
- // must land; a same-or-lower version is provably stale and is held
366
- // regardless of what the timestamps claim.
367
- return nextVersion <= previousVersion;
326
+ function sessionStatusVersion(session) {
327
+ const value = session?.statusVersion;
328
+ if (value == null || value === "" || typeof value === "boolean") return null;
329
+ const version = Number(value);
330
+ return Number.isSafeInteger(version) && version > 0 ? version : null;
331
+ }
332
+
333
+ // List polls and detail requests can arrive out of order. Compare the server
334
+ // counter in both directions: an old running row must not revive an idle
335
+ // session, and an old idle row must not hide a running one. Keep the version
336
+ // with its status; accepting an older counter would let the next stale row win.
337
+ function shouldPreserveSessionStatus(previousSession, nextSession) {
338
+ const previousVersion = sessionStatusVersion(previousSession);
339
+ const nextVersion = sessionStatusVersion(nextSession);
340
+ if (previousVersion != null && nextVersion != null) {
341
+ if (nextVersion < previousVersion) return true;
342
+ if (nextVersion > previousVersion) return false;
368
343
  }
369
- return isSameOrOlderSessionUpdate(previousSession, nextSession);
344
+ const previousStatus = previousSession?.status;
345
+ const nextStatus = nextSession?.status;
346
+ if (!previousStatus || !nextStatus || previousStatus === nextStatus
347
+ || TERMINAL_SESSION_STATUSES.has(nextStatus)) return false;
348
+ if (previousVersion != null && nextVersion != null) return true;
349
+ // Legacy rows and live event patches may lack a counter. Do not guess
350
+ // their order without timestamps. Preserve the terminal-status escape
351
+ // hatch, since updatedAt also changes for non-status events.
352
+ if (previousStatus === "running") return isSameOrOlderSessionUpdate(previousSession, nextSession);
353
+ // An unversioned new turn may share its timestamp with the preceding idle
354
+ // row. Equal timestamps alone are not evidence that a start is stale.
355
+ const previousAt = sessionUpdateTimestampMs(previousSession);
356
+ const nextAt = sessionUpdateTimestampMs(nextSession);
357
+ return nextStatus === "running" && previousAt > 0 && nextAt > 0 && nextAt < previousAt;
370
358
  }
371
359
 
360
+ // These values describe the same run as status. Mixing an old question or
361
+ // pause reason with a newer status can change composer routing and controls.
362
+ const SESSION_STATUS_FIELDS = new Set([
363
+ "status", "statusVersion", "updatedAt", "orchestrationStatus",
364
+ "pendingQuestion", "waitReason", "pauseState", "error", "result",
365
+ ]);
366
+
372
367
  function mergeDefinedSessionFields(previousSession = {}, nextSession = {}) {
373
368
  let merged = previousSession || {};
374
- const preserveRunning = shouldPreserveRunningStatus(previousSession, nextSession);
369
+ const preserveStatus = shouldPreserveSessionStatus(previousSession, nextSession);
370
+ const previousVersion = sessionStatusVersion(previousSession);
375
371
  for (const [key, value] of Object.entries(nextSession || {})) {
376
372
  if (value === undefined) continue;
377
- if (key === "status" && preserveRunning) continue;
378
- if (key === "pendingQuestion" && isAnsweredPendingQuestion(previousSession, value)) {
373
+ if (preserveStatus && SESSION_STATUS_FIELDS.has(key)) continue;
374
+ // Repeated snapshots of one server version cannot roll its timestamp
375
+ // back and then admit an older unversioned running update.
376
+ if (key === "updatedAt" && previousVersion != null
377
+ && previousVersion === sessionStatusVersion(nextSession)
378
+ && sessionUpdateTimestampMs(nextSession) < sessionUpdateTimestampMs(previousSession)) continue;
379
+ // An absent/invalid counter must not erase the last known server version.
380
+ if (key === "statusVersion" && previousVersion != null
381
+ && sessionStatusVersion(nextSession) == null) continue;
382
+ if (key === "pendingQuestion" && isAnsweredPendingQuestion(previousSession, value, nextSession)) {
379
383
  if (merged === previousSession) {
380
384
  merged = { ...(previousSession || {}) };
381
385
  }
@@ -551,10 +555,18 @@ function normalizedPendingQuestionText(pendingQuestion) {
551
555
  return String(pendingQuestion?.question || "").trim();
552
556
  }
553
557
 
554
- function isAnsweredPendingQuestion(previousSession, pendingQuestion) {
555
- const answeredQuestion = normalizedPendingQuestionText(previousSession?.answeredPendingQuestion);
558
+ function isAnsweredPendingQuestion(previousSession, pendingQuestion, nextSession) {
556
559
  const incomingQuestion = normalizedPendingQuestionText(pendingQuestion);
557
- return Boolean(answeredQuestion && incomingQuestion && answeredQuestion === incomingQuestion);
560
+ if (!incomingQuestion) return false;
561
+ const incomingTime = sessionUpdateTimestampMs({ updatedAt: pendingQuestion?.askedAt })
562
+ || sessionUpdateTimestampMs(nextSession);
563
+ return [previousSession?.answeredPendingQuestion, previousSession?.resolvedInputQuestion].some(answered => {
564
+ if (normalizedPendingQuestionText(answered) !== incomingQuestion) return false;
565
+ const answeredAt = sessionUpdateTimestampMs({ updatedAt: answered?.answeredAt });
566
+ // Preserve the answer fence across stale detail polls, but let a new
567
+ // instance of the same question through when it was asked later.
568
+ return !incomingTime || !answeredAt || incomingTime <= answeredAt;
569
+ });
558
570
  }
559
571
 
560
572
  function pickDefaultActiveSessionId(sessions = []) {
@@ -1,4 +1,5 @@
1
1
  import { INSPECTOR_TABS, FOCUS_REGIONS } from "./commands.js";
2
+ import { chatCallLine, firstCallLine } from "./chat-activity.js";
2
3
  import { canvasKey as canvasSlotKey, parseCanvasKey } from "./state.js";
3
4
  import { isManuallyOrderableSession } from "./session-tree.js";
4
5
  import {
@@ -28,7 +29,7 @@ import {
28
29
  getContextHeaderBadge,
29
30
  } from "./context-usage.js";
30
31
  import { canonicalSystemTitle } from "./system-titles.js";
31
- import { withSessionWarnings } from "./session-errors.js";
32
+ import { withSessionWarnings, isActivityOnlySessionError } from "./session-errors.js";
32
33
  import {
33
34
  BUDGET_PERIODS,
34
35
  BUDGET_SERIES_DAYS,
@@ -1645,7 +1646,7 @@ function chatAlreadyContainsPendingQuestion(chat, question) {
1645
1646
 
1646
1647
  function buildSessionErrorMessage(session, events = []) {
1647
1648
  const errorText = String(session?.error || "").trim();
1648
- if (!errorText) return null;
1649
+ if (!errorText || isActivityOnlySessionError(errorText)) return null;
1649
1650
 
1650
1651
  const errorKind = getSessionErrorVisualKind(session);
1651
1652
  if (!errorKind) return null;
@@ -2251,6 +2252,7 @@ function chatMessageSpacingKind(message) {
2251
2252
 
2252
2253
  function shouldInsertChatSpacer(currentMessage, nextMessage) {
2253
2254
  if (!currentMessage || !nextMessage) return false;
2255
+ if (currentMessage.kind === "chat-call" || nextMessage.kind === "chat-call") return false;
2254
2256
  return chatMessageSpacingKind(currentMessage) !== chatMessageSpacingKind(nextMessage);
2255
2257
  }
2256
2258
 
@@ -2287,7 +2289,7 @@ function buildCollapsedSystemNoticeLine(text, timestamp = "") {
2287
2289
  };
2288
2290
  }
2289
2291
 
2290
- function buildSystemNoticeLine({ title, summary, body, timestamp = "", color = "gray" }) {
2292
+ function buildSystemNoticeLine({ title, summary, body, timestamp = "", color = "gray", callPreview, callKey }) {
2291
2293
  const normalizedTitle = String(title || "System").trim() || "System";
2292
2294
  const normalizedSummary = String(summary || "").replace(/\s+/g, " ").trim();
2293
2295
  return {
@@ -2296,6 +2298,7 @@ function buildSystemNoticeLine({ title, summary, body, timestamp = "", color = "
2296
2298
  summary: normalizedSummary,
2297
2299
  body: decorateArtifactLinksForChat(body || normalizedSummary),
2298
2300
  color,
2301
+ ...(callPreview !== undefined ? { callPreview: `${normalizedTitle} — ${firstCallLine(callPreview)}`, callKey } : {}),
2299
2302
  };
2300
2303
  }
2301
2304
 
@@ -2401,6 +2404,8 @@ function buildSystemNoticeCardLines(systemText, message, maxWidth) {
2401
2404
  ].filter((line, index) => index === 2 || line).join("\n");
2402
2405
  return [buildSystemNoticeLine({
2403
2406
  title: `Sub-agent Response — ${shortId}`,
2407
+ callPreview: subAgent.result || subAgent.status,
2408
+ callKey: `${message.id}:agent:${shortId}`,
2404
2409
  summary: subAgent.status || "completed",
2405
2410
  timestamp,
2406
2411
  body,
@@ -2420,6 +2425,8 @@ function buildSystemNoticeCardLines(systemText, message, maxWidth) {
2420
2425
  .join(" · ");
2421
2426
  return [buildSystemNoticeLine({
2422
2427
  title: `Sub-agent Responses (${subAgent.agents.length})`,
2428
+ callPreview: subAgent.agents.map(agent => firstCallLine(agent.result) || agent.status).join(" · "),
2429
+ callKey: `${message.id}:agents`,
2423
2430
  summary,
2424
2431
  timestamp,
2425
2432
  body: sections.join("\n\n---\n\n"),
@@ -2505,6 +2512,17 @@ function buildChatMessageLines(message, maxWidth, options = {}) {
2505
2512
  }
2506
2513
 
2507
2514
  function buildChatMessageLinesUncached(message, maxWidth, options = {}) {
2515
+ if (message?.kind === "chat-call") {
2516
+ return options.tableMode === "sentinel" ? [chatCallLine(message)] : [];
2517
+ }
2518
+ if (options.tableMode === "sentinel" && (message?.sender?.kind === "agent" || message?.agentCallPreview !== undefined)) {
2519
+ const label = message.cardTitle || message.sender?.display || "Agent message";
2520
+ return [{
2521
+ kind: "chatCall", callKey: message.id, category: "Agent",
2522
+ text: `${label} — ${firstCallLine(message.agentCallPreview ?? message.text)}`,
2523
+ body: message.text, time: message.time,
2524
+ }];
2525
+ }
2508
2526
  if (message?.splash) {
2509
2527
  // Swap in the narrow-viewport variant when the main art would
2510
2528
  // overflow the pane (mobile portal, narrow terminals). When the
@@ -2574,7 +2592,9 @@ function buildChatMessageLinesUncached(message, maxWidth, options = {}) {
2574
2592
  const askedAndAnswered = parseAskedAndAnsweredExchange(message?.text || "");
2575
2593
  if (askedAndAnswered) {
2576
2594
  return [
2577
- ...buildMessageCardLines({
2595
+ // Older runtimes inserted this placeholder for late answers.
2596
+ // Keep the user's answer, without displaying a fabricated card.
2597
+ ...(askedAndAnswered.question === "a question" ? [] : buildMessageCardLines({
2578
2598
  title: "Question",
2579
2599
  timestamp: formatTimestamp(message?.createdAt || message?.time),
2580
2600
  body: askedAndAnswered.question,
@@ -2582,7 +2602,7 @@ function buildChatMessageLinesUncached(message, maxWidth, options = {}) {
2582
2602
  titleColor: USER_CHAT_COLOR,
2583
2603
  borderColor: USER_CHAT_COLOR,
2584
2604
  tableMode: options.tableMode,
2585
- }),
2605
+ })),
2586
2606
  ...buildChatMessageLines({
2587
2607
  ...message,
2588
2608
  text: askedAndAnswered.answer,
@@ -1,3 +1,10 @@
1
+ // Copilot emits this diagnostic for an accepted turn without visible text.
2
+ // Keep its durable event and Activity entry, but do not turn it into a chat
3
+ // warning. Match only this message and the runtime's known retry wrapper.
4
+ export function isActivityOnlySessionError(text) {
5
+ return /^(?:Execution failed:\s*)?No response was returned\. Send your message again to retry\.(?: \(retry \d+\/\d+ in \d+s\))?$/.test(String(text || "").trim());
6
+ }
7
+
1
8
  export function shouldKeepSessionWarning(previousSession, nextSession) {
2
9
  if (!String(previousSession?.error || "").trim()) return false;
3
10
  if (nextSession?.error !== undefined) return false;
@@ -26,7 +33,7 @@ const sameError = (statusText, eventText) => Boolean(eventText) && statusText.in
26
33
 
27
34
  export function buildSessionWarning(event) {
28
35
  const text = errorText(event);
29
- if (!text) return null;
36
+ if (!text || isActivityOnlySessionError(text)) return null;
30
37
  const failed = event?.data?.fatal === true;
31
38
  return {
32
39
  id: `${event.sessionId}:${event.seq}:warning`,
@@ -79,8 +86,9 @@ export function retainSessionWarnings(previous, next, events = [], now = Date.no
79
86
  }
80
87
 
81
88
  export function withSessionWarnings(chat, session, events = []) {
82
- const messages = [...chat];
89
+ const messages = chat.filter(message => message.kind !== "session-warning" || !isActivityOnlySessionError(message.text));
83
90
  for (const warning of session?.chatWarnings || []) {
91
+ if (isActivityOnlySessionError(warning.text)) continue;
84
92
  // Status often wins the race against its durable event. Match only
85
93
  // that boundary, never a later failure after a new user/agent message.
86
94
  const following = events.filter(e => Number(e.seq) > warning.afterSeq);
@@ -5,12 +5,14 @@ export function canonicalSystemTitle(session, brandingTitle = "PilotSwarm") {
5
5
  if (agentId === "sweeper") return "Sweeper Agent";
6
6
  if (agentId === "resourcemgr") return "Resource Manager Agent";
7
7
  if (agentId === "facts-manager") return "Facts Manager";
8
+ if (agentId === "agent-manager") return "Agent Smith";
8
9
 
9
10
  const rawTitle = String(session?.title || "").trim();
10
11
  if (/^pilotswarm(?: agent)?$/i.test(rawTitle)) return brandedRootTitle;
11
12
  if (/^sweeper agent$/i.test(rawTitle) || /^sweeper$/i.test(rawTitle)) return "Sweeper Agent";
12
13
  if (/^resource manager agent$/i.test(rawTitle) || /^resourcemgr$/i.test(rawTitle)) return "Resource Manager Agent";
13
14
  if (/^facts manager$/i.test(rawTitle) || /^facts-manager$/i.test(rawTitle)) return "Facts Manager";
15
+ if (/^agent manager$/i.test(rawTitle)) return "Agent Smith";
14
16
  return rawTitle || "System Agent";
15
17
  }
16
18