@flamingo-stack/openframe-frontend-core 0.0.444 → 0.0.445

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 (123) hide show
  1. package/dist/{chunk-DTMJWBDQ.js → chunk-22NSWMI6.js} +3 -3
  2. package/dist/{chunk-NDWIFCLS.js → chunk-3BHV2QAH.js} +555 -135
  3. package/dist/{chunk-NDWIFCLS.js.map → chunk-3BHV2QAH.js.map} +1 -1
  4. package/dist/{chunk-PMTR5W5G.js → chunk-3DQ5BFPM.js} +5 -5
  5. package/dist/{chunk-RALJEQWM.cjs → chunk-3HYSD7NR.cjs} +15 -15
  6. package/dist/{chunk-RALJEQWM.cjs.map → chunk-3HYSD7NR.cjs.map} +1 -1
  7. package/dist/{chunk-YP7VJPCJ.cjs → chunk-5DWPXK37.cjs} +40 -40
  8. package/dist/{chunk-YP7VJPCJ.cjs.map → chunk-5DWPXK37.cjs.map} +1 -1
  9. package/dist/{chunk-OXZLBIR7.cjs → chunk-5FUQTSTT.cjs} +18 -18
  10. package/dist/{chunk-OXZLBIR7.cjs.map → chunk-5FUQTSTT.cjs.map} +1 -1
  11. package/dist/{chunk-MG4AMIFX.js → chunk-5NYXGWBN.js} +2 -2
  12. package/dist/{chunk-LCJGOV6P.js → chunk-7R256DKK.js} +2 -2
  13. package/dist/{chunk-2PLNDTRG.cjs → chunk-DTLNCOSG.cjs} +32 -32
  14. package/dist/{chunk-2PLNDTRG.cjs.map → chunk-DTLNCOSG.cjs.map} +1 -1
  15. package/dist/{chunk-3SCNEGL2.js → chunk-FIAXFNAA.js} +5 -5
  16. package/dist/{chunk-FQASXKBK.js → chunk-G2KXNMFQ.js} +2 -2
  17. package/dist/{chunk-H7H3AFLF.js → chunk-H2DYCT6M.js} +2 -2
  18. package/dist/{chunk-CE3AITV2.js → chunk-H352AZJN.js} +6 -6
  19. package/dist/{chunk-DCDB3SM4.js → chunk-K4RASNQM.js} +3 -3
  20. package/dist/{chunk-WXU6A6CA.cjs → chunk-KISQUH33.cjs} +73 -73
  21. package/dist/{chunk-WXU6A6CA.cjs.map → chunk-KISQUH33.cjs.map} +1 -1
  22. package/dist/{chunk-BYTJNRJW.cjs → chunk-MPSA6YAM.cjs} +40 -40
  23. package/dist/{chunk-BYTJNRJW.cjs.map → chunk-MPSA6YAM.cjs.map} +1 -1
  24. package/dist/{chunk-ABIRMYG5.js → chunk-MQUS6BFR.js} +6 -6
  25. package/dist/{chunk-DSW6FIEF.cjs → chunk-N64VTHZ5.cjs} +7 -7
  26. package/dist/{chunk-DSW6FIEF.cjs.map → chunk-N64VTHZ5.cjs.map} +1 -1
  27. package/dist/{chunk-F6NMMPDL.cjs → chunk-P3FN4NAE.cjs} +29 -29
  28. package/dist/{chunk-F6NMMPDL.cjs.map → chunk-P3FN4NAE.cjs.map} +1 -1
  29. package/dist/{chunk-PHWQLKVE.js → chunk-P47NN7RY.js} +29 -31
  30. package/dist/chunk-P47NN7RY.js.map +1 -0
  31. package/dist/{chunk-2CJ734N2.cjs → chunk-PTT4J5GF.cjs} +3 -3
  32. package/dist/{chunk-2CJ734N2.cjs.map → chunk-PTT4J5GF.cjs.map} +1 -1
  33. package/dist/{chunk-4R3SVOKH.cjs → chunk-QL2EI56X.cjs} +797 -377
  34. package/dist/chunk-QL2EI56X.cjs.map +1 -0
  35. package/dist/{chunk-VI2QQIGR.cjs → chunk-R6WGCJBB.cjs} +73 -73
  36. package/dist/{chunk-VI2QQIGR.cjs.map → chunk-R6WGCJBB.cjs.map} +1 -1
  37. package/dist/{chunk-Q6XOEVPH.cjs → chunk-RXLMN6B5.cjs} +12 -12
  38. package/dist/{chunk-Q6XOEVPH.cjs.map → chunk-RXLMN6B5.cjs.map} +1 -1
  39. package/dist/{chunk-F2XHBAHT.js → chunk-TCEBFMLQ.js} +2 -2
  40. package/dist/{chunk-7CZ4MJSI.cjs → chunk-VIJARDBM.cjs} +3 -3
  41. package/dist/{chunk-7CZ4MJSI.cjs.map → chunk-VIJARDBM.cjs.map} +1 -1
  42. package/dist/{chunk-PXXS27EE.cjs → chunk-W24U7ZUV.cjs} +40 -42
  43. package/dist/chunk-W24U7ZUV.cjs.map +1 -0
  44. package/dist/{chunk-37ZZQXHK.js → chunk-XPZLIONZ.js} +4 -4
  45. package/dist/{chunk-FO4CI4R5.cjs → chunk-YCLSV2H6.cjs} +5 -5
  46. package/dist/{chunk-FO4CI4R5.cjs.map → chunk-YCLSV2H6.cjs.map} +1 -1
  47. package/dist/{chunk-U7U2CZBZ.js → chunk-Z4N7XLFG.js} +3 -3
  48. package/dist/components/case-studies/index.cjs +10 -10
  49. package/dist/components/case-studies/index.js +4 -4
  50. package/dist/components/chat/hooks/use-chunk-catchup.d.ts.map +1 -1
  51. package/dist/components/chat/hooks/use-jetstream-dialog-subscription.d.ts.map +1 -1
  52. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +44 -1
  53. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  54. package/dist/components/chat/hooks/use-nats-dialog-subscription.d.ts.map +1 -1
  55. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
  56. package/dist/components/chat/index.cjs +10 -4
  57. package/dist/components/chat/index.cjs.map +1 -1
  58. package/dist/components/chat/index.js +9 -3
  59. package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
  60. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  61. package/dist/components/contact/index.cjs +5 -5
  62. package/dist/components/contact/index.js +4 -4
  63. package/dist/components/docs/index.cjs +7 -7
  64. package/dist/components/docs/index.js +6 -6
  65. package/dist/components/embeds/index.cjs +5 -5
  66. package/dist/components/embeds/index.js +4 -4
  67. package/dist/components/faq/index.cjs +6 -6
  68. package/dist/components/faq/index.js +5 -5
  69. package/dist/components/features/index.cjs +4 -4
  70. package/dist/components/features/index.js +3 -3
  71. package/dist/components/help-center-pages/index.cjs +23 -23
  72. package/dist/components/help-center-pages/index.js +14 -14
  73. package/dist/components/index.cjs +142 -136
  74. package/dist/components/index.cjs.map +1 -1
  75. package/dist/components/index.js +18 -12
  76. package/dist/components/index.js.map +1 -1
  77. package/dist/components/navigation/index.cjs +4 -4
  78. package/dist/components/navigation/index.js +3 -3
  79. package/dist/components/onboarding-guides/index.cjs +8 -8
  80. package/dist/components/onboarding-guides/index.js +7 -7
  81. package/dist/components/related-content/index.cjs +6 -6
  82. package/dist/components/related-content/index.js +5 -5
  83. package/dist/components/tickets/index.cjs +7 -7
  84. package/dist/components/tickets/index.js +6 -6
  85. package/dist/components/ui/index.cjs +10 -4
  86. package/dist/components/ui/index.cjs.map +1 -1
  87. package/dist/components/ui/index.js +9 -3
  88. package/dist/hooks/index.cjs +3 -3
  89. package/dist/hooks/index.js +2 -2
  90. package/dist/index.cjs +10 -4
  91. package/dist/index.cjs.map +1 -1
  92. package/dist/index.js +9 -3
  93. package/dist/nats/index.cjs +2 -2
  94. package/dist/nats/index.js +1 -1
  95. package/dist/nats/shared-connection.d.ts +6 -0
  96. package/dist/nats/shared-connection.d.ts.map +1 -1
  97. package/package.json +1 -1
  98. package/src/components/chat/hooks/__tests__/nats-adapter-message-updaters.test.ts +270 -0
  99. package/src/components/chat/hooks/use-chunk-catchup.ts +212 -57
  100. package/src/components/chat/hooks/use-jetstream-dialog-subscription.ts +12 -8
  101. package/src/components/chat/hooks/use-nats-chat-adapter.ts +574 -9
  102. package/src/components/chat/hooks/use-nats-dialog-subscription.ts +12 -6
  103. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +40 -3
  104. package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +62 -0
  105. package/src/components/chat/utils/message-segment-accumulator.ts +6 -0
  106. package/src/components/chat/utils/process-historical-messages.ts +25 -3
  107. package/src/nats/shared-connection.ts +42 -29
  108. package/dist/chunk-4R3SVOKH.cjs.map +0 -1
  109. package/dist/chunk-PHWQLKVE.js.map +0 -1
  110. package/dist/chunk-PXXS27EE.cjs.map +0 -1
  111. /package/dist/{chunk-DTMJWBDQ.js.map → chunk-22NSWMI6.js.map} +0 -0
  112. /package/dist/{chunk-PMTR5W5G.js.map → chunk-3DQ5BFPM.js.map} +0 -0
  113. /package/dist/{chunk-MG4AMIFX.js.map → chunk-5NYXGWBN.js.map} +0 -0
  114. /package/dist/{chunk-LCJGOV6P.js.map → chunk-7R256DKK.js.map} +0 -0
  115. /package/dist/{chunk-3SCNEGL2.js.map → chunk-FIAXFNAA.js.map} +0 -0
  116. /package/dist/{chunk-FQASXKBK.js.map → chunk-G2KXNMFQ.js.map} +0 -0
  117. /package/dist/{chunk-H7H3AFLF.js.map → chunk-H2DYCT6M.js.map} +0 -0
  118. /package/dist/{chunk-CE3AITV2.js.map → chunk-H352AZJN.js.map} +0 -0
  119. /package/dist/{chunk-DCDB3SM4.js.map → chunk-K4RASNQM.js.map} +0 -0
  120. /package/dist/{chunk-ABIRMYG5.js.map → chunk-MQUS6BFR.js.map} +0 -0
  121. /package/dist/{chunk-F2XHBAHT.js.map → chunk-TCEBFMLQ.js.map} +0 -0
  122. /package/dist/{chunk-37ZZQXHK.js.map → chunk-XPZLIONZ.js.map} +0 -0
  123. /package/dist/{chunk-U7U2CZBZ.js.map → chunk-Z4N7XLFG.js.map} +0 -0
