pilotswarm 0.5.32 → 0.5.34

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 (98) hide show
  1. package/package.json +2 -2
  2. package/tui/src/app.js +10 -0
  3. package/tui/src/node-sdk-transport.js +11 -0
  4. package/ui/core/src/controller.js +381 -12
  5. package/ui/core/src/reducer.js +125 -28
  6. package/ui/core/src/selectors.js +158 -268
  7. package/ui/core/src/state.js +6 -1
  8. package/ui/core/src/themes/catppuccin-mocha.js +2 -1
  9. package/ui/core/src/themes/cobalt2.js +2 -1
  10. package/ui/core/src/themes/doom.js +2 -1
  11. package/ui/core/src/themes/dracula.js +2 -1
  12. package/ui/core/src/themes/github-dark.js +2 -1
  13. package/ui/core/src/themes/github-light.js +2 -1
  14. package/ui/core/src/themes/gruvbox-dark.js +2 -1
  15. package/ui/core/src/themes/hacker-x-matrix.js +2 -1
  16. package/ui/core/src/themes/hacker-x-orion-prime.js +2 -1
  17. package/ui/core/src/themes/helpers.js +22 -4
  18. package/ui/core/src/themes/high-contrast-mono.js +2 -1
  19. package/ui/core/src/themes/index.js +24 -5
  20. package/ui/core/src/themes/ms-dos.js +2 -1
  21. package/ui/core/src/themes/noctis-obscuro.js +2 -1
  22. package/ui/core/src/themes/noctis.js +2 -1
  23. package/ui/core/src/themes/rust.js +125 -0
  24. package/ui/core/src/themes/spongebob.js +110 -0
  25. package/ui/core/src/themes/terminal-green.js +2 -1
  26. package/ui/core/src/themes/win95.js +2 -1
  27. package/ui/core/src/themes/winamp.js +2 -1
  28. package/ui/core/src/themes/workspace-dark.js +2 -1
  29. package/ui/react/src/web-app.js +240 -266
  30. package/web/dist/assets/{abnfDiagram-VRR7QNED-mXFQ4a3c.js → abnfDiagram-VRR7QNED-BFOimVWK.js} +1 -1
  31. package/web/dist/assets/{arc-BPuuvbge.js → arc-uUHjvwEo.js} +1 -1
  32. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-Dt4uUYIe.js → architectureDiagram-ZJ3FMSHR-CxeX_vea.js} +1 -1
  33. package/web/dist/assets/{blockDiagram-677ZJIJ3-Ax8Q22vV.js → blockDiagram-677ZJIJ3-Cno8kltG.js} +1 -1
  34. package/web/dist/assets/{c4Diagram-LMCZKHZV-CPiukyHt.js → c4Diagram-LMCZKHZV-viONVybf.js} +1 -1
  35. package/web/dist/assets/channel-IURmSsjn.js +1 -0
  36. package/web/dist/assets/{chunk-2Q5K7J3B-VJLqCZJX.js → chunk-2Q5K7J3B-CKfICIGD.js} +1 -1
  37. package/web/dist/assets/{chunk-32BRIVSS-DqOshxhf.js → chunk-32BRIVSS-CxmwEY56.js} +1 -1
  38. package/web/dist/assets/{chunk-5VM5RSS4-KPqV21jV.js → chunk-5VM5RSS4-_eVYLGll.js} +1 -1
  39. package/web/dist/assets/{chunk-EX3LRPZG-DfhiCVl_.js → chunk-EX3LRPZG-C4aqbjrf.js} +1 -1
  40. package/web/dist/assets/{chunk-JWPE2WC7-H6khJPuW.js → chunk-JWPE2WC7-Bh8ZxrI8.js} +1 -1
  41. package/web/dist/assets/{chunk-MOJQB5TN-BKMAzQxj.js → chunk-MOJQB5TN-BXgLlWVP.js} +1 -1
  42. package/web/dist/assets/{chunk-RYQCIY6F-DNXRTxtj.js → chunk-RYQCIY6F-B1rMIdfF.js} +1 -1
  43. package/web/dist/assets/{chunk-V7JOEXUC-B56PM6Gv.js → chunk-V7JOEXUC-C3AnSHXk.js} +1 -1
  44. package/web/dist/assets/{chunk-VR4S4FIN-Boi3hGkB.js → chunk-VR4S4FIN-CeoMJIBs.js} +1 -1
  45. package/web/dist/assets/{chunk-XXDRQBXY-DmJyn890.js → chunk-XXDRQBXY-DzaxQ9iP.js} +1 -1
  46. package/web/dist/assets/classDiagram-OUVF2IWQ-FsfpM3iu.js +1 -0
  47. package/web/dist/assets/classDiagram-v2-EOCWNBFH-FsfpM3iu.js +1 -0
  48. package/web/dist/assets/{cose-bilkent-JH36ORCC-BRdFlFWt.js → cose-bilkent-JH36ORCC-G46fvUOx.js} +1 -1
  49. package/web/dist/assets/{cynefin-VYW2F7L2-DqE-xib2.js → cynefin-VYW2F7L2-DXwqVhWA.js} +1 -1
  50. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-D-QBZjIL.js → cynefinDiagram-TSTJHNR4-DNNFmaXb.js} +1 -1
  51. package/web/dist/assets/{dagre-VKFMJZFB-DYu9oQV6.js → dagre-VKFMJZFB-BCfh-h3o.js} +1 -1
  52. package/web/dist/assets/{diagram-FQU43EPY-DOwYccCg.js → diagram-FQU43EPY-CyG9b6bj.js} +1 -1
  53. package/web/dist/assets/{diagram-G47NLZAW-DTSEmrPr.js → diagram-G47NLZAW-DdxDahK3.js} +1 -1
  54. package/web/dist/assets/{diagram-NH7WQ7WH-OqbzzquA.js → diagram-NH7WQ7WH-CjrZ_eja.js} +1 -1
  55. package/web/dist/assets/{diagram-OA4YK3LP-BMVG6A8I.js → diagram-OA4YK3LP-5NAjQpk2.js} +1 -1
  56. package/web/dist/assets/{diagram-WEI45ONY-Rn2Q5xaz.js → diagram-WEI45ONY-fLuDb54-.js} +1 -1
  57. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-aL32_v5y.js → ebnfDiagram-CCIWWBDH-DL9qnZpa.js} +1 -1
  58. package/web/dist/assets/{erDiagram-Q63AITRT-DKCpyczx.js → erDiagram-Q63AITRT-C_ynfrWY.js} +1 -1
  59. package/web/dist/assets/{flowDiagram-23GEKE2U-CrZTPgR_.js → flowDiagram-23GEKE2U-dYuH8HYv.js} +1 -1
  60. package/web/dist/assets/{ganttDiagram-NO4QXBWP-DnSE1OtC.js → ganttDiagram-NO4QXBWP-e8Meh3f3.js} +1 -1
  61. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-C5SeYPUR.js → gitGraphDiagram-IHSO6WYX-D_tZ4VL9.js} +1 -1
  62. package/web/dist/assets/index-Ct7SQv1M.css +1 -0
  63. package/web/dist/assets/{index-B-TEKrRB.js → index-DcQes9h8.js} +6 -6
  64. package/web/dist/assets/{infoDiagram-FWYZ7A6U-DDnJZp0X.js → infoDiagram-FWYZ7A6U-Dffhqp73.js} +1 -1
  65. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-ewhzfFkq.js → ishikawaDiagram-FXEZZL3T-DYt9H7kU.js} +1 -1
  66. package/web/dist/assets/{journeyDiagram-5HDEW3XC-T0iWWkLM.js → journeyDiagram-5HDEW3XC-Qv1vVu0F.js} +1 -1
  67. package/web/dist/assets/{kanban-definition-HUTT4EX6-D1MFQZjb.js → kanban-definition-HUTT4EX6-CTAUo40G.js} +1 -1
  68. package/web/dist/assets/{linear-BKfPK9wE.js → linear-CqLsWFLj.js} +1 -1
  69. package/web/dist/assets/{mermaid.core-CyzD7dTc.js → mermaid.core-okRCGPQQ.js} +4 -4
  70. package/web/dist/assets/{mindmap-definition-LN4V7U3C-CWXtcTlP.js → mindmap-definition-LN4V7U3C-DVOOrIwD.js} +1 -1
  71. package/web/dist/assets/{papaparse.min-DLUoV5k5.js → papaparse.min-Ddflu8oC.js} +1 -1
  72. package/web/dist/assets/{pegDiagram-2B236MQR-BFjZun-r.js → pegDiagram-2B236MQR-BLJlC0MZ.js} +1 -1
  73. package/web/dist/assets/{pieDiagram-ENE6RG2P-DMDtW4g7.js → pieDiagram-ENE6RG2P-DdOwICOg.js} +1 -1
  74. package/web/dist/assets/pilotswarm-CKh2MxfL.js +99 -0
  75. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-CAwPVaYL.js → quadrantDiagram-ABIIQ3AL-B0Th1YO5.js} +1 -1
  76. package/web/dist/assets/{railroadDiagram-RFXS5EU6-Co102PkQ.js → railroadDiagram-RFXS5EU6-BF2XKMKf.js} +1 -1
  77. package/web/dist/assets/{requirementDiagram-TGXJPOKE-D19VlRSl.js → requirementDiagram-TGXJPOKE-Mr1iSwbS.js} +1 -1
  78. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-Sd_a3ukL.js → sankeyDiagram-HTMAVEWB-CrOjVZyg.js} +1 -1
  79. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CojDRK1c.js → sequenceDiagram-DBY2YBRQ-H0X4Lydk.js} +1 -1
  80. package/web/dist/assets/{sizeCapture-X5ZJPWSS-XqXcxnDo.js → sizeCapture-X5ZJPWSS-Dus1QEqh.js} +1 -1
  81. package/web/dist/assets/{stateDiagram-2N3HPSRC-BcN_w699.js → stateDiagram-2N3HPSRC-BOBUSwdR.js} +1 -1
  82. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DABTDwFh.js +1 -0
  83. package/web/dist/assets/{swimlanes-5IMT3BWC-BcO6ai_I.js → swimlanes-5IMT3BWC-BoqT-Lh7.js} +2 -2
  84. package/web/dist/assets/swimlanesDiagram-G3AALYLV-BF5mFN4t.js +8 -0
  85. package/web/dist/assets/{timeline-definition-FHXFAJF6-DOPfYxUq.js → timeline-definition-FHXFAJF6-BOrqUQHX.js} +1 -1
  86. package/web/dist/assets/{vennDiagram-L72KCM5P-BSKLkEOC.js → vennDiagram-L72KCM5P-DDkNL-Oo.js} +1 -1
  87. package/web/dist/assets/{wardleyDiagram-EHGQE667-BdcYAVw2.js → wardleyDiagram-EHGQE667-XUAJQ2X0.js} +1 -1
  88. package/web/dist/assets/{xychartDiagram-FW5EYKEG-C6EJT4jB.js → xychartDiagram-FW5EYKEG-ph69PV9y.js} +1 -1
  89. package/web/dist/index.html +3 -3
  90. package/ui/core/src/themes/dark-high-contrast.js +0 -56
  91. package/ui/core/src/themes/workspace-dark-rich.js +0 -65
  92. package/web/dist/assets/channel-BCzDctYg.js +0 -1
  93. package/web/dist/assets/classDiagram-OUVF2IWQ-ChqjVZil.js +0 -1
  94. package/web/dist/assets/classDiagram-v2-EOCWNBFH-ChqjVZil.js +0 -1
  95. package/web/dist/assets/index-D2B5bzSN.css +0 -1
  96. package/web/dist/assets/pilotswarm-DA0u7Cq_.js +0 -99
  97. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-BV_w1D3C.js +0 -1
  98. package/web/dist/assets/swimlanesDiagram-G3AALYLV-BnYdP0qP.js +0 -8
