@flamingo-stack/openframe-frontend-core 0.0.588 → 0.0.589-1973.5571.1

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 (161) hide show
  1. package/dist/chat-protocol/events.d.ts +8 -2
  2. package/dist/chat-protocol/events.d.ts.map +1 -1
  3. package/dist/chat-protocol/index.cjs +188 -0
  4. package/dist/chat-protocol/index.cjs.map +1 -1
  5. package/dist/chat-protocol/index.js +188 -0
  6. package/dist/chat-protocol/index.js.map +1 -1
  7. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
  8. package/dist/chat-protocol/source-metadata.d.ts +5 -0
  9. package/dist/chat-protocol/source-metadata.d.ts.map +1 -0
  10. package/dist/{chunk-GKPY7VLK.js → chunk-47NIV4FC.js} +4 -4
  11. package/dist/{chunk-D7YISHXC.js → chunk-4MVGFYRE.js} +534 -190
  12. package/dist/chunk-4MVGFYRE.js.map +1 -0
  13. package/dist/{chunk-I5CJJFEJ.js → chunk-4VDGIODG.js} +3 -3
  14. package/dist/{chunk-PAUJQV3C.cjs → chunk-627JVTID.cjs} +15 -15
  15. package/dist/{chunk-PAUJQV3C.cjs.map → chunk-627JVTID.cjs.map} +1 -1
  16. package/dist/{chunk-3OCYNMZ5.js → chunk-CQNILR7U.js} +3 -3
  17. package/dist/{chunk-BONXSRHZ.cjs → chunk-DPQGO4PH.cjs} +21 -21
  18. package/dist/{chunk-BONXSRHZ.cjs.map → chunk-DPQGO4PH.cjs.map} +1 -1
  19. package/dist/{chunk-A6X7EXP6.cjs → chunk-DVAAXDZV.cjs} +10 -10
  20. package/dist/{chunk-A6X7EXP6.cjs.map → chunk-DVAAXDZV.cjs.map} +1 -1
  21. package/dist/{chunk-CN36E4PK.js → chunk-EJEHIAOV.js} +2 -2
  22. package/dist/{chunk-G45RGWHF.cjs → chunk-EKKR5ZFB.cjs} +887 -543
  23. package/dist/chunk-EKKR5ZFB.cjs.map +1 -0
  24. package/dist/{chunk-IYDHV2CS.cjs → chunk-FIEOWSXH.cjs} +43 -43
  25. package/dist/{chunk-IYDHV2CS.cjs.map → chunk-FIEOWSXH.cjs.map} +1 -1
  26. package/dist/{chunk-CJOWLRUU.js → chunk-G4ND6ZZK.js} +2 -2
  27. package/dist/{chunk-KWTV3KGY.cjs → chunk-GDHQTVBP.cjs} +9 -9
  28. package/dist/{chunk-KWTV3KGY.cjs.map → chunk-GDHQTVBP.cjs.map} +1 -1
  29. package/dist/{chunk-ADANP3ZA.cjs → chunk-GOHHFX4O.cjs} +33 -33
  30. package/dist/{chunk-ADANP3ZA.cjs.map → chunk-GOHHFX4O.cjs.map} +1 -1
  31. package/dist/{chunk-KVNGYFFP.js → chunk-GU5TWGAJ.js} +3 -3
  32. package/dist/{chunk-EGYX55HT.cjs → chunk-HQKWJFFX.cjs} +159 -159
  33. package/dist/{chunk-EGYX55HT.cjs.map → chunk-HQKWJFFX.cjs.map} +1 -1
  34. package/dist/{chunk-NXZC6UTN.js → chunk-HZ6VUTLA.js} +2 -2
  35. package/dist/{chunk-WUCYI3BC.cjs → chunk-KH5KDKCE.cjs} +24 -24
  36. package/dist/{chunk-WUCYI3BC.cjs.map → chunk-KH5KDKCE.cjs.map} +1 -1
  37. package/dist/{chunk-JDINUWND.cjs → chunk-MQFA5DXU.cjs} +69 -69
  38. package/dist/{chunk-JDINUWND.cjs.map → chunk-MQFA5DXU.cjs.map} +1 -1
  39. package/dist/{chunk-5CHW7AMP.cjs → chunk-NHW2AM2X.cjs} +10 -10
  40. package/dist/{chunk-5CHW7AMP.cjs.map → chunk-NHW2AM2X.cjs.map} +1 -1
  41. package/dist/{chunk-KK3XU6YJ.cjs → chunk-NIHPN72N.cjs} +4 -4
  42. package/dist/{chunk-KK3XU6YJ.cjs.map → chunk-NIHPN72N.cjs.map} +1 -1
  43. package/dist/{chunk-EGUU3QCU.cjs → chunk-NNRRRWCD.cjs} +3 -3
  44. package/dist/{chunk-EGUU3QCU.cjs.map → chunk-NNRRRWCD.cjs.map} +1 -1
  45. package/dist/{chunk-4ZKAMPBX.js → chunk-OAY342OE.js} +2 -2
  46. package/dist/{chunk-TOYHZHTK.js → chunk-PDFH7CXB.js} +2 -2
  47. package/dist/{chunk-NIDPX52C.js → chunk-QIHBS5PM.js} +5 -5
  48. package/dist/{chunk-CNKVEVUN.js → chunk-SPOIDC23.js} +5 -5
  49. package/dist/{chunk-MENQLD57.cjs → chunk-T5FJOWHA.cjs} +5 -5
  50. package/dist/{chunk-MENQLD57.cjs.map → chunk-T5FJOWHA.cjs.map} +1 -1
  51. package/dist/{chunk-5FNWOF46.js → chunk-WZZSFOWG.js} +6 -6
  52. package/dist/{chunk-FVRRYJ6P.cjs → chunk-XIKSZSHC.cjs} +22 -22
  53. package/dist/{chunk-FVRRYJ6P.cjs.map → chunk-XIKSZSHC.cjs.map} +1 -1
  54. package/dist/{chunk-C6RJRSIM.cjs → chunk-XTMNO7MC.cjs} +3 -3
  55. package/dist/{chunk-C6RJRSIM.cjs.map → chunk-XTMNO7MC.cjs.map} +1 -1
  56. package/dist/{chunk-FQBR6SCA.js → chunk-XW4GOMK6.js} +3 -3
  57. package/dist/{chunk-77Y7R243.js → chunk-YEEMXD25.js} +5 -5
  58. package/dist/{chunk-LGBNHWDN.js → chunk-YTVTZ7JO.js} +3 -3
  59. package/dist/components/case-studies/index.cjs +9 -9
  60. package/dist/components/case-studies/index.js +3 -3
  61. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  62. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  63. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  64. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  65. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +4 -1
  66. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  67. package/dist/components/chat/hooks/use-slash-commands.d.ts +3 -3
  68. package/dist/components/chat/hooks/use-slash-commands.d.ts.map +1 -1
  69. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +2 -28
  70. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  71. package/dist/components/chat/index.cjs +2 -2
  72. package/dist/components/chat/index.js +1 -1
  73. package/dist/components/chat/remark-strip-citations.d.ts.map +1 -1
  74. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
  75. package/dist/components/chat/tool-execution-display.d.ts.map +1 -1
  76. package/dist/components/chat/types/component.types.d.ts +4 -0
  77. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  78. package/dist/components/chat/types/message.types.d.ts +35 -1
  79. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  80. package/dist/components/chat/types/unified-chat-state.types.d.ts +3 -3
  81. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  82. package/dist/components/chat/utils/chat-source-metadata.d.ts +5 -0
  83. package/dist/components/chat/utils/chat-source-metadata.d.ts.map +1 -0
  84. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  85. package/dist/components/contact/index.cjs +4 -4
  86. package/dist/components/contact/index.js +3 -3
  87. package/dist/components/docs/index.cjs +7 -7
  88. package/dist/components/docs/index.js +6 -6
  89. package/dist/components/embeds/index.cjs +4 -4
  90. package/dist/components/embeds/index.js +3 -3
  91. package/dist/components/faq/index.cjs +4 -4
  92. package/dist/components/faq/index.js +3 -3
  93. package/dist/components/features/index.cjs +3 -3
  94. package/dist/components/features/index.js +2 -2
  95. package/dist/components/help-center-pages/index.cjs +23 -23
  96. package/dist/components/help-center-pages/index.js +14 -14
  97. package/dist/components/index.cjs +36 -36
  98. package/dist/components/index.js +13 -13
  99. package/dist/components/meeting-scheduler/index.cjs +21 -21
  100. package/dist/components/meeting-scheduler/index.js +2 -2
  101. package/dist/components/navigation/index.cjs +6 -6
  102. package/dist/components/navigation/index.js +5 -5
  103. package/dist/components/onboarding-guides/index.cjs +6 -6
  104. package/dist/components/onboarding-guides/index.js +5 -5
  105. package/dist/components/tickets/index.cjs +7 -7
  106. package/dist/components/tickets/index.js +6 -6
  107. package/dist/components/ui/hover-dropdown.d.ts.map +1 -1
  108. package/dist/components/ui/index.cjs +3 -3
  109. package/dist/components/ui/index.js +2 -2
  110. package/dist/index.cjs +7 -7
  111. package/dist/index.js +6 -6
  112. package/dist/utils/index.cjs.map +1 -1
  113. package/dist/utils/index.js.map +1 -1
  114. package/package.json +2 -1
  115. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +130 -0
  116. package/src/chat-protocol/__tests__/source-metadata.test.ts +74 -0
  117. package/src/chat-protocol/events.ts +9 -1
  118. package/src/chat-protocol/nats-decoder.ts +7 -0
  119. package/src/chat-protocol/source-metadata.ts +202 -0
  120. package/src/components/chat/__tests__/chat-message-list.test.tsx +70 -0
  121. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +86 -0
  122. package/src/components/chat/__tests__/embeddable-chat-sources.test.tsx +411 -0
  123. package/src/components/chat/__tests__/remark-strip-citations.test.ts +52 -0
  124. package/src/components/chat/__tests__/tool-execution-display.test.tsx +93 -0
  125. package/src/components/chat/chat-message-enhanced.tsx +28 -13
  126. package/src/components/chat/chat-message-list.tsx +27 -22
  127. package/src/components/chat/embeddable-chat.tsx +55 -36
  128. package/src/components/chat/entity-cards/__tests__/dispatch-video-refs.test.tsx +74 -0
  129. package/src/components/chat/entity-cards/dispatch.tsx +86 -17
  130. package/src/components/chat/hooks/__tests__/use-slash-commands.test.ts +64 -0
  131. package/src/components/chat/hooks/use-nats-chat-adapter.ts +7 -0
  132. package/src/components/chat/hooks/use-slash-commands.ts +9 -5
  133. package/src/components/chat/hooks/use-sse-chat-adapter.ts +2 -29
  134. package/src/components/chat/remark-strip-citations.ts +5 -3
  135. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +56 -0
  136. package/src/components/chat/stream/chat-stream-reducer.ts +31 -0
  137. package/src/components/chat/tool-execution-display.tsx +5 -2
  138. package/src/components/chat/types/component.types.ts +4 -0
  139. package/src/components/chat/types/message.types.ts +39 -0
  140. package/src/components/chat/types/unified-chat-state.types.ts +4 -3
  141. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +57 -0
  142. package/src/components/chat/utils/chat-source-metadata.ts +19 -0
  143. package/src/components/chat/utils/process-historical-messages.ts +19 -1
  144. package/src/components/ui/hover-dropdown.tsx +8 -3
  145. package/dist/chunk-D7YISHXC.js.map +0 -1
  146. package/dist/chunk-G45RGWHF.cjs.map +0 -1
  147. /package/dist/{chunk-GKPY7VLK.js.map → chunk-47NIV4FC.js.map} +0 -0
  148. /package/dist/{chunk-I5CJJFEJ.js.map → chunk-4VDGIODG.js.map} +0 -0
  149. /package/dist/{chunk-3OCYNMZ5.js.map → chunk-CQNILR7U.js.map} +0 -0
  150. /package/dist/{chunk-CN36E4PK.js.map → chunk-EJEHIAOV.js.map} +0 -0
  151. /package/dist/{chunk-CJOWLRUU.js.map → chunk-G4ND6ZZK.js.map} +0 -0
  152. /package/dist/{chunk-KVNGYFFP.js.map → chunk-GU5TWGAJ.js.map} +0 -0
  153. /package/dist/{chunk-NXZC6UTN.js.map → chunk-HZ6VUTLA.js.map} +0 -0
  154. /package/dist/{chunk-4ZKAMPBX.js.map → chunk-OAY342OE.js.map} +0 -0
  155. /package/dist/{chunk-TOYHZHTK.js.map → chunk-PDFH7CXB.js.map} +0 -0
  156. /package/dist/{chunk-NIDPX52C.js.map → chunk-QIHBS5PM.js.map} +0 -0
  157. /package/dist/{chunk-CNKVEVUN.js.map → chunk-SPOIDC23.js.map} +0 -0
  158. /package/dist/{chunk-5FNWOF46.js.map → chunk-WZZSFOWG.js.map} +0 -0
  159. /package/dist/{chunk-FQBR6SCA.js.map → chunk-XW4GOMK6.js.map} +0 -0
  160. /package/dist/{chunk-77Y7R243.js.map → chunk-YEEMXD25.js.map} +0 -0
  161. /package/dist/{chunk-LGBNHWDN.js.map → chunk-YTVTZ7JO.js.map} +0 -0
