pilotswarm 0.5.61 → 0.5.63

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/mcp/dist/src/resources/models.d.ts.map +1 -1
  2. package/mcp/dist/src/resources/models.js +6 -1
  3. package/mcp/dist/src/resources/models.js.map +1 -1
  4. package/mcp/dist/src/tools/models.d.ts.map +1 -1
  5. package/mcp/dist/src/tools/models.js +11 -1
  6. package/mcp/dist/src/tools/models.js.map +1 -1
  7. package/mcp/dist/src/tools/sessions.d.ts.map +1 -1
  8. package/mcp/dist/src/tools/sessions.js +8 -2
  9. package/mcp/dist/src/tools/sessions.js.map +1 -1
  10. package/package.json +2 -2
  11. package/tui/src/node-sdk-transport.js +1 -1
  12. package/ui/core/src/chat-activity.js +94 -0
  13. package/ui/core/src/controller.js +87 -19
  14. package/ui/core/src/history.js +15 -0
  15. package/ui/core/src/moa.js +41 -11
  16. package/ui/core/src/reducer.js +25 -10
  17. package/ui/core/src/selectors.js +25 -5
  18. package/ui/core/src/session-errors.js +10 -2
  19. package/ui/core/src/state.js +6 -2
  20. package/ui/core/src/system-titles.js +2 -0
  21. package/ui/react/src/components.js +2 -1
  22. package/ui/react/src/web-app.js +110 -35
  23. package/web/dist/assets/{abnfDiagram-VRR7QNED-CCVkGWsr.js → abnfDiagram-VRR7QNED-C8iAmIqt.js} +1 -1
  24. package/web/dist/assets/{arc-y040btO3.js → arc-Cg8qs17j.js} +1 -1
  25. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-24Yt5esy.js → architectureDiagram-ZJ3FMSHR-fYr0dRm3.js} +1 -1
  26. package/web/dist/assets/{blockDiagram-677ZJIJ3-BlbS-_GI.js → blockDiagram-677ZJIJ3-Bpj79Dzq.js} +1 -1
  27. package/web/dist/assets/{c4Diagram-LMCZKHZV-_mamn0Fv.js → c4Diagram-LMCZKHZV-b26Lsm6m.js} +1 -1
  28. package/web/dist/assets/channel-C-ASbcRr.js +1 -0
  29. package/web/dist/assets/{chunk-2Q5K7J3B-TCbIB2Ni.js → chunk-2Q5K7J3B-VLYQfjgG.js} +1 -1
  30. package/web/dist/assets/{chunk-32BRIVSS-C9q5KNKG.js → chunk-32BRIVSS-mVx--2Hg.js} +1 -1
  31. package/web/dist/assets/{chunk-5VM5RSS4-BvoqfU8P.js → chunk-5VM5RSS4-jwnJmked.js} +1 -1
  32. package/web/dist/assets/{chunk-EX3LRPZG-zFJjZeNd.js → chunk-EX3LRPZG-DdfzAT1n.js} +1 -1
  33. package/web/dist/assets/{chunk-JWPE2WC7-Ce_6C5M-.js → chunk-JWPE2WC7-BHm6Udd8.js} +1 -1
  34. package/web/dist/assets/{chunk-MOJQB5TN-CPjpLM65.js → chunk-MOJQB5TN-Dn5qSVAS.js} +1 -1
  35. package/web/dist/assets/{chunk-RYQCIY6F-CsmVUSDH.js → chunk-RYQCIY6F-D4IKMROn.js} +1 -1
  36. package/web/dist/assets/{chunk-V7JOEXUC-CgrmPPOd.js → chunk-V7JOEXUC-BhME3pRf.js} +1 -1
  37. package/web/dist/assets/{chunk-VR4S4FIN-BxAHDR7h.js → chunk-VR4S4FIN-CHG_NBff.js} +1 -1
  38. package/web/dist/assets/{chunk-XXDRQBXY-BGfFoO5_.js → chunk-XXDRQBXY-yw4QCgiL.js} +1 -1
  39. package/web/dist/assets/classDiagram-OUVF2IWQ-D2vlJIb1.js +1 -0
  40. package/web/dist/assets/classDiagram-v2-EOCWNBFH-D2vlJIb1.js +1 -0
  41. package/web/dist/assets/{cose-bilkent-JH36ORCC-B6KcngJW.js → cose-bilkent-JH36ORCC-DqwPj9Kx.js} +1 -1
  42. package/web/dist/assets/{cynefin-VYW2F7L2-DaY7l4Ce.js → cynefin-VYW2F7L2-CVokaaE_.js} +1 -1
  43. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-BldfGsoa.js → cynefinDiagram-TSTJHNR4-Bkhj9fuY.js} +1 -1
  44. package/web/dist/assets/{dagre-VKFMJZFB-C4T9X5Bs.js → dagre-VKFMJZFB-DVv-jnjb.js} +1 -1
  45. package/web/dist/assets/{diagram-FQU43EPY-DCVA_t-e.js → diagram-FQU43EPY-DDlZjkrF.js} +1 -1
  46. package/web/dist/assets/{diagram-G47NLZAW-pbTZGgE5.js → diagram-G47NLZAW-CNDUgqYa.js} +1 -1
  47. package/web/dist/assets/{diagram-NH7WQ7WH-P8Iks122.js → diagram-NH7WQ7WH-DywF8u8i.js} +1 -1
  48. package/web/dist/assets/{diagram-OA4YK3LP-hEjZPipp.js → diagram-OA4YK3LP-ZfN8nvLw.js} +1 -1
  49. package/web/dist/assets/{diagram-WEI45ONY-CVAzVCGu.js → diagram-WEI45ONY-S94A6Avm.js} +1 -1
  50. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-BLL398qt.js → ebnfDiagram-CCIWWBDH-Mpt3QUnw.js} +1 -1
  51. package/web/dist/assets/{erDiagram-Q63AITRT-CLTADyUG.js → erDiagram-Q63AITRT-Bh-eWbdV.js} +1 -1
  52. package/web/dist/assets/{flowDiagram-23GEKE2U-BAdlWAt0.js → flowDiagram-23GEKE2U-BwRtDI6V.js} +1 -1
  53. package/web/dist/assets/{ganttDiagram-NO4QXBWP-C7Z2wavr.js → ganttDiagram-NO4QXBWP-CrZoBmcB.js} +1 -1
  54. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-BI546pYu.js → gitGraphDiagram-IHSO6WYX-giXv1ynk.js} +1 -1
  55. package/web/dist/assets/index-Br-gGWmc.js +24 -0
  56. package/web/dist/assets/{index-CvW-4eXF.css → index-DRFDn87d.css} +1 -1
  57. package/web/dist/assets/{infoDiagram-FWYZ7A6U-8C7iQydo.js → infoDiagram-FWYZ7A6U-H_5oc4-7.js} +1 -1
  58. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-Co4STVxQ.js → ishikawaDiagram-FXEZZL3T-BydiPDX0.js} +1 -1
  59. package/web/dist/assets/{journeyDiagram-5HDEW3XC-DbUt11q-.js → journeyDiagram-5HDEW3XC-DS1HXms0.js} +1 -1
  60. package/web/dist/assets/{kanban-definition-HUTT4EX6-DUNcFmme.js → kanban-definition-HUTT4EX6-BjY5SyYu.js} +1 -1
  61. package/web/dist/assets/{linear-Cjaxj94d.js → linear-BvOotjKL.js} +1 -1
  62. package/web/dist/assets/{mermaid.core-B3cPFf4f.js → mermaid.core-0iRu7sp-.js} +4 -4
  63. package/web/dist/assets/{mindmap-definition-LN4V7U3C-DaZtxB4N.js → mindmap-definition-LN4V7U3C-DB93zsvL.js} +1 -1
  64. package/web/dist/assets/{papaparse.min-vfdso2fs.js → papaparse.min-f5Xncedc.js} +1 -1
  65. package/web/dist/assets/{pegDiagram-2B236MQR-DkBpYV7m.js → pegDiagram-2B236MQR-Dh_U1cFA.js} +1 -1
  66. package/web/dist/assets/{pieDiagram-ENE6RG2P-By3tI-vc.js → pieDiagram-ENE6RG2P-Csv1qxMm.js} +1 -1
  67. package/web/dist/assets/pilotswarm-DQNSWfNK.js +105 -0
  68. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-DZmVm0xK.js → quadrantDiagram-ABIIQ3AL-Bo0pcrBM.js} +1 -1
  69. package/web/dist/assets/{railroadDiagram-RFXS5EU6-BC5Mbz7q.js → railroadDiagram-RFXS5EU6-CHPvHNWG.js} +1 -1
  70. package/web/dist/assets/{requirementDiagram-TGXJPOKE-CyZNX_O1.js → requirementDiagram-TGXJPOKE-DgX8l-6H.js} +1 -1
  71. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-7603NEmu.js → sankeyDiagram-HTMAVEWB-xR6MTs5a.js} +1 -1
  72. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CXJ-i2Xj.js → sequenceDiagram-DBY2YBRQ-CD_45diB.js} +1 -1
  73. package/web/dist/assets/{sizeCapture-X5ZJPWSS-CKT0ATnS.js → sizeCapture-X5ZJPWSS-BJVEXU0K.js} +1 -1
  74. package/web/dist/assets/{stateDiagram-2N3HPSRC-BIDPX4YU.js → stateDiagram-2N3HPSRC-Bwu6kd23.js} +1 -1
  75. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-aypsN5jW.js +1 -0
  76. package/web/dist/assets/{swimlanes-5IMT3BWC-CPoy6vnO.js → swimlanes-5IMT3BWC-BmhEzevT.js} +2 -2
  77. package/web/dist/assets/swimlanesDiagram-G3AALYLV-Duf5C4YK.js +8 -0
  78. package/web/dist/assets/{timeline-definition-FHXFAJF6-BGdNkplk.js → timeline-definition-FHXFAJF6-DFPI_uTI.js} +1 -1
  79. package/web/dist/assets/{vennDiagram-L72KCM5P-ByFzarUb.js → vennDiagram-L72KCM5P-DDTpTkBC.js} +1 -1
  80. package/web/dist/assets/{wardleyDiagram-EHGQE667-CAk0SYuv.js → wardleyDiagram-EHGQE667-bV2_YVIL.js} +1 -1
  81. package/web/dist/assets/{xychartDiagram-FW5EYKEG-Np7h6_A0.js → xychartDiagram-FW5EYKEG-C7bDIXAV.js} +1 -1
  82. package/web/dist/index.html +3 -3
  83. package/web/runtime.js +1 -0
  84. package/web/dist/assets/channel-COFs9dqk.js +0 -1
  85. package/web/dist/assets/classDiagram-OUVF2IWQ-B5-KmsCx.js +0 -1
  86. package/web/dist/assets/classDiagram-v2-EOCWNBFH-B5-KmsCx.js +0 -1
  87. package/web/dist/assets/index-DsPaDrf9.js +0 -24
  88. package/web/dist/assets/pilotswarm-BL_NbZug.js +0 -98
  89. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-Dr8oD9T6.js +0 -1
  90. package/web/dist/assets/swimlanesDiagram-G3AALYLV-BV0EQRlB.js +0 -8