@@ -321,15 +321,19 @@ function mergeDefinedSessionFields(previousSession = {}, nextSession = {}) {
321
321
  return merged;
322
322
  }
323
323
 
324
+ // Mirrors selectors' getSessionVisualStatus — the two must agree or the row
325
+ // label (debounced, from here) and the summary label (immediate, from there)
326
+ // will disagree mid-turn.
324
327
  function computeRawSessionVisualStatus(session) {
325
328
  if (!session) return "unknown";
326
329
  const status = session.status || "unknown";
327
- if (
328
- session.cronActive === true
329
- && (status === "waiting" || status === "idle" || status === "unknown")
330
- ) {
330
+ const dormant = status === "waiting" || status === "idle" || status === "unknown";
331
+ if (session.cronActive === true && dormant) {
331
332
  return "cron_waiting";
332
333
  }
334
+ if (dormant && (Number(session.activeChildCount) || 0) > 0) {
335
+ return "awaiting_children";
336
+ }
333
337
  return status;
334
338
  }
335
339
 
@@ -396,6 +400,76 @@ function mergeSessionRowVisualStatus(previousSession, nextSession, nowMs) {
396
400
  };
397
401
  }
398
402
 
403
+ // A sub-agent that finishes its task stays alive and IDLE waiting for follow-up
404
+ // (see managed-session's spawn_agent contract), so "has children" says nothing
405
+ // about whether work is still happening below. Only these two statuses mean the
406
+ // parent is genuinely blocked on someone else.
407
+ const ACTIVE_DESCENDANT_STATUSES = new Set(["running", "input_required"]);
408
+
409
+ /**
410
+ * Stamp `activeChildCount` — the number of running/input_required descendants,
411
+ * at any depth — onto every session that has one. selectors' visual-status
412
+ * derivation reads it to distinguish "idle because it is waiting on its
413
+ * children" from "idle because there is nothing to do".
414
+ *
415
+ * Counted transitively so the whole ancestor chain lights up: without that, a
416
+ * parent whose direct child is itself only waiting on a grandchild would read
417
+ * as plain idle and the delegation would look dead.
418
+ *
419
+ * Returns `byId` unchanged when nothing moved, so this stays free for the
420
+ * common poll that changes nothing.
421
+ */
422
+ function applyActiveDescendantCounts(byId) {
423
+ const counts = new Map();
424
+ for (const session of Object.values(byId || {})) {
425
+ if (!session?.sessionId || session.isGroup) continue;
426
+ if (!ACTIVE_DESCENDANT_STATUSES.has(session.status)) continue;
427
+ // A malformed parent chain (self-parent, cycle) would spin forever;
428
+ // `seen` bounds the walk to one visit per ancestor.
429
+ const seen = new Set([session.sessionId]);
430
+ let parentId = session.parentSessionId;
431
+ while (parentId && byId[parentId] && !seen.has(parentId)) {
432
+ seen.add(parentId);
433
+ counts.set(parentId, (counts.get(parentId) || 0) + 1);
434
+ parentId = byId[parentId].parentSessionId;
435
+ }
436
+ }
437
+
438
+ let changed = false;
439
+ const next = {};
440
+ for (const [sessionId, session] of Object.entries(byId || {})) {
441
+ const nextCount = counts.get(sessionId) || 0;
442
+ const previousCount = Number(session?.activeChildCount) || 0;
443
+ if (nextCount === previousCount) {
444
+ next[sessionId] = session;
445
+ continue;
446
+ }
447
+ changed = true;
448
+ next[sessionId] = nextCount > 0
449
+ ? { ...session, activeChildCount: nextCount }
450
+ : { ...session, activeChildCount: undefined };
451
+ }
452
+ return changed ? next : byId;
453
+ }
454
+
455
+ /**
456
+ * Run the row-status debounce across a whole map. A session going running or
457
+ * idle moves its ANCESTORS' visual status too (they gain or lose
458
+ * awaiting_children), and those ancestors are not in the payload that caused
459
+ * the change — so the debounce has to be re-evaluated for everyone, not just
460
+ * the rows that arrived.
461
+ */
462
+ function applyRowVisualStatuses(previousById = {}, nextById = {}, nowMs) {
463
+ let changed = false;
464
+ const out = {};
465
+ for (const [sessionId, session] of Object.entries(nextById)) {
466
+ const merged = mergeSessionRowVisualStatus(previousById[sessionId], session, nowMs);
467
+ out[sessionId] = merged;
468
+ if (merged !== session) changed = true;
469
+ }
470
+ return changed ? out : nextById;
471
+ }
472
+
399
473
  function normalizedPendingQuestionText(pendingQuestion) {
400
474
  return String(pendingQuestion?.question || "").trim();
401
475
  }