@@ -22,13 +22,16 @@ import type { Plugin } from 'unified';
22
22
  import { visit } from 'unist-util-visit';
23
23
 
24
24
  const CITATION_REGEX = /(^|\s)\[\d{1,2}(?:\s*,\s*\d{1,2})*\](?=[\s.,;:!?)\]]|$)/g;
25
+ const CITATION_LIST_REGEX =
26
+ /(^|\s)(?:\[\d{1,2}(?:\s*,\s*\d{1,2})*\]\s*,\s*)+(?:(?:and|or)\s+)?\[\d{1,2}(?:\s*,\s*\d{1,2})*\](?=[\s.,;:!?)\]]|$)/g;
25
27
 
26
28
  export const remarkStripCitations: Plugin<[], Root> = () => {
27
29
  return (tree: Root) => {
28
30
  visit(tree, 'text', (node: Text, index, parent) => {
29
31
  if (!parent || typeof index !== 'number') return;
30
32
  if (!node.value || !node.value.includes('[')) return;
31
- const withoutCitations = node.value.replace(CITATION_REGEX, '$1');
33
+ const withoutCitationLists = node.value.replace(CITATION_LIST_REGEX, '$1');
34
+ const withoutCitations = withoutCitationLists.replace(CITATION_REGEX, '$1');
32
35
  // No citation matched → leave the node untouched, INCLUDING its
33
36
  // whitespace (collapsing unconditionally would rewrite deliberate
34
37
  // spacing in prose like `array[1] value`).
@@ -41,8 +44,7 @@ export const remarkStripCitations: Plugin<[], Root> = () => {
41
44
  // leaf and moves on to the next sibling, never re-entering this slot.
42
45
  parent.children.splice(index, 1, {
43
46
  ...node,
44
- // Collapse doubled spaces the removal can leave mid-sentence.
45
- value: withoutCitations.replace(/ {2,}/g, ' '),
47
+ value: withoutCitations.replace(/\s+([.,;:!?])/g, '$1').replace(/ {2,}/g, ' '),
46
48
  });
47
49
  });
48
50
  };
@@ -1289,3 +1289,59 @@ describe('createChatStreamReducer — ask cards', () => {
1289
1289
  expect(last.segments?.map(s => s.type)).toEqual(['ask', 'ask']);
1290
1290
  });
1291
1291
  });