@@ -11,6 +11,7 @@ import {
11
11
  DEFAULT_HISTORY_EVENT_LIMIT,
12
12
  dedupeChatMessages,
13
13
  getNextHistoryEventLimit,
14
+ parseAskedAndAnsweredExchange,
14
15
  } from "./history.js";
15
16
  import { applySessionUsageEvent, cloneContextUsageSnapshot } from "./context-usage.js";
16
17
  import { validateCanvasAction, formatCanvasActionPrompt, createCanvasActionLimiter } from "./canvas-actions.js";
@@ -5490,6 +5491,7 @@ export class PilotSwarmUiController {
5490
5491
  /**
5491
5492
  * Reconcile outbox state against a single CMS event. Removes outbox items
5492
5493
  * whose `clientMessageIds` were observed as a durable `user.message`
5494
+ * (or a legacy merged `system.message` carrying those exact identities)
5493
5495
  * (acknowledged → drop the local optimistic item) or as a
5494
5496
  * `pending_messages.cancelled` (cancel confirmed → drop the cancelling
5495
5497
  * item). Pure side-effect helper that is safe to call repeatedly.
@@ -5501,16 +5503,20 @@ export class PilotSwarmUiController {
5501
5503
  */
5502
5504
  reconcileOutboxAgainstEvent(sessionId, event) {
5503
5505
  if (!sessionId || !event) return;
5504
- if (event.eventType === "user.message") {
5506
+ if (event.eventType === "user.message" || event.eventType === "system.message") {
5505
5507
  const content = event?.data?.content;
5506
5508
  const clientMessageIds = Array.isArray(event?.data?.clientMessageIds)
5507
5509
  ? event.data.clientMessageIds.filter((id) => typeof id === "string")
5508
5510
  : (typeof event?.data?.clientMessageId === "string" ? [event.data.clientMessageId] : []);
5509
5511
  if (clientMessageIds.length > 0) {
5510
5512
  for (const id of clientMessageIds) {
5511
- this.acknowledgeOutboxPrompt(sessionId, content, id);
5513
+ // Older workers classified a merged internal + user turn
5514
+ // from its leading protocol marker. The preserved IDs are
5515
+ // still an exact receipt. Never text-match a system event:
5516
+ // its content can quote unrelated pending user input.
5517
+ this.acknowledgeOutboxPrompt(sessionId, event.eventType === "user.message" ? content : undefined, id);
5512
5518
  }
5513
- } else if (typeof content === "string" && content.trim()) {
5519
+ } else if (event.eventType === "user.message" && typeof content === "string" && content.trim()) {
5514
5520
  // Fallback: text-match acknowledgement until clientMessageId is
5515
5521
  // plumbed through every layer.
5516
5522
  this.acknowledgeOutboxPrompt(sessionId, content);
@@ -5832,10 +5838,11 @@ export class PilotSwarmUiController {
5832
5838
  }
5833
5839
  const existing = this.getState().history.bySessionId.get(sessionId)
5834
5840
  || { chat: [], activity: [], events: [], lastSeq: 0 };
5841
+ const liveHistory = this.preservePausedChatWindow(sessionId, existing);
5835
5842
  this.dispatch({
5836
5843
  type: "history/set",
5837
5844
  sessionId,
5838
- history: applyLiveTurnToHistory(existing, event.data, {
5845
+ history: applyLiveTurnToHistory(liveHistory, event.data, {
5839
5846
  sessionId,
5840
5847
  seq: event.liveSeq,
5841
5848
  createdAt: Date.now(),
@@ -5872,7 +5879,10 @@ export class PilotSwarmUiController {
5872
5879
  this._liveTurnIdleTimers.delete(sessionId);
5873
5880
  }
5874
5881
  const state = this.getState();
5875
- const existing = state.history.bySessionId.get(sessionId) || { chat: [], activity: [], lastSeq: 0 };
5882
+ const existing = this.preservePausedChatWindow(
5883
+ sessionId,
5884
+ state.history.bySessionId.get(sessionId) || { chat: [], activity: [], events: [], lastSeq: 0 },
5885
+ );
5876
5886
  if (event.seq <= (existing.lastSeq || 0)) return false;
5877
5887
  this.dispatch({
5878
5888
  type: "history/set",
@@ -5887,7 +5897,11 @@ export class PilotSwarmUiController {
5887
5897
  // misrouted into the outbox queue, where it would sit until a later send
5888
5898
  // flushed it. The eventual detail-sync reconciles to the authoritative
5889
5899
  // customStatus.
5890
- if (event.eventType === "session.input_required_started" && event.data?.question) {
5900
+ const questionSession = this.getState().sessions.byId[sessionId];
5901
+ const questionEventTime = timestampMs(event.createdAt);
5902
+ const currentSessionTime = timestampMs(questionSession?.updatedAt);
5903
+ if (event.eventType === "session.input_required_started" && event.data?.question
5904
+ && (!questionEventTime || !currentSessionTime || questionEventTime >= currentSessionTime)) {
5891
5905
  this.dispatch({
5892
5906
  type: "sessions/merged",
5893
5907
  session: {
@@ -5895,12 +5909,27 @@ export class PilotSwarmUiController {
5895
5909
  status: "input_required",
5896
5910
  pendingQuestion: {
5897
5911
  question: event.data.question,
5912
+ ...(questionEventTime ? { askedAt: questionEventTime } : {}),
5913
+ ...(Number.isSafeInteger(event.data.questionIteration) ? { iteration: event.data.questionIteration } : {}),
5898
5914
  choices: Array.isArray(event.data.choices) ? event.data.choices : undefined,
5899
5915
  allowFreeform: event.data.allowFreeform ?? true,
5900
5916
  },
5901
5917
  },
5902
5918
  });
5903
5919
  }
5920
+ // Catch-up events also include answers from other windows/writers.
5921
+ // Retire the matching question before another send can use sendAnswer.
5922
+ if (event.eventType === "user.message" && questionSession?.pendingQuestion) {
5923
+ const exchange = parseAskedAndAnsweredExchange(event.data?.content);
5924
+ const pendingSince = Math.max(currentSessionTime, timestampMs(questionSession.pendingQuestion.askedAt));
5925
+ if (exchange?.question === questionSession.pendingQuestion.question
5926
+ && (!questionEventTime || !pendingSince || questionEventTime >= pendingSince)) {
5927
+ this.dispatch({ type: "sessions/merged", session: {
5928
+ sessionId, pendingQuestion: null,
5929
+ resolvedInputQuestion: { question: exchange.question, answeredAt: questionEventTime },
5930
+ } });
5931
+ }
5932
+ }
5904
5933
  // show_artifact: the agent is presenting something to look at, so the
5905
5934
  // inspector switches to Files and opens that preview live.
5906
5935
  //
@@ -5954,6 +5983,31 @@ export class PilotSwarmUiController {
5954
5983
  return true;
5955
5984
  }
5956
5985
 
5986
+ /**
5987
+ * Do not evict the first rendered item while its active chat is paused.
5988
+ * A fixed-size history window normally drops one old item for each live
5989
+ * append. Keeping the same numeric scrollTop then moves the actual text the
5990
+ * user is reading. Grow the in-memory window just enough for arrivals made
5991
+ * during that pause; normal bottom-follow appends resume bounded trimming.
5992
+ */
5993
+ preservePausedChatWindow(sessionId, history) {
5994
+ const state = this.getState();
5995
+ if (state.sessions.activeSessionId !== sessionId || state.ui.followBottom?.chat !== false) {
5996
+ return history;
5997
+ }
5998
+ const currentLimit = Math.max(
5999
+ DEFAULT_HISTORY_EVENT_LIMIT,
6000
+ Number(history?.loadedEventLimit ?? DEFAULT_HISTORY_EVENT_LIMIT) || DEFAULT_HISTORY_EVENT_LIMIT,
6001
+ );
6002
+ const requiredLimit = Math.max(
6003
+ currentLimit,
6004
+ (Array.isArray(history?.events) ? history.events.length : 0) + 1,
6005
+ (Array.isArray(history?.chat) ? history.chat.length : 0) + 4,
6006
+ (Array.isArray(history?.activity) ? history.activity.length : 0) + 1,
6007
+ );
6008
+ return requiredLimit === currentLimit ? history : { ...history, loadedEventLimit: requiredLimit };
6009
+ }
6010
+
5957
6011
  async syncSessionEvents(sessionId) {
5958
6012
  if (!sessionId || typeof this.transport.getSessionEvents !== "function") return;
5959
6013
  const existing = this.getState().history.bySessionId.get(sessionId);
@@ -8685,7 +8739,10 @@ export class PilotSwarmUiController {
8685
8739
  });
8686
8740
  this.dispatch({ type: "ui/status", text: "Sending answer..." });
8687
8741
  try {
8688
- await this.transport.sendAnswer(sessionId, prompt);
8742
+ await this.transport.sendAnswer(sessionId, prompt, { expectedQuestion: {
8743
+ question: activePendingQuestion.question,
8744
+ ...(Number.isSafeInteger(activePendingQuestion.iteration) ? { iteration: activePendingQuestion.iteration } : {}),
8745
+ } });
8689
8746
  this.dispatch({
8690
8747
  type: "sessions/merged",
8691
8748
  session: {
@@ -9150,6 +9207,7 @@ export class PilotSwarmUiController {
9150
9207
  }
9151
9208
 
9152
9209
  paneUsesStickyBottomFollow(pane, state = this.getState()) {
9210
+ if (pane === "chat") return true;
9153
9211
  if (pane === "activity") return true;
9154
9212
  if (pane === "inspector") return state.ui.inspectorTab === "logs";
9155
9213
  return false;
@@ -9223,6 +9281,15 @@ export class PilotSwarmUiController {
9223
9281
  const current = this.getPaneVisualScrollOffset(pane, state);
9224
9282
  const nextOffset = Math.max(0, Math.min(current - delta, maxOffset));
9225
9283
  this.applyPaneVisualScrollOffset(pane, nextOffset, { followBottom: nextOffset >= maxOffset }, state);
9284
+ if (pane === "chat" && delta > 0) {
9285
+ if (current <= 0) {
9286
+ this.handleChatTopHistoryScrollIntent(nextOffset).catch(() => {});
9287
+ } else if (nextOffset <= 0) {
9288
+ this.armChatTopHistoryLoad();
9289
+ }
9290
+ } else if (pane === "chat" && delta < 0) {
9291
+ this.disarmChatTopHistoryLoad();
9292
+ }
9226
9293
  return;
9227
9294
  }
9228
9295
  const current = Math.max(0, Math.min(Number(state.ui.scroll?.[pane]) || 0, maxOffset));
@@ -9245,6 +9312,10 @@ export class PilotSwarmUiController {
9245
9312
  const nextOffset = Math.max(0, Math.min(Number(offset) || 0, maxOffset));
9246
9313
  if (this.paneUsesStickyBottomFollow(pane, state)) {
9247
9314
  this.applyPaneVisualScrollOffset(pane, nextOffset, { followBottom: nextOffset >= maxOffset }, state);
9315
+ if (pane === "chat") {
9316
+ if (maxOffset > 0 && nextOffset <= 0) this.armChatTopHistoryLoad();
9317
+ else this.disarmChatTopHistoryLoad();
9318
+ }
9248
9319
  return;
9249
9320
  }
9250
9321
  this.dispatch({ type: "ui/scroll", pane, offset: nextOffset });
@@ -9280,6 +9351,7 @@ export class PilotSwarmUiController {
9280
9351
 
9281
9352
  await this.maybeAutoExpandActiveHistory(requestedScrollOffset, {
9282
9353
  pages: AUTO_HISTORY_SCROLL_PAGE_COUNT,
9354
+ preserveDomAnchor: options.preserveDomAnchor === true,
9283
9355
  // Always bypass the offset gate. Every caller of this method fires
9284
9356
  // only when the DOM scroller is already AT the top — a direct
9285
9357
  // measurement. The gate is a second, weaker guess that compares a
@@ -9341,7 +9413,7 @@ export class PilotSwarmUiController {
9341
9413
  const pane = this.getScrollablePaneForFocus();
9342
9414
  if (!pane) return;
9343
9415
  if (this.paneUsesStickyBottomFollow(pane)) {
9344
- this.applyPaneVisualScrollOffset(pane, 0, { followBottom: false });
9416
+ this.scrollPaneTo(pane, 0);
9345
9417
  return;
9346
9418
  }
9347
9419
  const inspectorUsesBottomScroll = pane === "inspector" && this.inspectorUsesBottomScroll();
@@ -9621,6 +9693,7 @@ export class PilotSwarmUiController {
9621
9693
  requestedScrollOffset: targetOffset,
9622
9694
  autoTriggered: true,
9623
9695
  pages: options.pages,
9696
+ preserveDomAnchor: options.preserveDomAnchor === true,
9624
9697
  // Chat-driven pull: page backward over renderable message types
9625
9698
  // only, so each page is transcript instead of raw event noise.
9626
9699
  eventTypes: CHAT_HISTORY_EVENT_TYPES,
@@ -9757,9 +9830,12 @@ export class PilotSwarmUiController {
9757
9830
  pane: "chat",
9758
9831
  offset: 0,
9759
9832
  });
9760
- } else if (preserveChatView && previousScrollOffset > 0 && !options.autoTriggered) {
9761
- // EXPLICIT "load older" (the TUI's `e`): jump up to the start
9762
- // of what was just fetched — the user asked to SEE it.
9833
+ } else if (preserveChatView && options.autoTriggered && !options.preserveDomAnchor) {
9834
+ // Terminal panes store paused chat as a top offset. Prepending
9835
+ // must advance that offset by the rendered rows inserted above
9836
+ // it, preserving the same visible message. Browser panes use
9837
+ // measured DOM height because rich cards and wrapping do not
9838
+ // match terminal row metrics.
9763
9839
  const nextState = this.getState();
9764
9840
  const nextRenderedLines = this.getActiveChatRenderMetrics(nextState).totalLines;
9765
9841
  const addedLines = Math.max(0, nextRenderedLines - previousRenderedLines);
@@ -9771,14 +9847,6 @@ export class PilotSwarmUiController {
9771
9847
  });
9772
9848
  }
9773
9849
  }
9774
- // AUTO-triggered (scrolling reached the top): change NOTHING. The
9775
- // offset is distance-from-BOTTOM, which prepended content cannot
9776
- // move — leaving it alone keeps the viewport glued to the exact
9777
- // messages the user was reading, and they scroll on into the new
9778
- // page naturally. Adding the delta here teleported the view to
9779
- // the TOP of a 3.3x-larger window — observed as "loading history
9780
- // takes you way back instead of a few pages up".
9781
-
9782
9850
  const stateLabel = history.hasOlderEvents
9783
9851
  ? options.autoTriggered
9784
9852
  ? `Loaded older history page from CMS (${history.loadedEventCount} events loaded)`
@@ -3,6 +3,7 @@ import { isCanvasActionContent, parseCanvasActionContent } from "./canvas-action
3
3
  import { formatCompactionActivityRuns } from "./context-usage.js";
4
4
  import { canonicalSystemTitle } from "./system-titles.js";
5
5
  import { buildSessionWarning } from "./session-errors.js";
6
+ import { appendChatCall, CHAT_CALL_EVENT_TYPES } from "./chat-activity.js";
6
7
 
7
8
  export const DEFAULT_HISTORY_EVENT_LIMIT = 300;
8
9
  export const HISTORY_EVENT_LIMIT_STEPS = [
@@ -17,6 +18,7 @@ export const HISTORY_EVENT_LIMIT_STEPS = [
17
18
  // noisy sessions (thousands of tool/orchestration events between messages)
18
19
  // load transcript pages instead of raw-stream pages.
19
20
  export const CHAT_HISTORY_EVENT_TYPES = [
21
+ ...CHAT_CALL_EVENT_TYPES,
20
22
  "session.error",
21
23
  "user.message",
22
24
  "assistant.message",
@@ -354,6 +356,7 @@ function buildSessionMessageChatCard(event, text) {
354
356
  time: formatTimestamp(event.createdAt),
355
357
  createdAt: event.createdAt instanceof Date ? event.createdAt.getTime() : new Date(event.createdAt).getTime(),
356
358
  cardTitle: expectsResponse ? "Session Request" : "Session Message",
359
+ agentCallPreview: body,
357
360
  cardTitleColor: "cyan",
358
361
  cardBorderColor: "cyan",
359
362
  };
@@ -381,6 +384,7 @@ function buildSessionMessageChatCard(event, text) {
381
384
  time: formatTimestamp(event.createdAt),
382
385
  createdAt: event.createdAt instanceof Date ? event.createdAt.getTime() : new Date(event.createdAt).getTime(),
383
386
  cardTitle: "Session Reply",
387
+ agentCallPreview: body,
384
388
  cardTitleColor: "green",
385
389
  cardBorderColor: "green",
386
390
  };
@@ -405,6 +409,13 @@ function sharesClientMessageId(left, right) {
405
409
  function areMessagesEquivalent(left, right) {
406
410
  if (!left || !right) return false;
407
411
  if (left.role !== right.role) return false;
412
+ // Different agents can send the same short acknowledgement. Only a
413
+ // durable identity or a redelivered queue envelope can merge those calls.
414
+ if (left.sender?.kind === "agent" || right.sender?.kind === "agent") {
415
+ return left.sender?.kind === right.sender?.kind
416
+ && left.sender?.sessionId === right.sender?.sessionId
417
+ && (left.id === right.id || sharesClientMessageId(left, right) === true);
418
+ }
408
419
  // Identical text in separate model messages still represents separate
409
420
  // preview slots (and one may later become the final answer).
410
421
  if (left.role === "assistant" && left.messageId && right.messageId) {
@@ -415,6 +426,7 @@ function areMessagesEquivalent(left, right) {
415
426
  // link) — the redelivery time-window below would eat rev N-1 on every
416
427
  // reload, making the transcript disagree with what live append showed.
417
428
  if (left.kind === "canvas-update" || right.kind === "canvas-update"
429
+ || left.kind === "chat-call" || right.kind === "chat-call"
418
430
  || left.kind === "session-warning" || right.kind === "session-warning") {
419
431
  return left.kind === right.kind && left.id === right.id;
420
432
  }
@@ -1214,6 +1226,7 @@ export function buildHistoryModel(events = [], options = {}) {
1214
1226
 
1215
1227
  for (const event of events) {
1216
1228
  storedEvents.push(event);
1229
+ appendChatCall(chat, event);
1217
1230
  if (event.eventType === "session.error") {
1218
1231
  const warning = buildSessionWarning(event);
1219
1232
  if (warning) chat.push(warning);
@@ -1322,6 +1335,8 @@ export function appendEventToHistory(history, event) {
1322
1335
  }
1323
1336
  }
1324
1337
 
1338
+ appendChatCall(next.chat, event);
1339
+
1325
1340
  if (["user.message", "session.turn_completed", "session.turn_stopped", "session.epoch_committed"].includes(event.eventType)) {
1326
1341
  settleAssistantResponses(next.chat, event);
1327
1342
  }
@@ -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,46 @@ 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), ...(Number.isFinite(value.aspectRatio) && value.aspectRatio >= 0.2 && value.aspectRatio <= 8 ? { aspectRatio: value.aspectRatio } : {}) };
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) });
59
+ }
60
+ export function moveMoaDashboard(value, dashboardId, toIndex) {
61
+ const normalized = normalizeMoa(value);
62
+ const fromIndex = normalized.dashboards.findIndex(d => d.id === dashboardId);
63
+ if (fromIndex < 0 || !Number.isInteger(toIndex)) return normalized;
64
+ const dashboards = [...normalized.dashboards];
65
+ const [dashboard] = dashboards.splice(fromIndex, 1);
66
+ dashboards.splice(Math.max(0, Math.min(dashboards.length, toIndex)), 0, dashboard);
67
+ return { ...normalized, dashboards };
38
68
  }
39
69
  export function replaceMoaNode(tree, nodeId, next) {
40
70
  if (!tree) return null;
@@ -379,7 +379,7 @@ function mergeDefinedSessionFields(previousSession = {}, nextSession = {}) {
379
379
  // An absent/invalid counter must not erase the last known server version.
380
380
  if (key === "statusVersion" && previousVersion != null
381
381
  && sessionStatusVersion(nextSession) == null) continue;
382
- if (key === "pendingQuestion" && isAnsweredPendingQuestion(previousSession, value)) {
382
+ if (key === "pendingQuestion" && isAnsweredPendingQuestion(previousSession, value, nextSession)) {
383
383
  if (merged === previousSession) {
384
384
  merged = { ...(previousSession || {}) };
385
385
  }
@@ -555,10 +555,18 @@ function normalizedPendingQuestionText(pendingQuestion) {
555
555
  return String(pendingQuestion?.question || "").trim();
556
556
  }
557
557
 
558
- function isAnsweredPendingQuestion(previousSession, pendingQuestion) {
559
- const answeredQuestion = normalizedPendingQuestionText(previousSession?.answeredPendingQuestion);
558
+ function isAnsweredPendingQuestion(previousSession, pendingQuestion, nextSession) {
560
559
  const incomingQuestion = normalizedPendingQuestionText(pendingQuestion);
561
- 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
+ });
562
570
  }
563
571
 
564
572
  function pickDefaultActiveSessionId(sessions = []) {
@@ -638,6 +646,7 @@ function updateUiForSessionSelection(state, nextActiveSessionId) {
638
646
  },
639
647
  followBottom: {
640
648
  ...(state.ui.followBottom || {}),
649
+ chat: true,
641
650
  inspector: true,
642
651
  activity: true,
643
652
  },
@@ -1473,7 +1482,7 @@ function baseReducer(state, action) {
1473
1482
  };
1474
1483
 
1475
1484
  case "ui/followBottom": {
1476
- if (action.pane !== "inspector" && action.pane !== "activity") {
1485
+ if (action.pane !== "chat" && action.pane !== "inspector" && action.pane !== "activity") {
1477
1486
  return state;
1478
1487
  }
1479
1488
  const nextFollowBottom = Boolean(action.followBottom);
@@ -1868,14 +1877,15 @@ function baseReducer(state, action) {
1868
1877
 
1869
1878
  case "sessions/selected": {
1870
1879
  state = { ...state, sessions: { ...state.sessions, listDeselected: false } };
1871
- // Per-session chat scroll memory: stash the outgoing session's
1872
- // offset and restore the incoming one's. If new chat arrives on
1873
- // re-entry, history/set's activeChatUpdated reset still snaps to
1874
- // latest — "latest chat, or where you left it".
1880
+ // Per-session chat scroll memory: stash both the outgoing offset
1881
+ // and whether it follows the bottom. A paused reading position
1882
+ // stays paused as new messages arrive and across session switches.
1875
1883
  const previousActiveId = state.sessions.activeSessionId;
1876
1884
  const savedChatScroll = { ...(state.ui.chatScrollBySession || {}) };
1885
+ const savedChatFollowBottom = { ...(state.ui.chatFollowBottomBySession || {}) };
1877
1886
  if (previousActiveId && previousActiveId !== action.sessionId) {
1878
1887
  savedChatScroll[previousActiveId] = Number(state.ui.scroll?.chat) || 0;
1888
+ savedChatFollowBottom[previousActiveId] = state.ui.followBottom?.chat !== false;
1879
1889
  }
1880
1890
  // Per-session prompt drafts: a half-written message belongs to the
1881
1891
  // session it was written in. Stash the outgoing draft (text +
@@ -1928,6 +1938,7 @@ function baseReducer(state, action) {
1928
1938
  ui: {
1929
1939
  ...state.ui,
1930
1940
  chatScrollBySession: savedChatScroll,
1941
+ chatFollowBottomBySession: savedChatFollowBottom,
1931
1942
  promptDraftBySession: savedDrafts,
1932
1943
  prompt: nextPrompt,
1933
1944
  promptCursor: nextPromptCursor,
@@ -1946,6 +1957,7 @@ function baseReducer(state, action) {
1946
1957
  },
1947
1958
  followBottom: {
1948
1959
  ...(state.ui.followBottom || {}),
1960
+ chat: savedChatFollowBottom[action.sessionId] !== false,
1949
1961
  inspector: true,
1950
1962
  activity: true,
1951
1963
  },
@@ -2193,7 +2205,7 @@ function baseReducer(state, action) {
2193
2205
  ...state.history,
2194
2206
  bySessionId: nextHistory,
2195
2207
  },
2196
- ui: activeChatUpdated
2208
+ ui: activeChatUpdated && state.ui.followBottom?.chat !== false
2197
2209
  ? {
2198
2210
  ...state.ui,
2199
2211
  scroll: {
@@ -2214,6 +2226,8 @@ function baseReducer(state, action) {
2214
2226
  for (const id of ids) delete nextOutbox[id];
2215
2227
  const nextChatScroll = { ...(state.ui.chatScrollBySession || {}) };
2216
2228
  for (const id of ids) delete nextChatScroll[id];
2229
+ const nextChatFollowBottom = { ...(state.ui.chatFollowBottomBySession || {}) };
2230
+ for (const id of ids) delete nextChatFollowBottom[id];
2217
2231
  const nextDrafts = { ...(state.ui.promptDraftBySession || {}) };
2218
2232
  for (const id of ids) delete nextDrafts[id];
2219
2233
  return {
@@ -2229,6 +2243,7 @@ function baseReducer(state, action) {
2229
2243
  ui: {
2230
2244
  ...state.ui,
2231
2245
  chatScrollBySession: nextChatScroll,
2246
+ chatFollowBottomBySession: nextChatFollowBottom,
2232
2247
  promptDraftBySession: nextDrafts,
2233
2248
  },
2234
2249
  };
@@ -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);
@@ -448,10 +448,14 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
448
448
  activity: 0,
449
449
  filePreview: 0,
450
450
  },
451
- // Per-session chat scroll memory (rows from bottom), restored on
452
- // sessions/selected and evicted with the session's history.
451
+ // Per-session chat scroll memory. While following, the offset is
452
+ // distance from the bottom; once the reader scrolls up it becomes
453
+ // a stable viewport offset from the top. The paired follow flag
454
+ // disambiguates the two modes across session switches.
453
455
  chatScrollBySession: {},
456
+ chatFollowBottomBySession: {},
454
457
  followBottom: {
458
+ chat: true,
455
459
  inspector: true,
456
460
  activity: true,
457
461
  },
@@ -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