@@ -27,7 +27,7 @@ import {
27
27
  useOgPlaceholderUrl,
28
28
  useOnboardingState,
29
29
  useToast
30
- } from "./chunk-MG4AMIFX.js";
30
+ } from "./chunk-5NYXGWBN.js";
31
31
  import {
32
32
  AlertCircleIcon,
33
33
  AnthropicLogoGreyIcon,
@@ -249,7 +249,7 @@ import {
249
249
  acquireClient,
250
250
  releaseClient,
251
251
  startConnectionLifecycle
252
- } from "./chunk-PHWQLKVE.js";
252
+ } from "./chunk-P47NN7RY.js";
253
253
  import {
254
254
  __esm
255
255
  } from "./chunk-GGWZFCYS.js";
@@ -46956,8 +46956,10 @@ function useChunkCatchup({
46956
46956
  }) {
46957
46957
  const processedSequenceKeys = useRef64(/* @__PURE__ */ new Set());
46958
46958
  const lastSequenceId = useRef64(null);
46959
+ const lastSequenceIdByType = useRef64(/* @__PURE__ */ new Map());
46959
46960
  const fetchingInProgress = useRef64(false);
46960
46961
  const lastFetchParams = useRef64(null);
46962
+ const pendingCatchupRef = useRef64(null);
46961
46963
  const chunkBuffer = useRef64([]);
46962
46964
  const bufferUntilInitialCatchupComplete = useRef64(false);
46963
46965
  const hasCompletedInitialCatchup = useRef64(false);
@@ -46976,8 +46978,14 @@ function useChunkCatchup({
46976
46978
  }
46977
46979
  if (chunk.sequenceId !== void 0 && chunk.sequenceId !== null) {
46978
46980
  const chunkType = typeof chunk.type === "string" ? chunk.type : "";
46979
- processedSequenceKeys.current.add(makeSeqKey(messageType, chunkType, chunk.sequenceId));
46981
+ const key = makeSeqKey(messageType, chunkType, chunk.sequenceId);
46982
+ if (processedSequenceKeys.current.has(key)) return true;
46983
+ processedSequenceKeys.current.add(key);
46980
46984
  lastSequenceId.current = chunk.sequenceId;
46985
+ const prevTypeSeq = lastSequenceIdByType.current.get(messageType);
46986
+ if (prevTypeSeq === void 0 || chunk.sequenceId > prevTypeSeq) {
46987
+ lastSequenceIdByType.current.set(messageType, chunk.sequenceId);
46988
+ }
46981
46989
  }
46982
46990
  onChunkReceivedRef.current(chunk, messageType);
46983
46991
  return true;
@@ -46987,8 +46995,8 @@ function useChunkCatchup({
46987
46995
  const buffered = [...chunkBuffer.current];
46988
46996
  chunkBuffer.current = [];
46989
46997
  buffered.sort((a, b) => {
46990
- const seqA = a.chunk.sequenceId ?? 0;
46991
- const seqB = b.chunk.sequenceId ?? 0;
46998
+ const seqA = a.chunk.sequenceId ?? Number.MAX_SAFE_INTEGER;
46999
+ const seqB = b.chunk.sequenceId ?? Number.MAX_SAFE_INTEGER;
46992
47000
  return seqA - seqB;
46993
47001
  });
46994
47002
  buffered.forEach(({ chunk, messageType }) => {
@@ -47003,9 +47011,14 @@ function useChunkCatchup({
47003
47011
  return;
47004
47012
  }
47005
47013
  if (hasCompletedInitialCatchup.current) {
47014
+ if (bufferUntilInitialCatchupComplete.current) {
47015
+ bufferUntilInitialCatchupComplete.current = false;
47016
+ flushBufferedRealtimeChunks();
47017
+ }
47006
47018
  return;
47007
47019
  }
47008
47020
  if (fetchingInProgress.current) {
47021
+ pendingCatchupRef.current = { dialogId: dialogId2, fromSequenceId: fromSequenceId ?? lastSequenceId.current };
47009
47022
  return;
47010
47023
  }
47011
47024
  if (lastFetchParams.current && lastFetchParams.current.dialogId === dialogId2 && lastFetchParams.current.fromSequenceId === fromSequenceId) {
@@ -47022,7 +47035,8 @@ function useChunkCatchup({
47022
47035
  try {
47023
47036
  const chunkPromises = chatTypes2.map(async (chatType) => {
47024
47037
  try {
47025
- const chunks = await fetchChunks2(dialogId2, chatType, fromSequenceId);
47038
+ const typeFromSequenceId = fromSequenceId === void 0 ? void 0 : lastSequenceIdByType.current.get(getChatTypeMessageType(chatType)) ?? null;
47039
+ const chunks = await fetchChunks2(dialogId2, chatType, typeFromSequenceId);
47026
47040
  const messageType = getChatTypeMessageType(chatType);
47027
47041
  return chunks.map((chunk) => ({ chunk, messageType }));
47028
47042
  } catch (error) {
@@ -47042,49 +47056,66 @@ function useChunkCatchup({
47042
47056
  chunkBuffer.current = [];
47043
47057
  const allChunks = [...allCatchupChunks, ...bufferedNatsChunks];
47044
47058
  allChunks.sort((a, b) => {
47045
- const seqA = a.chunk.sequenceId ?? 0;
47046
- const seqB = b.chunk.sequenceId ?? 0;
47059
+ const seqA = a.chunk.sequenceId ?? Number.MAX_SAFE_INTEGER;
47060
+ const seqB = b.chunk.sequenceId ?? Number.MAX_SAFE_INTEGER;
47047
47061
  return seqA - seqB;
47048
47062
  });
47049
47063
  const uniqueAllChunks = [];
47050
47064
  const seenInBatch = /* @__PURE__ */ new Set();
47051
47065
  for (const item of allChunks) {
47052
- const k = makeBatchDedupKey(item);
47053
- if (seenInBatch.has(k)) continue;
47054
- seenInBatch.add(k);
47055
- uniqueAllChunks.push(item);
47056
- }
47057
- let lastMessageStartSeqId = null;
47058
- let lastMessageEndSeqId = null;
47059
- for (let i = uniqueAllChunks.length - 1; i >= 0; i--) {
47060
- const seq = uniqueAllChunks[i].chunk.sequenceId;
47061
- if (uniqueAllChunks[i].chunk.type === MESSAGE_TYPE.MESSAGE_END && seq !== void 0 && seq !== null) {
47062
- lastMessageEndSeqId = seq;
47063
- break;
47066
+ const hasSeq = item.chunk.sequenceId !== void 0 && item.chunk.sequenceId !== null;
47067
+ if (hasSeq) {
47068
+ const k = makeBatchDedupKey(item);
47069
+ if (seenInBatch.has(k)) continue;
47070
+ seenInBatch.add(k);
47064
47071
  }
47072
+ uniqueAllChunks.push(item);
47065
47073
  }
47066
- for (let i = uniqueAllChunks.length - 1; i >= 0; i--) {
47067
- const chunk = uniqueAllChunks[i].chunk;
47068
- const seq = chunk.sequenceId;
47069
- if (chunk.type === MESSAGE_TYPE.MESSAGE_START && seq !== void 0 && seq !== null) {
47070
- if (lastMessageEndSeqId === null || seq > lastMessageEndSeqId) {
47071
- lastMessageStartSeqId = seq;
47074
+ const hasNoSeq = (item) => item.chunk.sequenceId === void 0 || item.chunk.sequenceId === null;
47075
+ const byType = /* @__PURE__ */ new Map();
47076
+ for (const item of uniqueAllChunks) {
47077
+ const list = byType.get(item.messageType);
47078
+ if (list) list.push(item);
47079
+ else byType.set(item.messageType, [item]);
47080
+ }
47081
+ const chunksToProcess = [];
47082
+ for (const items of byType.values()) {
47083
+ let lastMessageStartSeqId = null;
47084
+ let lastMessageEndSeqId = null;
47085
+ for (let i = items.length - 1; i >= 0; i--) {
47086
+ const seq = items[i].chunk.sequenceId;
47087
+ if (items[i].chunk.type === MESSAGE_TYPE.MESSAGE_END && seq !== void 0 && seq !== null) {
47088
+ lastMessageEndSeqId = seq;
47072
47089
  break;
47073
47090
  }
47074
47091
  }
47092
+ for (let i = items.length - 1; i >= 0; i--) {
47093
+ const chunk = items[i].chunk;
47094
+ const seq = chunk.sequenceId;
47095
+ if (chunk.type === MESSAGE_TYPE.MESSAGE_START && seq !== void 0 && seq !== null) {
47096
+ if (lastMessageEndSeqId === null || seq > lastMessageEndSeqId) {
47097
+ lastMessageStartSeqId = seq;
47098
+ break;
47099
+ }
47100
+ }
47101
+ }
47102
+ if (lastMessageStartSeqId !== null) {
47103
+ chunksToProcess.push(
47104
+ ...items.filter((item) => hasNoSeq(item) || item.chunk.sequenceId >= lastMessageStartSeqId)
47105
+ );
47106
+ } else if (lastMessageEndSeqId !== null) {
47107
+ chunksToProcess.push(
47108
+ ...items.filter((item) => hasNoSeq(item) || item.chunk.sequenceId > lastMessageEndSeqId)
47109
+ );
47110
+ } else {
47111
+ chunksToProcess.push(...items);
47112
+ }
47075
47113
  }
47076
- let chunksToProcess;
47077
- if (lastMessageStartSeqId !== null) {
47078
- chunksToProcess = uniqueAllChunks.filter(
47079
- (item) => item.chunk.sequenceId !== void 0 && item.chunk.sequenceId >= lastMessageStartSeqId
47080
- );
47081
- } else if (lastMessageEndSeqId !== null) {
47082
- chunksToProcess = uniqueAllChunks.filter(
47083
- (item) => item.chunk.sequenceId !== void 0 && item.chunk.sequenceId > lastMessageEndSeqId
47084
- );
47085
- } else {
47086
- chunksToProcess = uniqueAllChunks;
47087
- }
47114
+ chunksToProcess.sort((a, b) => {
47115
+ const seqA = a.chunk.sequenceId ?? Number.MAX_SAFE_INTEGER;
47116
+ const seqB = b.chunk.sequenceId ?? Number.MAX_SAFE_INTEGER;
47117
+ return seqA - seqB;
47118
+ });
47088
47119
  chunksToProcess.forEach(({ chunk, messageType }) => {
47089
47120
  if (chunk.sequenceId !== void 0 && chunk.sequenceId !== null) {
47090
47121
  const chunkType = typeof chunk.type === "string" ? chunk.type : "";
@@ -47092,6 +47123,10 @@ function useChunkCatchup({
47092
47123
  if (processedSequenceKeys.current.has(key)) return;
47093
47124
  processedSequenceKeys.current.add(key);
47094
47125
  lastSequenceId.current = chunk.sequenceId;
47126
+ const prevTypeSeq = lastSequenceIdByType.current.get(messageType);
47127
+ if (prevTypeSeq === void 0 || chunk.sequenceId > prevTypeSeq) {
47128
+ lastSequenceIdByType.current.set(messageType, chunk.sequenceId);
47129
+ }
47095
47130
  }
47096
47131
  onChunkReceivedRef.current(chunk, messageType);
47097
47132
  });
@@ -47100,26 +47135,55 @@ function useChunkCatchup({
47100
47135
  } catch (error) {
47101
47136
  console.error("Error during chunk catchup:", error);
47102
47137
  } finally {
47103
- fetchingInProgress.current = false;
47104
- if (bufferUntilInitialCatchupComplete.current) {
47105
- bufferUntilInitialCatchupComplete.current = false;
47106
- hasCompletedInitialCatchup.current = true;
47107
- flushBufferedRealtimeChunks();
47138
+ if (dialogId2 !== dialogIdRef.current) {
47139
+ if (pendingCatchupRef.current?.dialogId === dialogId2) {
47140
+ pendingCatchupRef.current = null;
47141
+ }
47142
+ } else {
47143
+ fetchingInProgress.current = false;
47144
+ const pending = pendingCatchupRef.current;
47145
+ if (pending) {
47146
+ pendingCatchupRef.current = null;
47147
+ if (pending.dialogId === dialogId2) {
47148
+ hasCompletedInitialCatchup.current = false;
47149
+ lastFetchParams.current = null;
47150
+ bufferUntilInitialCatchupComplete.current = true;
47151
+ try {
47152
+ await catchUpChunksRef.current?.(pending.fromSequenceId);
47153
+ } catch (rerunError) {
47154
+ console.error("Queued catch-up re-run failed:", rerunError);
47155
+ }
47156
+ } else if (bufferUntilInitialCatchupComplete.current) {
47157
+ bufferUntilInitialCatchupComplete.current = false;
47158
+ hasCompletedInitialCatchup.current = true;
47159
+ flushBufferedRealtimeChunks();
47160
+ }
47161
+ } else if (bufferUntilInitialCatchupComplete.current) {
47162
+ bufferUntilInitialCatchupComplete.current = false;
47163
+ hasCompletedInitialCatchup.current = true;
47164
+ flushBufferedRealtimeChunks();
47165
+ }
47108
47166
  }
47109
47167
  }
47110
47168
  }, [flushBufferedRealtimeChunks]);
47169
+ const catchUpChunksRef = useRef64(null);
47170
+ catchUpChunksRef.current = catchUpChunks;
47111
47171
  const resetChunkTracking = useCallback46(() => {
47112
47172
  processedSequenceKeys.current.clear();
47113
47173
  lastSequenceId.current = null;
47174
+ lastSequenceIdByType.current.clear();
47114
47175
  fetchingInProgress.current = false;
47115
47176
  lastFetchParams.current = null;
47177
+ pendingCatchupRef.current = null;
47116
47178
  chunkBuffer.current = [];
47117
47179
  bufferUntilInitialCatchupComplete.current = false;
47118
47180
  hasCompletedInitialCatchup.current = false;
47119
47181
  }, []);
47120
47182
  const startInitialBuffering = useCallback46(() => {
47121
- chunkBuffer.current = [];
47122
- bufferUntilInitialCatchupComplete.current = true;
47183
+ if (!bufferUntilInitialCatchupComplete.current) {
47184
+ chunkBuffer.current = [];
47185
+ bufferUntilInitialCatchupComplete.current = true;
47186
+ }
47123
47187
  hasCompletedInitialCatchup.current = false;
47124
47188
  }, []);
47125
47189
  const isBufferingActive = useCallback46(() => bufferUntilInitialCatchupComplete.current, []);
@@ -47128,8 +47192,10 @@ function useChunkCatchup({
47128
47192
  const fromSeq = lastSequenceId.current;
47129
47193
  hasCompletedInitialCatchup.current = false;
47130
47194
  lastFetchParams.current = null;
47131
- bufferUntilInitialCatchupComplete.current = true;
47132
- chunkBuffer.current = [];
47195
+ if (!bufferUntilInitialCatchupComplete.current) {
47196
+ chunkBuffer.current = [];
47197
+ bufferUntilInitialCatchupComplete.current = true;
47198
+ }
47133
47199
  await catchUpChunks(fromSeq);
47134
47200
  }, [catchUpChunks]);
47135
47201
  return {
@@ -47329,6 +47395,10 @@ function useJetStreamDialogSubscription({
47329
47395
  }
47330
47396
  };
47331
47397
  }, [enabled, wsUrl]);
47398
+ useEffect79(() => {
47399
+ highestStreamSeqRef.current = null;
47400
+ setCurrentStreamSeq(null);
47401
+ }, [dialogId]);
47332
47402
  useEffect79(() => {
47333
47403
  if (!enabled || !dialogId || !isConnected) {
47334
47404
  if (subscriptionRef.current) {
@@ -47412,10 +47482,6 @@ function useJetStreamDialogSubscription({
47412
47482
  setIsSubscribed(false);
47413
47483
  };
47414
47484
  }, [enabled, dialogId, isConnected, streamName, topic, reconnectionCount]);
47415
- useEffect79(() => {
47416
- highestStreamSeqRef.current = null;
47417
- setCurrentStreamSeq(null);
47418
- }, [dialogId]);
47419
47485
  return { isConnected, isSubscribed, reconnectionCount, currentStreamSeq };
47420
47486
  }
47421
47487
 
@@ -47556,6 +47622,8 @@ function useNatsDialogSubscription({
47556
47622
  };
47557
47623
  }, [enabled, wsUrl]);
47558
47624
  const topicsKey = topics.join(",");
47625
+ const topicsRef = useRef66(topics);
47626
+ topicsRef.current = topics;
47559
47627
  const lastSubscribedDialogIdRef = useRef66(null);
47560
47628
  const isConnectedRef = useRef66(isConnected);
47561
47629
  useEffect80(() => {
@@ -47615,7 +47683,7 @@ function useNatsDialogSubscription({
47615
47683
  } catch {
47616
47684
  }
47617
47685
  };
47618
- topics.forEach((topic) => {
47686
+ topicsRef.current.forEach((topic) => {
47619
47687
  const subscription = client.subscribeBytes(
47620
47688
  `chat.${dialogId}.${topic}`,
47621
47689
  handleMessage(topic),
@@ -47643,7 +47711,7 @@ function useNatsDialogSubscription({
47643
47711
  lastSubscribedDialogIdRef.current = null;
47644
47712
  abortControllerRef.current = null;
47645
47713
  };
47646
- }, [enabled, dialogId, topicsKey, topics]);
47714
+ }, [enabled, dialogId, topicsKey]);
47647
47715
  useEffect80(() => {
47648
47716
  if (!enabled || !currentDialogIdRef.current || !isConnected) {
47649
47717
  return;
@@ -47666,7 +47734,7 @@ function useNatsDialogSubscription({
47666
47734
  } catch {
47667
47735
  }
47668
47736
  };
47669
- topics.forEach((topic) => {
47737
+ topicsRef.current.forEach((topic) => {
47670
47738
  const subscription = client.subscribeBytes(
47671
47739
  `chat.${dialogId2}.${topic}`,
47672
47740
  handleMessage(topic),
@@ -47680,7 +47748,7 @@ function useNatsDialogSubscription({
47680
47748
  } else if (subscriptionRefs.current.size > 0) {
47681
47749
  setIsSubscribed(true);
47682
47750
  }
47683
- }, [isConnected, enabled, topics, topicsKey]);
47751
+ }, [isConnected, enabled, topicsKey]);
47684
47752
  return { isConnected, isSubscribed, reconnectionCount };
47685
47753
  }
47686
47754
  function buildNatsWsUrl(apiBaseUrl, options) {
@@ -48041,6 +48109,10 @@ var MessageSegmentAccumulator = class {
48041
48109
  const hasCall = seg.data.toolCalls.some((c) => c.toolExecutionRequestId === execId);
48042
48110
  if (!hasCall) return seg;
48043
48111
  const prev = seg.data.executions?.[execId];
48112
+ if (toolData.type === "EXECUTING_TOOL" && prev?.status === "done") {
48113
+ matched = true;
48114
+ return seg;
48115
+ }
48044
48116
  const next = toolData.type === "EXECUTED_TOOL" ? { status: "done", result: toolData.result, success: toolData.success } : { status: "executing", result: prev?.result, success: prev?.success };
48045
48117
  matched = true;
48046
48118
  return {
@@ -48504,6 +48576,16 @@ function useRealtimeChunkProcessor(options) {
48504
48576
  explanation: escalatedData.explanation,
48505
48577
  approvalType: escalatedData.approvalType
48506
48578
  });
48579
+ const emitResolved = (segments) => {
48580
+ if (isInStreamRef.current) {
48581
+ emitSegments(segments);
48582
+ return;
48583
+ }
48584
+ const delta = segments.filter(
48585
+ (s) => s.type === "approval_request" && s.data.requestId === requestId || s.type === "approval_batch" && s.data.approvalRequestId === requestId
48586
+ );
48587
+ emitSegments(delta, { append: true });
48588
+ };
48507
48589
  if (escalatedData.toolCalls && escalatedData.toolCalls.length > 0) {
48508
48590
  if (batchApprovalsEnabled) {
48509
48591
  const segments = accumulator.addApprovalBatch(
@@ -48514,7 +48596,7 @@ function useRealtimeChunkProcessor(options) {
48514
48596
  void 0,
48515
48597
  resolvedByName
48516
48598
  );
48517
- emitSegments(segments);
48599
+ emitResolved(segments);
48518
48600
  } else {
48519
48601
  let segments = accumulator.getSegments();
48520
48602
  for (const call of escalatedData.toolCalls) {
@@ -48527,7 +48609,7 @@ function useRealtimeChunkProcessor(options) {
48527
48609
  status
48528
48610
  );
48529
48611
  }
48530
- emitSegments(segments);
48612
+ emitResolved(segments);
48531
48613
  }
48532
48614
  } else {
48533
48615
  const segments = accumulator.addApprovalRequest(
@@ -48537,7 +48619,7 @@ function useRealtimeChunkProcessor(options) {
48537
48619
  escalatedData.approvalType,
48538
48620
  status
48539
48621
  );
48540
- emitSegments(segments);
48622
+ emitResolved(segments);
48541
48623
  }
48542
48624
  } else {
48543
48625
  accumulator.updateApprovalStatus(requestId, status, resolvedByName);
@@ -48617,6 +48699,8 @@ function useRealtimeChunkProcessor(options) {
48617
48699
  hasInitializedWithData.current = false;
48618
48700
  sawDirectMessageRef.current = false;
48619
48701
  directTeardownFiredRef.current = false;
48702
+ isInStreamRef.current = false;
48703
+ hasEverStreamedRef.current = false;
48620
48704
  }, []);
48621
48705
  const updateApprovalStatus = useCallback47(
48622
48706
  (requestId, status, resolvedByName) => {
@@ -50147,7 +50231,15 @@ function processHistoricalMessages(messages, options = {}) {
50147
50231
  onReject,
50148
50232
  chatTypeFilter,
50149
50233
  approvalStatuses = {},
50150
- displayApprovalTypes
50234
+ // An omitted option means "display every approval type" — the original
50235
+ // history semantics. Deliberately NOT defaulted to the realtime
50236
+ // processor's ['CLIENT']: consumers that pass a wider list to their
50237
+ // realtime processor but omit it on the history path would silently lose
50238
+ // pending non-CLIENT approval cards on every reload/reconnect refetch
50239
+ // (they also ignore `escalatedApprovals`). Realtime/history parity is
50240
+ // opt-in: pass the same explicit list to both.
50241
+ displayApprovalTypes,
50242
+ batchApprovalsEnabled
50151
50243
  } = options;
50152
50244
  const processedMessages = [];
50153
50245
  const accumulator = createMessageSegmentAccumulator({ onApprove, onReject });
@@ -50212,7 +50304,13 @@ function processHistoricalMessages(messages, options = {}) {
50212
50304
  }
50213
50305
  lastAssistantId = msg.id;
50214
50306
  messageDataArray.forEach((data) => {
50215
- processMessageData(data, accumulator, approvalStatuses, { displayApprovalTypes }, escalatedApprovals);
50307
+ processMessageData(
50308
+ data,
50309
+ accumulator,
50310
+ approvalStatuses,
50311
+ { displayApprovalTypes, batchApprovalsEnabled },
50312
+ escalatedApprovals
50313
+ );
50216
50314
  });
50217
50315
  const nextMsg = messages[index + 1];
50218
50316
  const isLastMessage = index === messages.length - 1;
@@ -50436,7 +50534,7 @@ function extractErrorMessages(messages, options = {}) {
50436
50534
  return errorMessages;
50437
50535
  }
50438
50536
  function processHistoricalMessagesWithErrors(messages, options = {}) {
50439
- const { chatTypeFilter, assistantName = "Fae", assistantType = "fae", assistantAvatar, onApprove, onReject, approvalStatuses = {}, displayApprovalTypes } = options;
50537
+ const { chatTypeFilter, assistantName = "Fae", assistantType = "fae", assistantAvatar, onApprove, onReject, approvalStatuses = {}, displayApprovalTypes, batchApprovalsEnabled } = options;
50440
50538
  const processedMessages = [];
50441
50539
  const accumulator = createMessageSegmentAccumulator({ onApprove, onReject });
50442
50540
  const escalatedApprovals = /* @__PURE__ */ new Map();
@@ -50500,7 +50598,13 @@ function processHistoricalMessagesWithErrors(messages, options = {}) {
50500
50598
  }
50501
50599
  lastAssistantId = msg.id;
50502
50600
  messageDataArray.forEach((data) => {
50503
- processMessageData(data, accumulator, approvalStatuses, { displayApprovalTypes }, escalatedApprovals);
50601
+ processMessageData(
50602
+ data,
50603
+ accumulator,
50604
+ approvalStatuses,
50605
+ { displayApprovalTypes, batchApprovalsEnabled },
50606
+ escalatedApprovals
50607
+ );
50504
50608
  });
50505
50609
  const nextMsg = messages[index + 1];
50506
50610
  const isLastMessage = index === messages.length - 1;
@@ -50529,6 +50633,65 @@ function processHistoricalMessagesWithErrors(messages, options = {}) {
50529
50633
  };
50530
50634
  }
50531
50635
 
50636
+ // src/components/chat/utils/extract-incomplete-message-state.ts
50637
+ function extractIncompleteMessageState(lastMessage) {
50638
+ if (!lastMessage || lastMessage.role !== "assistant" || typeof lastMessage.content === "string") {
50639
+ return void 0;
50640
+ }
50641
+ const segments = lastMessage.content;
50642
+ const pendingApprovals = /* @__PURE__ */ new Map();
50643
+ const executingTools = /* @__PURE__ */ new Map();
50644
+ let hasIncompleteState = false;
50645
+ segments.forEach((segment) => {
50646
+ switch (segment.type) {
50647
+ case "tool_execution":
50648
+ if (segment.data.type === "EXECUTING_TOOL") {
50649
+ const toolKey = segment.data.toolExecutionRequestId || `${segment.data.integratedToolType}-${segment.data.toolFunction}`;
50650
+ executingTools.set(toolKey, {
50651
+ integratedToolType: segment.data.integratedToolType,
50652
+ toolFunction: segment.data.toolFunction,
50653
+ toolTitle: segment.data.toolTitle,
50654
+ parameters: segment.data.parameters
50655
+ });
50656
+ hasIncompleteState = true;
50657
+ }
50658
+ break;
50659
+ case "approval_request":
50660
+ if (segment.status === "pending" && segment.data.requestId) {
50661
+ pendingApprovals.set(segment.data.requestId, {
50662
+ command: segment.data.command,
50663
+ explanation: segment.data.explanation,
50664
+ approvalType: segment.data.approvalType || "CLIENT"
50665
+ });
50666
+ hasIncompleteState = true;
50667
+ }
50668
+ break;
50669
+ case "approval_batch": {
50670
+ const allDone = !!segment.data.executions && segment.data.toolCalls.every(
50671
+ (c) => segment.data.executions?.[c.toolExecutionRequestId]?.status === "done"
50672
+ );
50673
+ if (segment.status !== "rejected" && !allDone) {
50674
+ hasIncompleteState = true;
50675
+ }
50676
+ break;
50677
+ }
50678
+ case "context_compaction":
50679
+ if (segment.status === "started") {
50680
+ hasIncompleteState = true;
50681
+ }
50682
+ break;
50683
+ }
50684
+ });
50685
+ if (!hasIncompleteState) {
50686
+ return void 0;
50687
+ }
50688
+ return {
50689
+ existingSegments: segments,
50690
+ pendingApprovals: pendingApprovals.size > 0 ? pendingApprovals : void 0,
50691
+ executingTools: executingTools.size > 0 ? executingTools : void 0
50692
+ };
50693
+ }
50694
+
50532
50695
  // src/components/chat/hooks/use-nats-chat-adapter.ts
50533
50696
  function nextId(role) {
50534
50697
  return `${role}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
@@ -50551,6 +50714,110 @@ function updateTrailingAssistant(prev, segments) {
50551
50714
  { ...last, segments }
50552
50715
  ];
50553
50716
  }
50717
+ var CONTENT_DEDUP_WINDOW = 4;
50718
+ var SYSTEM_DEDUP_WINDOW = 10;
50719
+ function hasRecentMessage(prev, predicate, window2) {
50720
+ const start = Math.max(0, prev.length - window2);
50721
+ for (let i = prev.length - 1; i >= start; i--) {
50722
+ if (predicate(prev[i])) return true;
50723
+ }
50724
+ return false;
50725
+ }
50726
+ function appendToTrailingAssistant(prev, segments) {
50727
+ if (segments.length === 0) return prev;
50728
+ const last = prev[prev.length - 1];
50729
+ if (!last || last.role !== "assistant") {
50730
+ return [
50731
+ ...prev,
50732
+ { id: nextId("assistant"), role: "assistant", content: "", segments }
50733
+ ];
50734
+ }
50735
+ const merged = [...last.segments ?? []];
50736
+ for (const seg of segments) {
50737
+ const tail = merged[merged.length - 1];
50738
+ if (seg.type === "text" && tail?.type === "text") {
50739
+ merged[merged.length - 1] = { type: "text", text: tail.text + seg.text };
50740
+ } else if (seg.type === "thinking" && tail?.type === "thinking") {
50741
+ merged[merged.length - 1] = { type: "thinking", text: tail.text + seg.text };
50742
+ } else if (seg.type === "approval_batch") {
50743
+ const idx = merged.findIndex(
50744
+ (m) => m.type === "approval_batch" && m.data.approvalRequestId === seg.data.approvalRequestId
50745
+ );
50746
+ if (idx !== -1) merged[idx] = seg;
50747
+ else merged.push(seg);
50748
+ } else if (seg.type === "approval_request") {
50749
+ const idx = merged.findIndex(
50750
+ (m) => m.type === "approval_request" && m.data.requestId === seg.data.requestId && m.data.command === seg.data.command
50751
+ );
50752
+ if (idx !== -1) merged[idx] = seg;
50753
+ else merged.push(seg);
50754
+ } else {
50755
+ merged.push(seg);
50756
+ }
50757
+ }
50758
+ return [...prev.slice(0, -1), { ...last, segments: merged }];
50759
+ }
50760
+ function upsertTrailingCompaction(prev, segments) {
50761
+ const compaction = [...segments].reverse().find((s) => s.type === "context_compaction");
50762
+ if (!compaction) return prev;
50763
+ const last = prev[prev.length - 1];
50764
+ if (!last || last.role !== "assistant") {
50765
+ return [
50766
+ ...prev,
50767
+ { id: nextId("assistant"), role: "assistant", content: "", segments: [compaction] }
50768
+ ];
50769
+ }
50770
+ const existing = last.segments ?? [];
50771
+ const startedIdx = existing.map((s) => s.type === "context_compaction" && s.status === "started").lastIndexOf(true);
50772
+ const merged = startedIdx !== -1 ? existing.map((s, i) => i === startedIdx ? compaction : s) : [...existing, compaction];
50773
+ return [...prev.slice(0, -1), { ...last, segments: merged }];
50774
+ }
50775
+ function applyToolExecutionToMessages(prev, segment) {
50776
+ const toolData = segment.data;
50777
+ const execId = toolData.toolExecutionRequestId;
50778
+ for (let i = prev.length - 1; i >= 0; i--) {
50779
+ const message2 = prev[i];
50780
+ if (message2.role !== "assistant" || !message2.segments) continue;
50781
+ for (let j = message2.segments.length - 1; j >= 0; j--) {
50782
+ const seg = message2.segments[j];
50783
+ if (execId && seg.type === "approval_batch" && seg.data.toolCalls.some((c) => c.toolExecutionRequestId === execId)) {
50784
+ const prevExec = seg.data.executions?.[execId];
50785
+ if (toolData.type === "EXECUTING_TOOL" && prevExec?.status === "done") {
50786
+ return prev;
50787
+ }
50788
+ const nextExec = toolData.type === "EXECUTED_TOOL" ? { status: "done", result: toolData.result, success: toolData.success } : { status: "executing", result: prevExec?.result, success: prevExec?.success };
50789
+ const nextSegments = [...message2.segments];
50790
+ nextSegments[j] = {
50791
+ ...seg,
50792
+ data: { ...seg.data, executions: { ...seg.data.executions ?? {}, [execId]: nextExec } }
50793
+ };
50794
+ const next = [...prev];
50795
+ next[i] = { ...message2, segments: nextSegments };
50796
+ return next;
50797
+ }
50798
+ if (seg.type === "tool_execution") {
50799
+ const matches = execId ? seg.data.toolExecutionRequestId === execId : seg.data.type === "EXECUTING_TOOL" && seg.data.integratedToolType === toolData.integratedToolType && seg.data.toolFunction === toolData.toolFunction;
50800
+ if (!matches) continue;
50801
+ if (toolData.type === "EXECUTING_TOOL" && seg.data.type === "EXECUTED_TOOL") {
50802
+ return prev;
50803
+ }
50804
+ const nextSegments = [...message2.segments];
50805
+ nextSegments[j] = {
50806
+ type: "tool_execution",
50807
+ data: {
50808
+ ...toolData,
50809
+ toolTitle: toolData.toolTitle ?? seg.data.toolTitle,
50810
+ parameters: toolData.parameters || seg.data.parameters
50811
+ }
50812
+ };
50813
+ const next = [...prev];
50814
+ next[i] = { ...message2, segments: nextSegments };
50815
+ return next;
50816
+ }
50817
+ }
50818
+ }
50819
+ return appendToTrailingAssistant(prev, [segment]);
50820
+ }
50554
50821
  function mapProcessedToUnified(processed) {
50555
50822
  const out = [];
50556
50823
  for (const m of processed) {
@@ -50590,6 +50857,7 @@ function useNatsChatAdapter(config, options = {}) {
50590
50857
  fetchChunks,
50591
50858
  topics,
50592
50859
  batchApprovalsEnabled,
50860
+ displayApprovalTypes,
50593
50861
  fetchDialogs,
50594
50862
  fetchDialogMessages,
50595
50863
  createDialog: createDialogCallback,
@@ -50609,11 +50877,17 @@ function useNatsChatAdapter(config, options = {}) {
50609
50877
  const isManagedMode = fetchDialogs !== void 0;
50610
50878
  const [internalDialogId, setInternalDialogId] = useState116(null);
50611
50879
  const dialogId = isManagedMode ? internalDialogId : controlledDialogId !== void 0 ? controlledDialogId : null;
50880
+ const currentDialogIdRef = useRef74(dialogId);
50881
+ currentDialogIdRef.current = dialogId;
50612
50882
  const [messages, setMessages] = useState116([]);
50613
50883
  const [streamingPhase, setStreamingPhase] = useState116("idle");
50614
50884
  const streamingPhaseRef = useRef74(streamingPhase);
50615
50885
  streamingPhaseRef.current = streamingPhase;
50616
- const suppressAgentBusyRef = useRef74(false);
50886
+ const suppressAgentBusyRef = useRef74(0);
50887
+ const adoptTrailingAssistantRef = useRef74(false);
50888
+ const hasConnectedOnceRef = useRef74(false);
50889
+ const seenParticipantSeqsRef = useRef74(/* @__PURE__ */ new Set());
50890
+ const pendingEchoTextsRef = useRef74([]);
50617
50891
  const [approvalStatuses, setApprovalStatuses] = useState116({});
50618
50892
  const [dialogs, setDialogs] = useState116([]);
50619
50893
  const [dialogsNextCursor, setDialogsNextCursor] = useState116(null);
@@ -50679,17 +50953,39 @@ function useNatsChatAdapter(config, options = {}) {
50679
50953
  return handleRejectRef.current(requestId);
50680
50954
  }, []);
50681
50955
  const callbacksRef = useRef74({
50682
- onSegmentsUpdate: (segments) => {
50956
+ onSegmentsUpdate: (segments, meta) => {
50957
+ if (meta?.append && meta.isCompacting) {
50958
+ setMessages((prev) => upsertTrailingCompaction(prev, segments));
50959
+ return;
50960
+ }
50961
+ if (meta?.append) {
50962
+ setMessages((prev) => appendToTrailingAssistant(prev, segments));
50963
+ return;
50964
+ }
50683
50965
  setMessages((prev) => updateTrailingAssistant(prev, segments));
50684
50966
  },
50685
- onStreamStart: () => setStreamingPhase("streaming"),
50967
+ onStreamStart: () => {
50968
+ setStreamingPhase("streaming");
50969
+ const adoptTail = adoptTrailingAssistantRef.current;
50970
+ adoptTrailingAssistantRef.current = false;
50971
+ setMessages((prev) => {
50972
+ const last = prev[prev.length - 1];
50973
+ if (!last || last.role !== "assistant" || adoptTail) return prev;
50974
+ const hasContent = (last.segments?.length ?? 0) > 0 || last.content !== "";
50975
+ if (!hasContent) return prev;
50976
+ return [
50977
+ ...prev,
50978
+ { id: nextId("assistant"), role: "assistant", content: "", segments: [] }
50979
+ ];
50980
+ });
50981
+ },
50686
50982
  onStreamEnd: () => setStreamingPhase("idle"),
50687
50983
  // Agent is executing (approved) commands outside an open stream — keep
50688
50984
  // the composer locked exactly like the in-stream phases. An open stream
50689
50985
  // keeps ownership: only 'idle' upgrades to 'thinking'. Suppressed during
50690
50986
  // initial catchup so a replayed dead tail can't lock a finished dialog.
50691
50987
  onAgentBusy: () => {
50692
- if (suppressAgentBusyRef.current) return;
50988
+ if (suppressAgentBusyRef.current > 0) return;
50693
50989
  setStreamingPhase((p) => p === "idle" ? "thinking" : p);
50694
50990
  },
50695
50991
  // Terminal turn failures can arrive without a MESSAGE_END; unlock the
@@ -50704,27 +51000,171 @@ function useNatsChatAdapter(config, options = {}) {
50704
51000
  provider: meta.providerName || null,
50705
51001
  modelLabel: meta.modelDisplayName || meta.modelName || null,
50706
51002
  contextWindowMaxTokens: meta.contextWindow || null
50707
- })
51003
+ }),
51004
+ // Post-MESSAGE_END tool chunks (approved commands executing after the
51005
+ // approval bubble, async batch results). Routed through the cross-message
51006
+ // updater — the accumulator was reset at MESSAGE_END, so the old
51007
+ // fallthrough replaced the whole trailing bubble with one tool segment.
51008
+ onToolExecuted: (segment) => {
51009
+ setMessages((prev) => applyToolExecutionToMessages(prev, segment));
51010
+ },
51011
+ // Cross-message approval status flip. Wiring this also makes the
51012
+ // processor skip its cumulative re-emit for resolved approvals — which,
51013
+ // post-MESSAGE_END, was an empty array that blanked the trailing bubble.
51014
+ onApprovalResolved: (requestId, status, _approvalType, resolvedByName) => {
51015
+ setMessages((prev) => {
51016
+ let changed = false;
51017
+ const next = prev.map((m) => {
51018
+ if (m.role !== "assistant" || !m.segments) return m;
51019
+ let msgChanged = false;
51020
+ const segs = m.segments.map((s) => {
51021
+ if (s.type === "approval_request" && s.data.requestId === requestId && s.status !== status) {
51022
+ msgChanged = true;
51023
+ return { ...s, status };
51024
+ }
51025
+ if (s.type === "approval_batch" && s.data.approvalRequestId === requestId) {
51026
+ const nextResolvedBy = resolvedByName ?? s.resolvedByName;
51027
+ if (s.status === status && nextResolvedBy === s.resolvedByName) return s;
51028
+ msgChanged = true;
51029
+ return { ...s, status, resolvedByName: nextResolvedBy };
51030
+ }
51031
+ return s;
51032
+ });
51033
+ if (!msgChanged) return m;
51034
+ changed = true;
51035
+ return { ...m, segments: segs };
51036
+ });
51037
+ return changed ? next : prev;
51038
+ });
51039
+ setApprovalStatuses(
51040
+ (prev) => prev[requestId] === status ? prev : { ...prev, [requestId]: status }
51041
+ );
51042
+ },
51043
+ // MESSAGE_REQUEST echo — a user message from THIS or another session.
51044
+ onUserMessage: (text, meta) => {
51045
+ if (!text) return;
51046
+ const seq = meta?.streamSeq;
51047
+ if (typeof seq === "number") {
51048
+ if (seenParticipantSeqsRef.current.has(seq)) return;
51049
+ seenParticipantSeqsRef.current.add(seq);
51050
+ }
51051
+ if (meta?.ownerType !== "ADMIN") {
51052
+ const echoIdx = pendingEchoTextsRef.current.indexOf(text);
51053
+ if (echoIdx !== -1) {
51054
+ pendingEchoTextsRef.current.splice(echoIdx, 1);
51055
+ return;
51056
+ }
51057
+ }
51058
+ const authorType = meta?.ownerType === "ADMIN" ? "admin" : "user";
51059
+ setMessages((prev) => {
51060
+ if (typeof seq !== "number" && hasRecentMessage(
51061
+ prev,
51062
+ (m) => m.role === "user" && (m.authorType ?? "user") === authorType && m.content === text,
51063
+ CONTENT_DEDUP_WINDOW
51064
+ )) {
51065
+ return prev;
51066
+ }
51067
+ return [
51068
+ ...prev,
51069
+ {
51070
+ id: nextId("user"),
51071
+ role: "user",
51072
+ content: text,
51073
+ ...meta?.displayName ? { name: meta.displayName } : {},
51074
+ authorType,
51075
+ ...meta?.contextItems && meta.contextItems.length > 0 ? {
51076
+ contextItems: meta.contextItems.map((ci) => ({
51077
+ type: ci.type,
51078
+ id: ci.id,
51079
+ label: ci.label ?? ci.id
51080
+ }))
51081
+ } : {}
51082
+ }
51083
+ ];
51084
+ });
51085
+ },
51086
+ // Technician / admin direct message into the dialog.
51087
+ onDirectMessage: (text, meta) => {
51088
+ if (!text) return;
51089
+ const seq = meta?.streamSeq;
51090
+ if (typeof seq === "number") {
51091
+ if (seenParticipantSeqsRef.current.has(seq)) return;
51092
+ seenParticipantSeqsRef.current.add(seq);
51093
+ }
51094
+ setMessages((prev) => {
51095
+ if (typeof seq !== "number" && hasRecentMessage(
51096
+ prev,
51097
+ (m) => m.role === "user" && m.authorType === "admin" && m.content === text,
51098
+ CONTENT_DEDUP_WINDOW
51099
+ )) {
51100
+ return prev;
51101
+ }
51102
+ return [
51103
+ ...prev,
51104
+ {
51105
+ id: `direct-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
51106
+ role: "user",
51107
+ content: text,
51108
+ name: meta?.displayName ?? "Admin",
51109
+ authorType: "admin"
51110
+ }
51111
+ ];
51112
+ });
51113
+ },
51114
+ // System notice — rendered as a name-only row (same shape the history
51115
+ // processor produces via `pushStandaloneMessages`).
51116
+ onSystemMessage: (text, meta) => {
51117
+ if (!text) return;
51118
+ const seq = meta?.streamSeq;
51119
+ if (typeof seq === "number") {
51120
+ if (seenParticipantSeqsRef.current.has(seq)) return;
51121
+ seenParticipantSeqsRef.current.add(seq);
51122
+ }
51123
+ setMessages((prev) => {
51124
+ if (typeof seq !== "number" && hasRecentMessage(prev, (m) => m.authorType === "system" && m.name === text, SYSTEM_DEDUP_WINDOW)) {
51125
+ return prev;
51126
+ }
51127
+ return [
51128
+ ...prev,
51129
+ {
51130
+ id: `system-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
51131
+ role: "user",
51132
+ content: "",
51133
+ name: text,
51134
+ authorType: "system"
51135
+ }
51136
+ ];
51137
+ });
51138
+ }
50708
51139
  });
50709
51140
  const { processChunk, reset: resetAccumulator } = useRealtimeChunkProcessor({
50710
51141
  callbacks: {
50711
- onSegmentsUpdate: (segments) => callbacksRef.current.onSegmentsUpdate(segments),
51142
+ onSegmentsUpdate: (segments, meta) => callbacksRef.current.onSegmentsUpdate(segments, meta),
50712
51143
  onStreamStart: () => callbacksRef.current.onStreamStart(),
50713
51144
  onStreamEnd: () => callbacksRef.current.onStreamEnd(),
50714
51145
  onAgentBusy: () => callbacksRef.current.onAgentBusy(),
50715
51146
  onError: () => callbacksRef.current.onError(),
50716
51147
  onTokenUsage: (data) => callbacksRef.current.onTokenUsage(data),
50717
51148
  onMetadata: (meta) => callbacksRef.current.onMetadata(meta),
51149
+ onToolExecuted: (segment) => callbacksRef.current.onToolExecuted(segment),
51150
+ onApprovalResolved: (requestId, status, approvalType, resolvedByName) => callbacksRef.current.onApprovalResolved(requestId, status, approvalType, resolvedByName),
51151
+ onUserMessage: (text, meta) => callbacksRef.current.onUserMessage(text, meta),
51152
+ onDirectMessage: (text, meta) => callbacksRef.current.onDirectMessage(text, meta),
51153
+ onSystemMessage: (text, meta) => callbacksRef.current.onSystemMessage(text, meta),
50718
51154
  onApprove: accumApprove,
50719
51155
  onReject: accumReject
50720
51156
  },
50721
- batchApprovalsEnabled
51157
+ batchApprovalsEnabled,
51158
+ approvalStatuses,
51159
+ ...displayApprovalTypes ? { displayApprovalTypes } : {}
50722
51160
  });
50723
51161
  const {
50724
51162
  processChunk: catchupProcessChunk,
50725
51163
  catchUpChunks,
50726
51164
  startInitialBuffering,
50727
- resetChunkTracking
51165
+ resetChunkTracking,
51166
+ resetAndCatchUp,
51167
+ isBufferingActive
50728
51168
  } = useChunkCatchup({
50729
51169
  dialogId: active ? dialogId : null,
50730
51170
  onChunkReceived: (chunk) => processChunk(chunk),
@@ -50753,11 +51193,13 @@ function useNatsChatAdapter(config, options = {}) {
50753
51193
  onApprove: accumApprove,
50754
51194
  onReject: accumReject,
50755
51195
  approvalStatuses,
50756
- batchApprovalsEnabled
51196
+ batchApprovalsEnabled,
51197
+ ...displayApprovalTypes ? { displayApprovalTypes } : {}
50757
51198
  }
50758
51199
  );
50759
51200
  const unified = mapProcessedToUnified(rawProcessed);
50760
51201
  if (cursor === void 0) {
51202
+ adoptTrailingAssistantRef.current = isBufferingActive() && extractIncompleteMessageState(rawProcessed[rawProcessed.length - 1]) !== void 0;
50761
51203
  setMessages(unified);
50762
51204
  } else {
50763
51205
  setMessages((prev) => [...unified, ...prev]);
@@ -50781,8 +51223,10 @@ function useNatsChatAdapter(config, options = {}) {
50781
51223
  chatTypeFilter,
50782
51224
  approvalStatuses,
50783
51225
  batchApprovalsEnabled,
51226
+ displayApprovalTypes,
50784
51227
  accumApprove,
50785
- accumReject
51228
+ accumReject,
51229
+ isBufferingActive
50786
51230
  ]
50787
51231
  );
50788
51232
  const prevDialogIdRef = useRef74(void 0);
@@ -50791,6 +51235,9 @@ function useNatsChatAdapter(config, options = {}) {
50791
51235
  prevDialogIdRef.current = dialogId;
50792
51236
  historyLoadSeqRef.current += 1;
50793
51237
  resetAccumulator();
51238
+ adoptTrailingAssistantRef.current = false;
51239
+ seenParticipantSeqsRef.current.clear();
51240
+ pendingEchoTextsRef.current = [];
50794
51241
  setMessages([]);
50795
51242
  setMessagesNextCursor(null);
50796
51243
  setDialogTokenUsage(null);
@@ -50802,13 +51249,16 @@ function useNatsChatAdapter(config, options = {}) {
50802
51249
  }, [active, dialogId, loadDialogHistory, resetAccumulator]);
50803
51250
  useEffect88(() => {
50804
51251
  if (!active || !dialogId) return;
50805
- suppressAgentBusyRef.current = true;
51252
+ suppressAgentBusyRef.current += 1;
50806
51253
  resetChunkTracking();
50807
51254
  startInitialBuffering();
50808
51255
  catchUpChunks().catch((err) => {
50809
51256
  console.error("[useNatsChatAdapter] initial catchup failed:", err);
50810
51257
  }).finally(() => {
50811
- suppressAgentBusyRef.current = false;
51258
+ suppressAgentBusyRef.current = Math.max(0, suppressAgentBusyRef.current - 1);
51259
+ if (dialogId === currentDialogIdRef.current) {
51260
+ adoptTrailingAssistantRef.current = false;
51261
+ }
50812
51262
  });
50813
51263
  }, [active, dialogId, resetChunkTracking, startInitialBuffering, catchUpChunks]);
50814
51264
  useNatsDialogSubscription({
@@ -50823,6 +51273,30 @@ function useNatsChatAdapter(config, options = {}) {
50823
51273
  onEvent: (payload, messageType) => {
50824
51274
  catchupProcessChunk(payload, messageType);
50825
51275
  setConnectionState("connected");
51276
+ },
51277
+ onConnect: () => {
51278
+ if (!hasConnectedOnceRef.current) {
51279
+ hasConnectedOnceRef.current = true;
51280
+ return;
51281
+ }
51282
+ suppressAgentBusyRef.current += 1;
51283
+ startInitialBuffering();
51284
+ void (async () => {
51285
+ try {
51286
+ if (dialogId && fetchDialogMessages) {
51287
+ await loadDialogHistory(dialogId);
51288
+ }
51289
+ if (dialogId !== currentDialogIdRef.current) return;
51290
+ await resetAndCatchUp();
51291
+ } catch (err) {
51292
+ console.error("[useNatsChatAdapter] reconnect catchup failed:", err);
51293
+ } finally {
51294
+ suppressAgentBusyRef.current = Math.max(0, suppressAgentBusyRef.current - 1);
51295
+ if (dialogId === currentDialogIdRef.current) {
51296
+ adoptTrailingAssistantRef.current = false;
51297
+ }
51298
+ }
51299
+ })();
50826
51300
  }
50827
51301
  });
50828
51302
  useEffect88(() => {
@@ -50874,6 +51348,8 @@ function useNatsChatAdapter(config, options = {}) {
50874
51348
  const sendMessage = useCallback53(
50875
51349
  async (text, sendOptions) => {
50876
51350
  const hidden = sendOptions?.hidden ?? false;
51351
+ pendingEchoTextsRef.current.push(text);
51352
+ if (pendingEchoTextsRef.current.length > 5) pendingEchoTextsRef.current.shift();
50877
51353
  setMessages((prev) => [
50878
51354
  ...prev,
50879
51355
  {
@@ -51254,65 +51730,6 @@ function useUnifiedChat(options) {
51254
51730
  );
51255
51731
  }
51256
51732
 
51257
- // src/components/chat/utils/extract-incomplete-message-state.ts
51258
- function extractIncompleteMessageState(lastMessage) {
51259
- if (!lastMessage || lastMessage.role !== "assistant" || typeof lastMessage.content === "string") {
51260
- return void 0;
51261
- }
51262
- const segments = lastMessage.content;
51263
- const pendingApprovals = /* @__PURE__ */ new Map();
51264
- const executingTools = /* @__PURE__ */ new Map();
51265
- let hasIncompleteState = false;
51266
- segments.forEach((segment) => {
51267
- switch (segment.type) {
51268
- case "tool_execution":
51269
- if (segment.data.type === "EXECUTING_TOOL") {
51270
- const toolKey = segment.data.toolExecutionRequestId || `${segment.data.integratedToolType}-${segment.data.toolFunction}`;
51271
- executingTools.set(toolKey, {
51272
- integratedToolType: segment.data.integratedToolType,
51273
- toolFunction: segment.data.toolFunction,
51274
- toolTitle: segment.data.toolTitle,
51275
- parameters: segment.data.parameters
51276
- });
51277
- hasIncompleteState = true;
51278
- }
51279
- break;
51280
- case "approval_request":
51281
- if (segment.status === "pending" && segment.data.requestId) {
51282
- pendingApprovals.set(segment.data.requestId, {
51283
- command: segment.data.command,
51284
- explanation: segment.data.explanation,
51285
- approvalType: segment.data.approvalType || "CLIENT"
51286
- });
51287
- hasIncompleteState = true;
51288
- }
51289
- break;
51290
- case "approval_batch": {
51291
- const allDone = !!segment.data.executions && segment.data.toolCalls.every(
51292
- (c) => segment.data.executions?.[c.toolExecutionRequestId]?.status === "done"
51293
- );
51294
- if (segment.status !== "rejected" && !allDone) {
51295
- hasIncompleteState = true;
51296
- }
51297
- break;
51298
- }
51299
- case "context_compaction":
51300
- if (segment.status === "started") {
51301
- hasIncompleteState = true;
51302
- }
51303
- break;
51304
- }
51305
- });
51306
- if (!hasIncompleteState) {
51307
- return void 0;
51308
- }
51309
- return {
51310
- existingSegments: segments,
51311
- pendingApprovals: pendingApprovals.size > 0 ? pendingApprovals : void 0,
51312
- executingTools: executingTools.size > 0 ? executingTools : void 0
51313
- };
51314
- }
51315
-
51316
51733
  // src/components/chat/utils/history-merge.ts
51317
51734
  var SYNTHETIC_REALTIME_ID_PREFIXES = ["assistant-", "user-", "direct-", "system-", "error-"];
51318
51735
  function isSyntheticRealtimeId(id) {
@@ -53697,11 +54114,14 @@ export {
53697
54114
  processHistoricalMessages,
53698
54115
  extractErrorMessages,
53699
54116
  processHistoricalMessagesWithErrors,
54117
+ extractIncompleteMessageState,
54118
+ appendToTrailingAssistant,
54119
+ upsertTrailingCompaction,
54120
+ applyToolExecutionToMessages,
53700
54121
  mapProcessedToUnified,
53701
54122
  historicalToUnified,
53702
54123
  useNatsChatAdapter,
53703
54124
  useUnifiedChat,
53704
- extractIncompleteMessageState,
53705
54125
  SYNTHETIC_REALTIME_ID_PREFIXES,
53706
54126
  flattenMessagePagesChronological,
53707
54127
  maxPersistedStreamSeq,
@@ -54118,4 +54538,4 @@ export {
54118
54538
  LogsList,
54119
54539
  assets
54120
54540
  };
54121
- //# sourceMappingURL=chunk-NDWIFCLS.js.map
54541
+ //# sourceMappingURL=chunk-3BHV2QAH.js.map