1292
+
1293
+ describe('createChatStreamReducer — Mingo source metadata', () => {
1294
+ const sources = [
1295
+ {
1296
+ index: 1,
1297
+ name: 'Enroll devices',
1298
+ path: 'docs/enroll.md',
1299
+ documentType: 'markdown',
1300
+ },
1301
+ ];
1302
+
1303
+ it('attaches metadata received before text to the same assistant turn', () => {
1304
+ const reducer = createChatStreamReducer({ transport: 'nats' });
1305
+ const refs = [
1306
+ {
1307
+ type: 'video',
1308
+ id: 'mux-9b6586b494',
1309
+ title: 'Community demo',
1310
+ url: 'https://stream.mux.com/playback-id.m3u8',
1311
+ metadata: { videoUrl: 'https://stream.mux.com/playback-id.m3u8' },
1312
+ },
1313
+ ];
1314
+
1315
+ reducer.apply({ type: 'turn-start', seq: 1 });
1316
+ reducer.apply({ type: 'sources', sources, refs, seq: 2 });
1317
+ reducer.apply({ type: 'text-delta', text: 'Follow the guide [1].', seq: 3 });
1318
+
1319
+ expect(reducer.state.messages).toHaveLength(1);
1320
+ expect(reducer.state.messages[0]).toMatchObject({ sources, refs });
1321
+ });
1322
+
1323
+ it('deduplicates cumulative metadata and does not leak it into the next turn', () => {
1324
+ const reducer = createChatStreamReducer({ transport: 'nats' });
1325
+
1326
+ reducer.apply({ type: 'turn-start', seq: 1 });
1327
+ reducer.apply({ type: 'sources', sources, seq: 2 });
1328
+ reducer.apply({
1329
+ type: 'sources',
1330
+ sources: [
1331
+ { ...sources[0], name: 'Duplicate' },
1332
+ { index: 2, name: 'Install agent', path: 'docs/install.md', documentType: 'markdown' },
1333
+ ],
1334
+ seq: 3,
1335
+ });
1336
+ reducer.apply({ type: 'text-delta', text: 'First answer [1] [2].', seq: 4 });
1337
+ reducer.apply({ type: 'turn-end', seq: 5 });
1338
+ reducer.apply({ type: 'turn-start', seq: 6 });
1339
+ reducer.apply({ type: 'text-delta', text: 'Second answer.', seq: 7 });
1340
+
1341
+ expect(reducer.state.messages[0].sources).toEqual([
1342
+ sources[0],
1343
+ { index: 2, name: 'Install agent', path: 'docs/install.md', documentType: 'markdown' },
1344
+ ]);
1345
+ expect(reducer.state.messages[1].sources).toBeUndefined();
1346
+ });
1347
+ });
@@ -60,6 +60,7 @@ import {
60
60
  type ParticipantEvent,
61
61
  type UsageEvent,
62
62
  } from '../../../chat-protocol/events';
