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
@@ -18,6 +18,10 @@ export const HISTORY_EVENT_LIMIT_STEPS = [
18
18
  export const CHAT_HISTORY_EVENT_TYPES = [
19
19
  "user.message",
20
20
  "assistant.message",
21
+ // Needed to distinguish interim assistant output from the final answer
22
+ // when an older transcript page is loaded.
23
+ "session.turn_completed",
24
+ "session.turn_stopped",
21
25
  "system.message",
22
26
  // Session regeneration boundary — rendered as an inline epoch divider in
23
27
  // the transcript, so it must survive backward chat-history paging.
@@ -246,14 +250,18 @@ function comparableMessageText(message) {
246
250
  export function parseAskedAndAnsweredExchange(text) {
247
251
  const source = String(text || "").replace(/\r\n/g, "\n").trim();
248
252
  const prefix = 'The user was asked: "';
249
- const marker = '"\nThe user responded: "';
250
-
251
253
  if (!source.startsWith(prefix) || !source.endsWith('"')) return null;
252
- const markerIndex = source.indexOf(marker, prefix.length);
253
- if (markerIndex === -1) return null;
254
-
255
- const question = source.slice(prefix.length, markerIndex);
256
- const answer = source.slice(markerIndex + marker.length, -1);
254
+ // Multi-writer sessions include the answering participant in the runtime
255
+ // wrapper. This is still an agent question followed by a human answer,
256
+ // not a human-authored copy of the entire question. Identity comes from
257
+ // the event's sender metadata, never from this display-only wrapper.
258
+ const marker = /"\nThe user responded(?: \(answered by [^\r\n]+?\))?: "/g;
259
+ marker.lastIndex = prefix.length;
260
+ const match = marker.exec(source);
261
+ if (!match) return null;
262
+
263
+ const question = source.slice(prefix.length, match.index);
264
+ const answer = source.slice(marker.lastIndex, -1);
257
265
  if (!question.trim() || !answer.trim()) return null;
258
266
  return { question, answer };
259
267
  }
@@ -395,6 +403,11 @@ function sharesClientMessageId(left, right) {
395
403
  function areMessagesEquivalent(left, right) {
396
404
  if (!left || !right) return false;
397
405
  if (left.role !== right.role) return false;
406
+ // Identical text in separate model messages still represents separate
407
+ // preview slots (and one may later become the final answer).
408
+ if (left.role === "assistant" && left.messageId && right.messageId) {
409
+ return left.messageId === right.messageId;
410
+ }
398
411
  // Canvas revision lines are platform-minted, exactly-once (one durable
399
412
  // event per rev), and share identical text by design (the artifact://
400
413
  // link) — the redelivery time-window below would eat rev N-1 on every
@@ -530,18 +543,118 @@ function buildChatMessage(event, role) {
530
543
  const attachments = Array.isArray(event?.data?.attachments)
531
544
  ? event.data.attachments.filter((a) => a && typeof a.filename === "string" && a.filename)
532
545
  : [];
546
+ const messageId = typeof event?.data?.messageId === "string" && event.data.messageId
547
+ ? event.data.messageId
548
+ : null;
549
+ const chatRole = deriveChatRole(event, role, text);
533
550
  return {
534
551
  id: `${event.sessionId}:${event.seq}`,
535
- role: deriveChatRole(event, role, text),
552
+ role: chatRole,
536
553
  text,
537
554
  time: formatTimestamp(event.createdAt),
538
555
  createdAt: event.createdAt instanceof Date ? event.createdAt.getTime() : new Date(event.createdAt).getTime(),
539
556
  ...(clientMessageIds.length > 0 ? { clientMessageIds } : {}),
540
557
  ...(sender ? { sender } : {}),
541
558
  ...(attachments.length > 0 ? { attachments } : {}),
559
+ ...(messageId ? { messageId } : {}),
560
+ ...(chatRole === "assistant" ? {
561
+ assistantPreview: true,
562
+ responsePending: true,
563
+ responseFinal: false,
564
+ responseCanFinalize: !event?.data?.parentToolCallId && !event?.data?.toolRequests?.length,
565
+ ...(typeof event?.data?.reasoningText === "string" && event.data.reasoningText
566
+ ? { liveReasoningText: event.data.reasoningText } : {}),
567
+ } : {}),
542
568
  };
543
569
  }
544
570
 
571
+ // A saved assistant.message is not necessarily the answer: a single runTurn
572
+ // can contain many tool-loop utterances. Only its successful durable boundary
573
+ // promotes the last eligible message. Use the same rule for replay and append.
574
+ function settleAssistantResponses(chat, event) {
575
+ const resultType = event?.data?.resultType || event?.data?.result;
576
+ const successful = event?.eventType === "session.turn_completed"
577
+ && (!resultType || resultType === "completed");
578
+ let finalIndex = -1;
579
+ for (let index = chat.length - 1; index >= 0; index -= 1) {
580
+ if (chat[index]?.responsePending) {
581
+ if (successful && chat[index].responseCanFinalize) finalIndex = index;
582
+ break;
583
+ }
584
+ }
585
+ for (let index = 0; index < chat.length; index += 1) {
586
+ if (chat[index]?.responsePending) chat[index] = {
587
+ ...chat[index], responsePending: false, responseFinal: index === finalIndex,
588
+ };
589
+ }
590
+ }
591
+
592
+ /** Apply one transient `turn` topic snapshot without touching durable events/seq. */
593
+ export function applyLiveTurnToHistory(history, payload, meta = {}) {
594
+ if (!payload) return history;
595
+ if (payload.phase === "idle") return clearLiveTurnFromHistory(history, null, payload.streamId || null);
596
+ if (payload.phase !== "live") return history;
597
+ const messageId = typeof payload.messageId === "string" && payload.messageId ? payload.messageId : null;
598
+ const reasoningId = typeof payload.reasoningId === "string" && payload.reasoningId ? payload.reasoningId : null;
599
+ const liveKey = messageId || (reasoningId ? `reasoning:${reasoningId}` : "active-turn");
600
+ const createdAt = Number(meta.createdAt) || Date.now();
601
+ const current = history || { chat: [], activity: [], events: [], lastSeq: 0 };
602
+ const oldChat = current.chat || [];
603
+ // Durable truth wins even when a delayed retained snapshot arrives later.
604
+ if ((messageId && oldChat.some((item) => !item.liveTurn && item.messageId === messageId))
605
+ || (current.closedLiveKeys || []).includes(liveKey)
606
+ || (payload.streamId && (current.closedLiveStreams || []).includes(payload.streamId))) return current;
607
+ const matches = (item) => item?.liveTurn === true && (
608
+ item?.liveKey === liveKey
609
+ || (messageId && item?.messageId === messageId)
610
+ || (reasoningId && item?.reasoningId === reasoningId && (!item.messageId || !messageId))
611
+ );
612
+ const firstMatch = oldChat.findIndex(matches);
613
+ const previous = firstMatch >= 0 ? oldChat[firstMatch] : null;
614
+ const chat = oldChat.filter((item) => !matches(item));
615
+ const reasoningText = typeof payload.reasoningText === "string" ? payload.reasoningText : "";
616
+ const text = typeof payload.text === "string" ? payload.text : "";
617
+ const liveStartedAt = Number(previous?.liveStartedAt || previous?.createdAt) || createdAt;
618
+ // Reasoning and answer text are one visual turn, not two independently
619
+ // mounted transcript rows. Keeping them in one keyed item lets the browser
620
+ // collapse reasoning above the answer and lets the durable final settle
621
+ // into the exact same React component instead of flashing between nodes.
622
+ const replacement = (reasoningText || text) ? [{
623
+ id: previous?.id || `live:${meta.sessionId || ""}:${liveKey}`,
624
+ role: "assistant",
625
+ ...(messageId ? { messageId } : {}),
626
+ ...(reasoningId ? { reasoningId } : {}),
627
+ liveKey: previous?.liveKey || liveKey,
628
+ liveTurn: true,
629
+ streamId: payload.streamId || previous?.streamId || null,
630
+ streaming: true,
631
+ truncated: payload.truncated === true,
632
+ text,
633
+ reasoningText,
634
+ liveStartedAt,
635
+ createdAt: liveStartedAt,
636
+ time: "",
637
+ }] : [];
638
+ chat.splice(firstMatch < 0 ? chat.length : Math.min(firstMatch, chat.length), 0, ...replacement);
639
+ return { ...current, chat };
640
+ }
641
+
642
+ export function clearLiveTurnFromHistory(history, messageId = null, streamId = null) {
643
+ if (!history?.chat) return history;
644
+ const chat = history.chat.filter((item) => !(
645
+ item?.liveTurn === true && (!messageId || item?.messageId === messageId) && (!streamId || item?.streamId === streamId)
646
+ ));
647
+ const retired = history.chat.filter((item) => item.liveTurn && (!messageId || item.messageId === messageId) && (!streamId || item.streamId === streamId));
648
+ const closedLiveKeys = [...new Set([...(history.closedLiveKeys || []),
649
+ ...retired.flatMap((item) => [item.liveKey, item.messageId, item.reasoningId && `reasoning:${item.reasoningId}`].filter(Boolean)),
650
+ ])].slice(-256);
651
+ const closedLiveStreams = [...new Set([...(history.closedLiveStreams || []),
652
+ ...(!messageId ? retired.map((item) => item.streamId).filter(Boolean) : []),
653
+ ...(streamId ? [streamId] : []),
654
+ ])].slice(-256);
655
+ return chat.length === history.chat.length && !streamId ? history : { ...history, chat, closedLiveKeys, closedLiveStreams };
656
+ }
657
+
545
658
  // The Copilot SDK echoes the system prompt as a system.message on EVERY
546
659
  // turn, and the worker keeps only a 120-char snippet of it (session-proxy.ts,
547
660
  // "Full content omitted from CMS"). For one release (0.5.47) each echo also
@@ -765,6 +878,12 @@ function formatActivity(event) {
765
878
  case "assistant.streaming_progress":
766
879
  return null;
767
880
 
881
+ case "model.call_start": {
882
+ const model = String(event?.data?.model || event?.data?.modelId || "model").split(":").pop();
883
+ runs = buildLabeledActivityRuns(time, "[model]", "cyan", `calling ${model}`, "white");
884
+ break;
885
+ }
886
+
768
887
  case "session.artifact_presented": {
769
888
  const data = (event?.data ?? {}) || {};
770
889
  const filename = String(data.filename || "artifact");
@@ -1092,6 +1211,9 @@ export function buildHistoryModel(events = [], options = {}) {
1092
1211
 
1093
1212
  for (const event of events) {
1094
1213
  storedEvents.push(event);
1214
+ if (["user.message", "session.turn_completed", "session.turn_stopped", "session.epoch_committed"].includes(event.eventType)) {
1215
+ settleAssistantResponses(chat, event);
1216
+ }
1095
1217
  if (event.eventType === "user.message") {
1096
1218
  activity.push(...buildEmbeddedSystemNoticeActivityItems(event, "user"));
1097
1219
  const message = buildChatMessage(event, "user");
@@ -1167,6 +1289,8 @@ export function appendEventToHistory(history, event) {
1167
1289
  ? existingEvents
1168
1290
  : [...existingEvents, event].slice(-loadedEventLimit);
1169
1291
  const next = {
1292
+ closedLiveKeys: history?.closedLiveKeys || [],
1293
+ closedLiveStreams: history?.closedLiveStreams || [],
1170
1294
  chat: clampHistoryItems(history?.chat || [], loadedEventLimit),
1171
1295
  activity: clampHistoryItems(history?.activity || [], loadedEventLimit),
1172
1296
  events: nextEvents,
@@ -1184,6 +1308,10 @@ export function appendEventToHistory(history, event) {
1184
1308
  stoppedMessageIds: Array.isArray(history?.stoppedMessageIds) ? history.stoppedMessageIds : [],
1185
1309
  };
1186
1310
 
1311
+ if (["user.message", "session.turn_completed", "session.turn_stopped", "session.epoch_committed"].includes(event.eventType)) {
1312
+ settleAssistantResponses(next.chat, event);
1313
+ }
1314
+
1187
1315
  // A stopped turn leaves a durable session.turn_stopped carrying the
1188
1316
  // interrupted prompt's clientMessageIds. Record them and retroactively
1189
1317
  // flag any matching transcript message (the user.message usually lands
@@ -1201,6 +1329,7 @@ export function appendEventToHistory(history, event) {
1201
1329
  : m
1202
1330
  ));
1203
1331
  }
1332
+ Object.assign(next, clearLiveTurnFromHistory(next));
1204
1333
  return next;
1205
1334
  }
1206
1335
 
@@ -1241,10 +1370,34 @@ export function appendEventToHistory(history, event) {
1241
1370
  next.activity = clampHistoryItems(next.activity, loadedEventLimit);
1242
1371
  const message = buildChatMessage(event, "assistant");
1243
1372
  if (!message) return next;
1244
- next.chat.push(message);
1373
+ const liveIndex = message.messageId
1374
+ ? next.chat.findIndex((item) => item?.liveTurn === true && item?.messageId === message.messageId)
1375
+ : -1;
1376
+ const liveItem = liveIndex >= 0 ? next.chat[liveIndex] : null;
1377
+ Object.assign(next, clearLiveTurnFromHistory(next, message.messageId || null));
1378
+ // Preserve the model-call slot when several assistant messages are
1379
+ // interleaved: the durable final replaces its own live bubble in
1380
+ // place instead of jumping behind a later streaming message.
1381
+ if (liveIndex >= 0) {
1382
+ const settledAt = Date.now();
1383
+ const liveStartedAt = Number(liveItem?.liveStartedAt || liveItem?.createdAt) || settledAt;
1384
+ // Keep the disclosure identity even for short streams. Committing
1385
+ // an interim message must not reset expansion or its scroll box.
1386
+ Object.assign(message, {
1387
+ streamSettling: true,
1388
+ liveKey: liveItem?.liveKey || message.messageId,
1389
+ liveStartedAt,
1390
+ liveReasoningText: message.liveReasoningText || liveItem?.reasoningText || "",
1391
+ });
1392
+ next.chat.splice(Math.min(liveIndex, next.chat.length), 0, message);
1393
+ }
1394
+ else next.chat.push(message);
1245
1395
  next.chat = clampHistoryItems(dedupeChatMessages(next.chat), loadedEventLimit);
1246
1396
  return next;
1247
1397
  }
1398
+ if (event.eventType === "session.turn_completed" || event.eventType === "session.turn_stopped") {
1399
+ Object.assign(next, clearLiveTurnFromHistory(next));
1400
+ }
1248
1401
  if (event.eventType === "system.message") {
1249
1402
  if (shouldRenderSystemMessageAsActivity(event)) {
1250
1403
  const activityItem = formatActivity(event);
@@ -817,6 +817,9 @@ function normalizePromptAttachments(prompt, attachments) {
817
817
  }
818
818
 
819
819
  export function appReducer(state, action) {
820
+ const sessionId = action.sessionId ?? action.session?.sessionId;
821
+ const contentUpdate = /^(history|files|canvas|orchestration|executionHistory|sessionStats|outbox)\//.test(action.type) || action.type === "sessions/merged";
822
+ if (sessionId && contentUpdate && state.sessions?.goneIds?.includes(sessionId)) return state;
820
823
  const next = baseReducer(state, action);
821
824
  if (next === state) return next;
822
825
  return reconcileSessionView(state, next, action);
@@ -1567,7 +1570,10 @@ function baseReducer(state, action) {
1567
1570
  if (!goneId) return state;
1568
1571
  const hadRow = Boolean(state.sessions.byId[goneId]);
1569
1572
  const wasActive = state.sessions.activeSessionId === goneId;
1570
- if (!hadRow && !wasActive) return state;
1573
+ const hadContent = state.history.bySessionId.has(goneId)
1574
+ || [state.files, state.canvas, state.orchestration, state.executionHistory, state.outbox, state.sessionStats]
1575
+ .some((slice) => Object.hasOwn(slice?.bySessionId || {}, goneId));
1576
+ if (!hadRow && !wasActive && !hadContent) return state;
1571
1577
  const evictedById = { ...state.sessions.byId };
1572
1578
  delete evictedById[goneId];
1573
1579
  // Re-derive: the evicted row may have been the running CHILD that
@@ -1578,13 +1584,29 @@ function baseReducer(state, action) {
1578
1584
  const selectedIds = Array.isArray(state.sessions.selectedIds)
1579
1585
  ? state.sessions.selectedIds.filter((id) => id !== goneId)
1580
1586
  : state.sessions.selectedIds;
1581
- return {
1582
- ...state,
1587
+ const history = new Map(state.history.bySessionId);
1588
+ history.delete(goneId);
1589
+ const discard = (slice) => {
1590
+ if (!slice?.bySessionId) return slice;
1591
+ const bySessionId = { ...slice.bySessionId };
1592
+ delete bySessionId[goneId];
1593
+ return { ...slice, bySessionId };
1594
+ };
1595
+ return {
1596
+ ...state,
1597
+ history: { ...state.history, bySessionId: history },
1598
+ files: discard(state.files),
1599
+ orchestration: discard(state.orchestration),
1600
+ executionHistory: discard(state.executionHistory),
1601
+ canvas: discard(state.canvas),
1602
+ outbox: discard(state.outbox),
1603
+ sessionStats: discard(state.sessionStats),
1583
1604
  sessions: {
1584
1605
  ...state.sessions,
1585
1606
  byId,
1586
1607
  selectedIds,
1587
1608
  activeSessionId: wasActive ? null : state.sessions.activeSessionId,
1609
+ goneIds: [...new Set([...(state.sessions.goneIds || []), goneId])].slice(-1000),
1588
1610
  },
1589
1611
  };
1590
1612
  }
@@ -1681,6 +1703,7 @@ function baseReducer(state, action) {
1681
1703
  const nextSessions = {
1682
1704
  ...state.sessions,
1683
1705
  byId: rowStatusById,
1706
+ goneIds: (state.sessions.goneIds || []).filter((id) => !action.sessions.some((row) => row.sessionId === id)),
1684
1707
  collapsedIds,
1685
1708
  collapsedIdsExplicit: previousCollapsedIdsExplicit,
1686
1709
  listingSeen: true,
@@ -2940,7 +2963,10 @@ function baseReducer(state, action) {
2940
2963
  const list = Array.isArray(action.list) ? action.list : [];
2941
2964
  // Keep the selection when the package still exists; otherwise clear
2942
2965
  // the dependent detail/workspace state with it.
2943
- const stillThere = previous.selectedName && list.some((p) => p.name === previous.selectedName);
2966
+ const selector = previous.selectedSelector;
2967
+ const stillThere = previous.selectedName && list.some((p) => p.name === previous.selectedName
2968
+ && (!selector?.scope || p.scope === selector.scope)
2969
+ && (!selector?.owner || (p.owner?.provider === selector.owner.provider && p.owner?.subject === selector.owner.subject)));
2944
2970
  return {
2945
2971
  ...state,
2946
2972
  admin: {
@@ -2954,7 +2980,8 @@ function baseReducer(state, action) {
2954
2980
  fetchedAt: Date.now(),
2955
2981
  selectedName: stillThere ? previous.selectedName : null,
2956
2982
  selectedSelector: stillThere ? (previous.selectedSelector ?? null) : null,
2957
- ...(stillThere ? {} : { detail: null, workspace: { ...previous.workspace, tree: null, selectedPath: null, file: null } }),
2983
+ ...(stillThere ? {} : { selectionSeq: (previous.selectionSeq ?? 0) + 1, detail: null, changelog: null,
2984
+ workspace: { ...previous.workspace, tree: null, selectedPath: null, file: null } }),
2958
2985
  },
2959
2986
  },
2960
2987
  };