@@ -681,6 +755,10 @@ export function appReducer(state, action) {
681
755
  },
682
756
  };
683
757
 
758
+ case "ui/touchScale":
759
+ if (Boolean(action.enabled) === Boolean(state.ui.touchScale)) return state;
760
+ return { ...state, ui: { ...state.ui, touchScale: Boolean(action.enabled) } };
761
+
684
762
  case "profileSettings/apply": {
685
763
  const settings = action.settings && typeof action.settings === "object" && !Array.isArray(action.settings)
686
764
  ? action.settings
@@ -690,8 +768,10 @@ export function appReducer(state, action) {
690
768
  && settings.themeId.trim();
691
769
  const hasOwnerFilter = Object.prototype.hasOwnProperty.call(settings, "sessionOwnerFilter");
692
770
  const hasLayout = Object.prototype.hasOwnProperty.call(settings, "layoutAdjustments");
693
- // "rich" retired as a view mode (now theme.richChat) — stored
694
- // profiles that still say "rich" simply keep the transcript.
771
+ // "rich" was retired as a view mode — stored profiles that still
772
+ // say "rich" fall through to the transcript below.
773
+ const hasTouchScale = Object.prototype.hasOwnProperty.call(settings, "touchScale")
774
+ && typeof settings.touchScale === "boolean";
695
775
  const hasChatViewMode = Object.prototype.hasOwnProperty.call(settings, "chatViewMode")
696
776
  && (settings.chatViewMode === "summary" || settings.chatViewMode === "transcript");
697
777
  const hasPins = Object.prototype.hasOwnProperty.call(settings, "pinnedSessionIds");
@@ -755,6 +835,7 @@ export function appReducer(state, action) {
755
835
  ui: {
756
836
  ...selection.ui,
757
837
  themeId: hasTheme ? settings.themeId.trim() : selection.ui.themeId,
838
+ touchScale: hasTouchScale ? settings.touchScale : selection.ui.touchScale,
758
839
  chatViewMode: hasChatViewMode ? settings.chatViewMode : selection.ui.chatViewMode,
759
840
  layout: nextLayout,
760
841
  },
@@ -1105,8 +1186,13 @@ export function appReducer(state, action) {
1105
1186
  const hadRow = Boolean(state.sessions.byId[goneId]);
1106
1187
  const wasActive = state.sessions.activeSessionId === goneId;
1107
1188
  if (!hadRow && !wasActive) return state;
1108
- const byId = { ...state.sessions.byId };
1109
- delete byId[goneId];
1189
+ const evictedById = { ...state.sessions.byId };
1190
+ delete evictedById[goneId];
1191
+ // Re-derive: the evicted row may have been the running CHILD that
1192
+ // put an ancestor into awaiting_children, and that ancestor is not
1193
+ // named in this action. Without it the parent reads "waiting on 1"
1194
+ // with nothing left to wait for until the next poll.
1195
+ const byId = applyActiveDescendantCounts(evictedById);
1110
1196
  const selectedIds = Array.isArray(state.sessions.selectedIds)
1111
1197
  ? state.sessions.selectedIds.filter((id) => id !== goneId)
1112
1198
  : state.sessions.selectedIds;
@@ -1134,13 +1220,7 @@ export function appReducer(state, action) {
1134
1220
  }
1135
1221
  for (const session of action.sessions) {
1136
1222
  const previous = state.sessions.byId[session.sessionId];
1137
- const merged = mergeSessionRowVisualStatus(
1138
- previous,
1139
- mergeDefinedSessionFields(previous, session),
1140
- nowMs,
1141
- );
1142
- byId[session.sessionId] = merged;
1143
- if (!anyChanged && merged !== previous) anyChanged = true;
1223
+ byId[session.sessionId] = mergeDefinedSessionFields(previous, session);
1144
1224
  }
1145
1225
  if (
1146
1226
  state.sessions.activeSessionId
@@ -1153,9 +1233,17 @@ export function appReducer(state, action) {
1153
1233
  };
1154
1234
  anyChanged = true;
1155
1235
  }
1236
+ // Descendant counts BEFORE row visual status: awaiting_children is
1237
+ // derived from activeChildCount, so stamping it after the debounce
1238
+ // ran would leave every parent one poll behind its own children.
1239
+ const countedById = applyActiveDescendantCounts(byId);
1240
+ const rowStatusById = applyRowVisualStatuses(state.sessions.byId, countedById, nowMs);
1241
+ for (const [sessionId, session] of Object.entries(rowStatusById)) {
1242
+ if (session !== state.sessions.byId[sessionId]) { anyChanged = true; break; }
1243
+ }
1156
1244
  // Check if session set changed (added/removed)
1157
1245
  const prevIds = Object.keys(state.sessions.byId);
1158
- const nextIds = Object.keys(byId);
1246
+ const nextIds = Object.keys(rowStatusById);
1159
1247
  if (prevIds.length !== nextIds.length) anyChanged = true;
1160
1248
  if (!anyChanged) {
1161
1249
  for (const id of nextIds) {
@@ -1163,7 +1251,7 @@ export function appReducer(state, action) {
1163
1251
  }
1164
1252
  }
1165
1253
  if (!anyChanged) return state;
1166
- const mergedSessions = Object.values(byId);
1254
+ const mergedSessions = Object.values(rowStatusById);
1167
1255
  const hasExistingOrder = Object.keys(state.sessions.orderById || {}).length > 0;
1168
1256
  const initialCollapsedIds = collectDefaultCollapsedSessionIds(mergedSessions);
1169
1257
  const previousCollapsedIdsExplicit = Boolean(state.sessions.collapsedIdsExplicit);
@@ -1205,12 +1293,12 @@ export function appReducer(state, action) {
1205
1293
  // Drop pins/selection for sessions that no longer exist; only
1206
1294
  // keep pins on top-level rows (groups and ungrouped top-level
1207
1295
  // sessions). Sessions inside containers lose their pins.
1208
- const survivingPins = prunePinnedIds(state.sessions.pinnedIds, byId);
1209
- const survivingSelected = pruneIdList(state.sessions.selectedIds, byId);
1296
+ const survivingPins = prunePinnedIds(state.sessions.pinnedIds, rowStatusById);
1297
+ const survivingSelected = pruneIdList(state.sessions.selectedIds, rowStatusById);
1210
1298
  const flat = buildSessionTree(mergedSessions, collapsedIds, orderById, survivingPins, state.sessions.manualOrder);
1211
1299
  const nextSessions = {
1212
1300
  ...state.sessions,
1213
- byId,
1301
+ byId: rowStatusById,
1214
1302
  collapsedIds,
1215
1303
  collapsedIdsExplicit: previousCollapsedIdsExplicit,
1216
1304
  listingSeen: true,
@@ -1314,16 +1402,25 @@ export function appReducer(state, action) {
1314
1402
  case "sessions/merged": {
1315
1403
  if (!action.session?.sessionId) return state;
1316
1404
  const previousSession = state.sessions.byId[action.session.sessionId];
1317
- const mergedSession = mergeSessionRowVisualStatus(
1318
- previousSession,
1319
- mergeDefinedSessionFields(previousSession, action.session),
1405
+ const mergedSession = mergeDefinedSessionFields(previousSession, action.session);
1406
+ // A single session going running/idle changes its ANCESTORS' counts
1407
+ // and therefore their visual status, so both passes run over the
1408
+ // whole map rather than the one row. buildSessionTree below is
1409
+ // already an O(n) pass over the same map, so this adds no new
1410
+ // complexity class.
1411
+ const byId = applyRowVisualStatuses(
1412
+ state.sessions.byId,
1413
+ applyActiveDescendantCounts({
1414
+ ...state.sessions.byId,
1415
+ [action.session.sessionId]: mergedSession,
1416
+ }),
1320
1417
  Date.now(),
1321
1418
  );
1322
- if (mergedSession === previousSession) return state;
1323
- const byId = {
1324
- ...state.sessions.byId,
1325
- [action.session.sessionId]: mergedSession,
1326
- };
1419
+ let anyRowChanged = false;
1420
+ for (const [sessionId, session] of Object.entries(byId)) {
1421
+ if (session !== state.sessions.byId[sessionId]) { anyRowChanged = true; break; }
1422
+ }
1423
+ if (!anyRowChanged) return state;
1327
1424
  const survivingPins = prunePinnedIds(state.sessions.pinnedIds, byId);
1328
1425
  const {
1329
1426
  orderById,