63
+ import type { ChatRef } from '../chat-ref.types';
63
64
  import type {
64
65
  ApprovalBatchSegment,
65
66
  ApprovalRequestSegment,
@@ -70,6 +71,7 @@ import type {
70
71
  ToolExecutionSegment,
71
72
  ExecutingToolState,
72
73
  } from '../types';
74
+ import type { ChatSource } from '../types/message.types';
73
75
  import type { PendingApproval } from '../types/processing.types';
74
76
  import type {
75
77
  DialogTokenUsage,
@@ -78,6 +80,7 @@ import type {
78
80
  UnifiedUsageBreakdown,
79
81
  } from '../types/unified-chat-state.types';
80
82
  import { approvalDisplaysInline } from '../utils/approval-display';
83
+ import { mergeChatRefs, mergeChatSources } from '../utils/chat-source-metadata';
81
84
  import {
82
85
  type MessageSegmentAccumulator,
83
86
  createMessageSegmentAccumulator,
@@ -676,6 +679,9 @@ export function createChatStreamReducer(options: ChatStreamReducerOptions = {}):
676
679
  let sendCount = 0;
677
680
  let sseCurrentText = '';
678
681
 
682
+ let pendingSources: ChatSource[] | undefined;
683
+ let pendingRefs: ChatRef[] | undefined;
684
+
679
685
  // ── Snapshot cache ─────────────────────────────────────────────────────
680
686
  let stateCache: ChatReducerState | null = null;
681
687
  function invalidate(): void {
@@ -773,6 +779,18 @@ export function createChatStreamReducer(options: ChatStreamReducerOptions = {}):
773
779
  setMessagesInternal(stampTrailingAssistantSeq(updateTrailingAssistant(messages, segments), seq));
774
780
  }
775
781
 
782
+ function attachPendingSourceMetadata(): void {
783
+ if (!pendingSources && !pendingRefs) return;
784
+ const last = messages[messages.length - 1];
785
+ if (!last || last.role !== 'assistant') return;
786
+ const next = {
787
+ ...last,
788
+ ...(pendingSources ? { sources: pendingSources } : {}),
789
+ ...(pendingRefs ? { refs: pendingRefs } : {}),
790
+ };
791
+ setMessagesInternal([...messages.slice(0, -1), next]);
792
+ }
793
+
776
794
  function applyStreamStartToState(): void {
777
795
  setPhaseInternal('streaming');
778
796
  // A new stream must never overwrite a COMPLETED trailing assistant
@@ -966,6 +984,8 @@ export function createChatStreamReducer(options: ChatStreamReducerOptions = {}):
966
984
 
967
985
  switch (event.type) {
968
986
  case 'turn-start': {
987
+ pendingSources = undefined;
988
+ pendingRefs = undefined;
969
989
  isInStream = true;
970
990
  hasEverStreamed = true;
971
991
  turnStartedAt = Date.now();
@@ -981,6 +1001,15 @@ export function createChatStreamReducer(options: ChatStreamReducerOptions = {}):
981
1001
  emit('onStreamEnd');
982
1002
  setPhaseInternal('idle');
983
1003
  accumulator.resetSegments();
1004
+ pendingSources = undefined;
1005
+ pendingRefs = undefined;
1006
+ break;
1007
+ }
1008
+
1009
+ case 'sources': {
1010
+ pendingSources = mergeChatSources(pendingSources, event.sources);
1011
+ if (event.refs) pendingRefs = mergeChatRefs(pendingRefs, event.refs);
1012
+ attachPendingSourceMetadata();
984
1013
  break;
985
1014
  }
986
1015
 
@@ -1017,10 +1046,12 @@ export function createChatStreamReducer(options: ChatStreamReducerOptions = {}):
1017
1046
  if (isInStream || !hasEverStreamed) {
1018
1047
  emitSegments(segments);
1019
1048
  applySegmentsToState(segments, withSeqMeta(undefined));
1049
+ attachPendingSourceMetadata();
1020
1050
  } else {
1021
1051
  const delta: MessageSegment[] = [{ type: kind, text: event.text }];
1022
1052
  emitSegments(delta, { append: true });
1023
1053
  applySegmentsToState(delta, withSeqMeta({ append: true }));
1054
+ attachPendingSourceMetadata();
1024
1055
  }
1025
1056
  break;
1026
1057
  }
@@ -54,6 +54,9 @@ const ToolExecutionDisplay = forwardRef<HTMLDivElement, ToolExecutionDisplayProp
54
54
 
55
55
  const hasResult = isExecuted && typeof message.result === 'string' && message.result.length > 0;
56
56
  const hasBody = argEntries.length > 0 || hasResult || isExecuting;
57
+ const isConciseRemoteRead =
58
+ !message.integratedToolType && !!message.toolTitle?.trim() && argEntries.length === 0 && !hasResult;
59
+ const isStaticRow = isClientView || isConciseRemoteRead;
57
60
 
58
61
  const renderStatusIcon = () => {
59
62
  if (isExecuting) return <DotsLoaderIcon size={16} className="text-ods-text-secondary" />;
@@ -82,7 +85,7 @@ const ToolExecutionDisplay = forwardRef<HTMLDivElement, ToolExecutionDisplayProp
82
85
  {previewText}
83
86
  </div>
84
87
  <div className="flex h-5 w-5 shrink-0 items-center justify-center">{renderStatusIcon()}</div>
85
- {!isClientView && (
88
+ {!isStaticRow && (
86
89
  <div className="flex h-5 w-5 shrink-0 items-center justify-center">
87
90
  <ExpandChevron expanded={expanded} />
88
91
  </div>
@@ -101,7 +104,7 @@ const ToolExecutionDisplay = forwardRef<HTMLDivElement, ToolExecutionDisplayProp
101
104
  )}
102
105
  {...props}
103
106
  >
104
- {isClientView ? (
107
+ {isStaticRow ? (
105
108
  // Client (Fae end-user): static, non-expandable row — no chevron, no
106
109
  // body. Just the explanation + status; command/args/result are hidden.
107
110
  <div className="flex w-full items-start gap-[var(--spacing-system-xs)] p-[var(--spacing-system-s)] text-left">
@@ -195,6 +195,8 @@ export interface ChatMessageEnhancedProps extends Omit<HTMLAttributes<HTMLDivEle
195
195
  * back to plain text title-only.
196
196
  */
197
197
  renderEntityCard?: (reference: ChatRef) => ReactNode;
198
+ /** Enriched references associated with this assistant answer. */
199
+ refs?: ChatRef[];
198
200
  /**
199
201
  * Host-provided anchor component for markdown links. When supplied, the
200
202
  * `<a>` override in the markdown renderer delegates to this component
@@ -304,6 +306,8 @@ export interface ChatMessageListProps extends HTMLAttributes<HTMLDivElement> {
304
306
  * ChatMessageEnhanced; the host sends the received label as the user's next
305
307
  * message. Omit to render ask cards read-only. */
306
308
  onAskSelect?: (label: string) => void;
309
+ /** Renders content owned by one message directly after that message. */
310
+ renderAfterMessage?: (message: Message, index: number) => ReactNode;
307
311
  }
308
312
 
309
313
  export interface ChatMessageListRef {
@@ -3,6 +3,7 @@
3
3
  * Contains all message structures, segments, and content types
4
4
  */
5
5
 
6
+ import type { ChatRef } from '../chat-ref.types';
6
7
  import type { AssistantType, AuthorType, ChatApprovalStatus, MessageOwner } from './chat.types';
7
8
 
8
9
  // ========== Message Type Definitions ==========
@@ -10,6 +11,8 @@ import type { AssistantType, AuthorType, ChatApprovalStatus, MessageOwner } from
10
11
  export const MESSAGE_TYPE = {
11
12
  TEXT: 'TEXT',
12
13
  THINKING: 'THINKING',
14
+ GUIDE: 'GUIDE',
15
+ SOURCES: 'SOURCES',
13
16
  ASK: 'ASK',
14
17
  EXECUTING_TOOL: 'EXECUTING_TOOL',
15
18
  EXECUTED_TOOL: 'EXECUTED_TOOL',
@@ -46,6 +49,26 @@ export const SCROLL_ANCHOR = { TOP: 'top', BOTTOM: 'bottom' } as const;
46
49
 
47
50
  export type ScrollAnchor = (typeof SCROLL_ANCHOR)[keyof typeof SCROLL_ANCHOR];
48
51
 
52
+ export interface ChatSource {
53
+ index: number;
54
+ name: string;
55
+ path: string;
56
+ documentType: string;
57
+ externalUrl?: string;
58
+ targetPlatform?: string | null;
59
+ id?: string;
60
+ items?: Array<{
61
+ id: string;
62
+ documentType: string;
63
+ name: string;
64
+ externalUrl?: string;
65
+ targetPlatform?: string | null;
66
+ path?: string | null;
67
+ }>;
68
+ sourceRepo?: string;
69
+ label?: string;
70
+ }
71
+
49
72
  // ========== Tool Execution Types ==========
50
73
 
51
74
  export interface ToolExecutionData {
@@ -391,6 +414,16 @@ export interface ThinkingMessageData extends MessageDataBase {
391
414
  text?: string;
392
415
  }
393
416
 
417
+ export interface GuideMessageData extends MessageDataBase {
418
+ type: 'GUIDE';
419
+ payload?: Record<string, unknown>;
420
+ }
421
+
422
+ export interface SourcesMessageData extends MessageDataBase {
423
+ type: 'SOURCES';
424
+ payload?: Record<string, unknown>;
425
+ }
426
+
394
427
  /** Persisted `ASK` row (GraphQL `AskData`). `text` is the intro sentence, which
395
428
  * history replays as a text segment ahead of the card — same split the live
396
429
  * `ASK` chunk carries. */
@@ -532,6 +565,8 @@ export interface ContextCompactionEndMessageData extends MessageDataBase {
532
565
  export type MessageData =
533
566
  | TextMessageData
534
567
  | ThinkingMessageData
568
+ | GuideMessageData
569
+ | SourcesMessageData
535
570
  | AskMessageData
536
571
  | ExecutingToolMessageData
537
572
  | ExecutedToolMessageData
@@ -576,6 +611,8 @@ export interface ProcessedMessage {
576
611
  authorType?: AuthorType;
577
612
  timestamp: Date;
578
613
  avatar?: string;
614
+ sources?: ChatSource[];
615
+ refs?: ChatRef[];
579
616
  /** Persisted last-chunk stream sequence carried through from
580
617
  * `HistoricalMessage.lastChunkStreamSeq` (for assistant turns: the MAX
581
618
  * across the grouped rows). Hosts stamp it onto the rendered message's
@@ -598,6 +635,8 @@ export interface Message {
598
635
  authorType?: AuthorType;
599
636
  timestamp?: Date;
600
637
  avatar?: string | null;
638
+ sources?: ChatSource[];
639
+ refs?: ChatRef[];
601
640
  /** Highest CONTENT chunk streamSeq that composed this message. Stamped on
602
641
  * realtime synthetics so `mergeHistoryWithRealtime` can decide history
603
642
  * coverage per-message (see `MergeableChatMessage.streamSeq`). */
@@ -17,12 +17,11 @@
17
17
  */
18
18
 
19
19
  import type { ChatRef } from '../chat-ref.types';
20
- import type { ChatSource } from '../hooks/use-sse-chat-adapter';
21
20
  import type { ChatAttachment } from '../utils/chat-attachment-markdown';
22
21
  import type { AuthorType } from './chat.types';
23
22
  import type { DialogItem } from './component.types';
24
23
  import type { ChatContextItem } from './context-item.types';
25
- import type { MessageSegment, ScrollAnchor } from './message.types';
24
+ import type { ChatSource, MessageSegment, ScrollAnchor } from './message.types';
26
25
 
27
26
  // ─── Per-dialog token usage (Mingo backend telemetry) ────────────────────────
28
27
 
@@ -169,9 +168,11 @@ export interface UnifiedChatMessage {
169
168
  */
170
169
  timestamp?: Date | string | number;
171
170
 
172
- /** Guide/SSE-only: document citations. Undefined in Mingo mode. */
171
+ /** Document citations for a Guide answer over SSE or NATS. */
173
172
  sources?: ChatSource[];
174
173
 
174
+ refs?: ChatRef[];
175
+
175
176
  /**
176
177
  * Per-message viewport-positioning hint. Common to both modes; the
177
178
  * message-list reads it to override default tail-on-stream behaviour
@@ -391,3 +391,60 @@ describe('processHistoricalMessages — ASK rows', () => {
391
391
  expect(processHistoricalMessages(dialog).messages).toEqual([]);
392
392
  });
393
393
  });
394
+
395
+ describe('processHistoricalMessages — Mingo source metadata', () => {
396
+ it.each(['GUIDE', 'SOURCES'] as const)('restores %s metadata on the same assistant turn', type => {
397
+ const dialog: HistoricalMessage[] = [
398
+ {
399
+ id: 'a1',
400
+ chatType: 'ADMIN_AI_CHAT',
401
+ createdAt: '2026-07-19T09:00:05Z',
402
+ owner: { type: 'ASSISTANT' },
403
+ messageData: [
404
+ {
405
+ type,
406
+ payload: {
407
+ sources: [
408
+ {
409
+ index: 1,
410
+ name: 'Install agent',
411
+ path: 'docs/install.md',
412
+ documentType: 'markdown',
413
+ },
414
+ ],
415
+ videos: [
416
+ {
417
+ ref: '[card://video:mux-9b6586b494]',
418
+ title: 'Community demo',
419
+ url: 'https://stream.mux.com/playback-id.m3u8',
420
+ metadata: { videoUrl: 'https://stream.mux.com/playback-id.m3u8' },
421
+ },
422
+ ],
423
+ },
424
+ },
425
+ { type: 'TEXT', text: 'Install the agent [1]. [card://video:mux-9b6586b494]' },
426
+ ],
427
+ },
428
+ ];
429
+
430
+ expect(processHistoricalMessages(dialog).messages[0]).toMatchObject({
431
+ role: 'assistant',
432
+ sources: [
433
+ {
434
+ index: 1,
435
+ name: 'Install agent',
436
+ path: 'docs/install.md',
437
+ documentType: 'markdown',
438
+ },
439
+ ],
440
+ refs: [
441
+ {
442
+ type: 'video',
443
+ id: 'mux-9b6586b494',
444
+ metadata: { videoUrl: 'https://stream.mux.com/playback-id.m3u8' },
445
+ },
446
+ ],
447
+ content: [{ type: 'text', text: 'Install the agent [1]. [card://video:mux-9b6586b494]' }],
448
+ });
449
+ });
450
+ });
@@ -0,0 +1,19 @@
1
+ import type { ChatRef } from '../chat-ref.types';
2
+ import type { ChatSource } from '../types/message.types';
3
+
4
+ export function mergeChatSources(current: ChatSource[] | undefined, incoming: ChatSource[]): ChatSource[] {
5
+ const sourcesByIndex = new Map((current ?? []).map(source => [source.index, source]));
6
+ incoming.forEach(source => {
7
+ if (!sourcesByIndex.has(source.index)) sourcesByIndex.set(source.index, source);
8
+ });
9
+ return Array.from(sourcesByIndex.values());
10
+ }
11
+
12
+ export function mergeChatRefs(current: ChatRef[] | undefined, incoming: ChatRef[]): ChatRef[] {
13
+ const refsByIdentity = new Map((current ?? []).map(ref => [`${ref.type}:${ref.id}`, ref]));
14
+ incoming.forEach(ref => {
15
+ const identity = `${ref.type}:${ref.id}`;
16
+ if (!refsByIdentity.has(identity)) refsByIdentity.set(identity, ref);
17
+ });
18
+ return Array.from(refsByIdentity.values());
19
+ }
@@ -33,7 +33,8 @@ import {
33
33
  } from '../../../chat-protocol/events';
34
34
  // One normalizer for ask rows, shared with the live decoder — history and the
35
35
  // stream must agree on which options are usable.
36
- import { normalizeAskOptions } from '../../../chat-protocol/nats-decoder';
36
+ import { decodeNatsChunk, normalizeAskOptions } from '../../../chat-protocol/nats-decoder';
37
+ import type { ChatRef } from '../chat-ref.types';
37
38
  import { applyApprovalStatusToSegment } from '../stream/message-mutations';
38
39
  import {
39
40
  MESSAGE_TYPE,
@@ -46,8 +47,10 @@ import {
46
47
  type MessageProcessingOptions,
47
48
  type MessageData,
48
49
  type MessageOwner,
50
+ type ChatSource,
49
51
  } from '../types';
50
52
  import { approvalDisplaysInline } from './approval-display';
53
+ import { mergeChatRefs, mergeChatSources } from './chat-source-metadata';
51
54
  import { type MessageSegmentAccumulator, createMessageSegmentAccumulator } from './message-segment-accumulator';
52
55
  import { getCommandText } from './tool-call-helpers';
53
56
 
@@ -121,6 +124,10 @@ export function decodeHistoricalMessageData(data: MessageData): ChatStreamEvent
121
124
  }
122
125
  return null;
123
126
 
127
+ case MESSAGE_TYPE.GUIDE:
128
+ case MESSAGE_TYPE.SOURCES:
129
+ return decodeNatsChunk({ type: data.type, payload: data.payload });
130
+
124
131
  // Same completeness gate as the live decoder (`decodeNatsChunk`): a
125
132
  // persisted row without a question or without options is not a card the
126
133
  // user can answer, so it replays as nothing rather than as empty chrome.
@@ -591,6 +598,8 @@ export function processHistoricalMessages(
591
598
  // MAX persisted seq across the rows grouped into the current assistant turn
592
599
  // — carried onto the flushed message's streamSeq for per-role merge coverage.
593
600
  let currentAssistantStreamSeq: number | undefined;
601
+ let currentAssistantSources: ChatSource[] | undefined;
602
+ let currentAssistantRefs: ChatRef[] | undefined;
594
603
 
595
604
  /**
596
605
  * Flush the current assistant message to processedMessages.
@@ -608,6 +617,8 @@ export function processHistoricalMessages(
608
617
  authorType: assistantType,
609
618
  timestamp: currentAssistantTimestamp || new Date(),
610
619
  avatar: assistantAvatar,
620
+ ...(currentAssistantSources ? { sources: currentAssistantSources } : {}),
621
+ ...(currentAssistantRefs ? { refs: currentAssistantRefs } : {}),
611
622
  ...(currentAssistantStreamSeq !== undefined ? { streamSeq: currentAssistantStreamSeq } : {}),
612
623
  });
613
624
  accumulator.resetSegments();
@@ -623,6 +634,8 @@ export function processHistoricalMessages(
623
634
  currentAssistantTimestamp = null;
624
635
  lastAssistantId = null;
625
636
  currentAssistantStreamSeq = undefined;
637
+ currentAssistantSources = undefined;
638
+ currentAssistantRefs = undefined;
626
639
  };
627
640
 
628
641
  messages.forEach((msg, index) => {
@@ -689,6 +702,11 @@ export function processHistoricalMessages(
689
702
  messageDataArray.forEach(data => {
690
703
  const event = decodeHistoricalMessageData(data);
691
704
  if (!event) return;
705
+ if (event.type === 'sources') {
706
+ currentAssistantSources = mergeChatSources(currentAssistantSources, event.sources);
707
+ if (event.refs) currentAssistantRefs = mergeChatRefs(currentAssistantRefs, event.refs);
708
+ return;
709
+ }
692
710
  applyHistoryEvent(
693
711
  event,
694
712
  accumulator,
@@ -106,6 +106,7 @@ export function HoverDropdown({
106
106
  }: HoverDropdownProps) {
107
107
  const [open, setOpen] = useState(false);
108
108
  const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
109
+ const clickPinnedRef = useRef(false);
109
110
  const containerRef = useRef<HTMLDivElement>(null);
110
111
  const panelRef = useRef<HTMLDivElement>(null);
111
112
 
@@ -156,18 +157,22 @@ export function HoverDropdown({
156
157
  }, []);
157
158
 
158
159
  const hide = useCallback(() => {
160
+ if (clickPinnedRef.current) return;
159
161
  timeoutRef.current = setTimeout(() => setOpen(false), hideDelay);
160
162
  }, [hideDelay]);
161
163
 
162
164
  // Click handler — touch / mobile (no hover) + desktop users who
163
- // expect clicking a chip to open a menu. Toggles the dropdown state
164
- // so a second click closes it. `stopPropagation` prevents the click
165
+ // expect clicking a chip to open a menu. The first desktop click can arrive
166
+ // after mouse-enter already opened the panel; pin it open instead of blindly
167
+ // toggling it closed. A second click closes it. `stopPropagation` prevents the click
165
168
  // from bubbling to ancestor handlers (the chip's parent message
166
169
  // bubble shouldn't react to chip clicks).
167
170
  const toggle = useCallback((e: React.MouseEvent) => {
168
171
  e.stopPropagation();
169
172
  clearTimeout(timeoutRef.current);
170
- setOpen(prev => !prev);
173
+ const pinned = !clickPinnedRef.current;
174
+ clickPinnedRef.current = pinned;
175
+ setOpen(pinned);
171
176
  }, []);
172
177
 
173
178
  // Every hook is above this line: an `items` list that goes from empty to