@flamingo-stack/openframe-frontend-core 0.0.584-1973.5461.1 → 0.0.584

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 (162) hide show
  1. package/dist/chat-protocol/events.d.ts +2 -8
  2. package/dist/chat-protocol/events.d.ts.map +1 -1
  3. package/dist/chat-protocol/index.cjs +0 -188
  4. package/dist/chat-protocol/index.cjs.map +1 -1
  5. package/dist/chat-protocol/index.js +0 -188
  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/{chunk-U7MJV2V7.cjs → chunk-33DHEEMP.cjs} +33 -33
  9. package/dist/{chunk-U7MJV2V7.cjs.map → chunk-33DHEEMP.cjs.map} +1 -1
  10. package/dist/{chunk-23VBGHBL.js → chunk-3ELW4VML.js} +2 -2
  11. package/dist/{chunk-7KBKLN2Z.cjs → chunk-3ZA2OYRE.cjs} +3 -3
  12. package/dist/{chunk-7KBKLN2Z.cjs.map → chunk-3ZA2OYRE.cjs.map} +1 -1
  13. package/dist/{chunk-73XDYWYG.js → chunk-4T5KOD3G.js} +5 -5
  14. package/dist/{chunk-MI6PPL3P.js → chunk-5CJKZHWO.js} +5 -5
  15. package/dist/{chunk-E7Q24T4G.js → chunk-6BJYPG2V.js} +3 -3
  16. package/dist/{chunk-YXNMLGFI.cjs → chunk-7X5TCK4R.cjs} +10 -10
  17. package/dist/{chunk-YXNMLGFI.cjs.map → chunk-7X5TCK4R.cjs.map} +1 -1
  18. package/dist/{chunk-7DEGEI7G.js → chunk-EJKEDCTH.js} +6 -6
  19. package/dist/{chunk-XIV5VDWL.cjs → chunk-HCKEDTKJ.cjs} +10 -10
  20. package/dist/{chunk-XIV5VDWL.cjs.map → chunk-HCKEDTKJ.cjs.map} +1 -1
  21. package/dist/{chunk-EQGAYDTC.cjs → chunk-IIT5HWLY.cjs} +9 -9
  22. package/dist/{chunk-EQGAYDTC.cjs.map → chunk-IIT5HWLY.cjs.map} +1 -1
  23. package/dist/{chunk-7SMYBCYH.cjs → chunk-IKUSKKFZ.cjs} +540 -883
  24. package/dist/chunk-IKUSKKFZ.cjs.map +1 -0
  25. package/dist/{chunk-JKYALZB7.cjs → chunk-JAJQ5P2Q.cjs} +22 -22
  26. package/dist/{chunk-JKYALZB7.cjs.map → chunk-JAJQ5P2Q.cjs.map} +1 -1
  27. package/dist/{chunk-WAXL3E3B.cjs → chunk-JFKK3ZOA.cjs} +43 -43
  28. package/dist/{chunk-WAXL3E3B.cjs.map → chunk-JFKK3ZOA.cjs.map} +1 -1
  29. package/dist/{chunk-AWAHYAII.js → chunk-JVWRMLQZ.js} +2 -2
  30. package/dist/{chunk-PZ27ELFP.cjs → chunk-JWKAJTNA.cjs} +43 -33
  31. package/dist/chunk-JWKAJTNA.cjs.map +1 -0
  32. package/dist/{chunk-PISJGXXI.js → chunk-KP6VJP36.js} +3 -3
  33. package/dist/{chunk-NSDZAUJK.js → chunk-LNKULR4V.js} +2 -2
  34. package/dist/{chunk-AFQB4YL7.cjs → chunk-MEJFHD5N.cjs} +15 -15
  35. package/dist/{chunk-AFQB4YL7.cjs.map → chunk-MEJFHD5N.cjs.map} +1 -1
  36. package/dist/{chunk-5VHCSN44.js → chunk-MYEG5ZDT.js} +2 -2
  37. package/dist/{chunk-MMA3MTJD.cjs → chunk-P2VRLSAP.cjs} +21 -21
  38. package/dist/{chunk-MMA3MTJD.cjs.map → chunk-P2VRLSAP.cjs.map} +1 -1
  39. package/dist/{chunk-7Q34BVL7.cjs → chunk-Q66AMADB.cjs} +4 -4
  40. package/dist/{chunk-7Q34BVL7.cjs.map → chunk-Q66AMADB.cjs.map} +1 -1
  41. package/dist/{chunk-4ARYZTGV.cjs → chunk-QJR32JM2.cjs} +159 -159
  42. package/dist/{chunk-4ARYZTGV.cjs.map → chunk-QJR32JM2.cjs.map} +1 -1
  43. package/dist/{chunk-M4IIK3B7.js → chunk-RURZ6QLN.js} +3 -3
  44. package/dist/{chunk-TCEU6DCF.js → chunk-TLKHLGAB.js} +3 -3
  45. package/dist/{chunk-JCWUJHOD.js → chunk-TOUD3AGK.js} +23 -13
  46. package/dist/chunk-TOUD3AGK.js.map +1 -0
  47. package/dist/{chunk-CHI7GJ3K.cjs → chunk-UCNEQKIT.cjs} +5 -5
  48. package/dist/{chunk-CHI7GJ3K.cjs.map → chunk-UCNEQKIT.cjs.map} +1 -1
  49. package/dist/{chunk-ZRGBLM6W.cjs → chunk-VOAZU46B.cjs} +69 -69
  50. package/dist/{chunk-ZRGBLM6W.cjs.map → chunk-VOAZU46B.cjs.map} +1 -1
  51. package/dist/{chunk-GYJVLFK5.js → chunk-VZSTL5VG.js} +2 -2
  52. package/dist/{chunk-3CAC3AAE.js → chunk-XPJKQ63S.js} +3 -3
  53. package/dist/{chunk-KSC5LNSY.js → chunk-XWBND4G5.js} +187 -530
  54. package/dist/chunk-XWBND4G5.js.map +1 -0
  55. package/dist/{chunk-22DWXIV3.js → chunk-YFZDIEVC.js} +5 -5
  56. package/dist/{chunk-7J7XDGY6.cjs → chunk-ZHUBNWEX.cjs} +3 -3
  57. package/dist/{chunk-7J7XDGY6.cjs.map → chunk-ZHUBNWEX.cjs.map} +1 -1
  58. package/dist/components/case-studies/index.cjs +9 -9
  59. package/dist/components/case-studies/index.js +3 -3
  60. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  61. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  62. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  63. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  64. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +1 -4
  65. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  66. package/dist/components/chat/hooks/use-slash-commands.d.ts +3 -3
  67. package/dist/components/chat/hooks/use-slash-commands.d.ts.map +1 -1
  68. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +28 -2
  69. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  70. package/dist/components/chat/index.cjs +2 -2
  71. package/dist/components/chat/index.js +1 -1
  72. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
  73. package/dist/components/chat/tool-execution-display.d.ts.map +1 -1
  74. package/dist/components/chat/types/component.types.d.ts +0 -4
  75. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  76. package/dist/components/chat/types/message.types.d.ts +1 -35
  77. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  78. package/dist/components/chat/types/unified-chat-state.types.d.ts +3 -3
  79. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  80. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  81. package/dist/components/contact/index.cjs +4 -4
  82. package/dist/components/contact/index.js +3 -3
  83. package/dist/components/docs/index.cjs +7 -7
  84. package/dist/components/docs/index.js +6 -6
  85. package/dist/components/embeds/index.cjs +4 -4
  86. package/dist/components/embeds/index.js +3 -3
  87. package/dist/components/faq/index.cjs +4 -4
  88. package/dist/components/faq/index.js +3 -3
  89. package/dist/components/features/index.cjs +3 -3
  90. package/dist/components/features/index.js +2 -2
  91. package/dist/components/help-center-pages/index.cjs +23 -23
  92. package/dist/components/help-center-pages/index.js +14 -14
  93. package/dist/components/index.cjs +36 -36
  94. package/dist/components/index.js +13 -13
  95. package/dist/components/meeting-scheduler/index.cjs +21 -21
  96. package/dist/components/meeting-scheduler/index.js +2 -2
  97. package/dist/components/navigation/index.cjs +6 -6
  98. package/dist/components/navigation/index.js +5 -5
  99. package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
  100. package/dist/components/onboarding-guides/index.cjs +6 -6
  101. package/dist/components/onboarding-guides/index.js +5 -5
  102. package/dist/components/tickets/index.cjs +7 -7
  103. package/dist/components/tickets/index.js +6 -6
  104. package/dist/components/ui/hover-dropdown.d.ts.map +1 -1
  105. package/dist/components/ui/index.cjs +3 -3
  106. package/dist/components/ui/index.js +2 -2
  107. package/dist/index.cjs +7 -7
  108. package/dist/index.js +6 -6
  109. package/dist/utils/index.cjs.map +1 -1
  110. package/dist/utils/index.js.map +1 -1
  111. package/package.json +1 -1
  112. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +0 -130
  113. package/src/chat-protocol/events.ts +1 -9
  114. package/src/chat-protocol/nats-decoder.ts +0 -7
  115. package/src/components/chat/__tests__/chat-message-list.test.tsx +0 -70
  116. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +0 -86
  117. package/src/components/chat/chat-message-enhanced.tsx +13 -28
  118. package/src/components/chat/chat-message-list.tsx +22 -27
  119. package/src/components/chat/embeddable-chat.tsx +36 -55
  120. package/src/components/chat/entity-cards/dispatch.tsx +17 -86
  121. package/src/components/chat/hooks/use-nats-chat-adapter.ts +0 -7
  122. package/src/components/chat/hooks/use-slash-commands.ts +5 -9
  123. package/src/components/chat/hooks/use-sse-chat-adapter.ts +29 -2
  124. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +0 -56
  125. package/src/components/chat/stream/chat-stream-reducer.ts +0 -31
  126. package/src/components/chat/tool-execution-display.tsx +2 -5
  127. package/src/components/chat/types/component.types.ts +0 -4
  128. package/src/components/chat/types/message.types.ts +0 -39
  129. package/src/components/chat/types/unified-chat-state.types.ts +3 -4
  130. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +0 -57
  131. package/src/components/chat/utils/process-historical-messages.ts +1 -19
  132. package/src/components/navigation/navigation-sidebar-item.tsx +19 -9
  133. package/src/components/ui/hover-dropdown.tsx +3 -8
  134. package/dist/chat-protocol/source-metadata.d.ts +0 -5
  135. package/dist/chat-protocol/source-metadata.d.ts.map +0 -1
  136. package/dist/chunk-7SMYBCYH.cjs.map +0 -1
  137. package/dist/chunk-JCWUJHOD.js.map +0 -1
  138. package/dist/chunk-KSC5LNSY.js.map +0 -1
  139. package/dist/chunk-PZ27ELFP.cjs.map +0 -1
  140. package/dist/components/chat/utils/chat-source-metadata.d.ts +0 -5
  141. package/dist/components/chat/utils/chat-source-metadata.d.ts.map +0 -1
  142. package/src/chat-protocol/__tests__/source-metadata.test.ts +0 -74
  143. package/src/chat-protocol/source-metadata.ts +0 -202
  144. package/src/components/chat/__tests__/embeddable-chat-sources.test.tsx +0 -411
  145. package/src/components/chat/__tests__/tool-execution-display.test.tsx +0 -93
  146. package/src/components/chat/entity-cards/__tests__/dispatch-video-refs.test.tsx +0 -74
  147. package/src/components/chat/hooks/__tests__/use-slash-commands.test.ts +0 -64
  148. package/src/components/chat/utils/chat-source-metadata.ts +0 -19
  149. /package/dist/{chunk-23VBGHBL.js.map → chunk-3ELW4VML.js.map} +0 -0
  150. /package/dist/{chunk-73XDYWYG.js.map → chunk-4T5KOD3G.js.map} +0 -0
  151. /package/dist/{chunk-MI6PPL3P.js.map → chunk-5CJKZHWO.js.map} +0 -0
  152. /package/dist/{chunk-E7Q24T4G.js.map → chunk-6BJYPG2V.js.map} +0 -0
  153. /package/dist/{chunk-7DEGEI7G.js.map → chunk-EJKEDCTH.js.map} +0 -0
  154. /package/dist/{chunk-AWAHYAII.js.map → chunk-JVWRMLQZ.js.map} +0 -0
  155. /package/dist/{chunk-PISJGXXI.js.map → chunk-KP6VJP36.js.map} +0 -0
  156. /package/dist/{chunk-NSDZAUJK.js.map → chunk-LNKULR4V.js.map} +0 -0
  157. /package/dist/{chunk-5VHCSN44.js.map → chunk-MYEG5ZDT.js.map} +0 -0
  158. /package/dist/{chunk-M4IIK3B7.js.map → chunk-RURZ6QLN.js.map} +0 -0
  159. /package/dist/{chunk-TCEU6DCF.js.map → chunk-TLKHLGAB.js.map} +0 -0
  160. /package/dist/{chunk-GYJVLFK5.js.map → chunk-VZSTL5VG.js.map} +0 -0
  161. /package/dist/{chunk-3CAC3AAE.js.map → chunk-XPJKQ63S.js.map} +0 -0
  162. /package/dist/{chunk-22DWXIV3.js.map → chunk-YFZDIEVC.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.584-1973.5461.1",
3
+ "version": "0.0.584",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -329,133 +329,3 @@ describe('decodeNatsChunk — seq envelope', () => {
329
329
  }).toMatchSnapshot();
330
330
  });
331
331
  });
332
-
333
- describe('decodeNatsChunk — Mingo source metadata', () => {
334
- it.each(['GUIDE', 'SOURCES'] as const)('decodes %s payloads into safe sources and playable refs', type => {
335
- expect(
336
- decodeNatsChunk({
337
- type,
338
- streamSeq: 81,
339
- payload: {
340
- sources: [
341
- {
342
- index: 1,
343
- name: 'Install agent',
344
- path: 'docs/install.md',
345
- documentType: 'markdown',
346
- externalUrl: 'https://docs.example.test/install',
347
- },
348
- {
349
- index: 2,
350
- name: 'Unsafe source',
351
- path: 'docs/unsafe.md',
352
- documentType: 'markdown',
353
- externalUrl: 'javascript:alert(1)',
354
- },
355
- {
356
- index: 1,
357
- name: 'Duplicate',
358
- path: 'docs/duplicate.md',
359
- documentType: 'markdown',
360
- },
361
- { index: 3, name: '', path: 'docs/empty.md', documentType: 'markdown' },
362
- ],
363
- videos: [
364
- {
365
- ref: '[card://video:MdFJNoJeqZQ]',
366
- title: 'Install the agent',
367
- url: 'https://www.youtube.com/watch?v=MdFJNoJeqZQ',
368
- sourceRepo: 'embedded-videos',
369
- metadata: { youtubeUrl: 'MdFJNoJeqZQ' },
370
- },
371
- {
372
- ref: '[card://video:mux-9b6586b494]',
373
- title: 'Community demo',
374
- url: 'https://stream.mux.com/playback-id.m3u8',
375
- metadata: { videoUrl: 'javascript:alert(1)' },
376
- },
377
- {
378
- ref: '[card://video:mp4-1b2047dc1b]',
379
- title: 'Product overview',
380
- url: 'https://cdn.example.test/product-overview.mp4',
381
- metadata: { videoUrl: 'https://cdn.example.test/product-overview.mp4' },
382
- },
383
- {
384
- ref: '[card://video:MdFJNoJeqZQ]',
385
- title: 'Duplicate video',
386
- url: 'https://cdn.example.test/duplicate.mp4',
387
- },
388
- ],
389
- cards: [
390
- {
391
- ref: '[card://markdown:install-agent]',
392
- entityType: 'markdown',
393
- entityId: 'install-agent',
394
- },
395
- ],
396
- },
397
- }),
398
- ).toEqual({
399
- type: 'sources',
400
- seq: 81,
401
- sources: [
402
- {
403
- index: 1,
404
- name: 'Install agent',
405
- path: 'docs/install.md',
406
- documentType: 'markdown',
407
- externalUrl: 'https://docs.example.test/install',
408
- },
409
- {
410
- index: 2,
411
- name: 'Unsafe source',
412
- path: 'docs/unsafe.md',
413
- documentType: 'markdown',
414
- },
415
- ],
416
- refs: [
417
- {
418
- type: 'video',
419
- id: 'MdFJNoJeqZQ',
420
- title: 'Install the agent',
421
- url: 'https://www.youtube.com/watch?v=MdFJNoJeqZQ',
422
- sourceRepo: 'embedded-videos',
423
- metadata: { youtubeUrl: 'MdFJNoJeqZQ' },
424
- },
425
- {
426
- type: 'video',
427
- id: 'mux-9b6586b494',
428
- title: 'Community demo',
429
- url: 'https://stream.mux.com/playback-id.m3u8',
430
- metadata: { videoUrl: 'https://stream.mux.com/playback-id.m3u8' },
431
- },
432
- {
433
- type: 'video',
434
- id: 'mp4-1b2047dc1b',
435
- title: 'Product overview',
436
- url: 'https://cdn.example.test/product-overview.mp4',
437
- metadata: { videoUrl: 'https://cdn.example.test/product-overview.mp4' },
438
- },
439
- {
440
- type: 'markdown',
441
- id: 'install-agent',
442
- title: 'install-agent',
443
- url: null,
444
- },
445
- ],
446
- });
447
- });
448
-
449
- it('drops a metadata payload with no valid sources or refs', () => {
450
- expect(
451
- decodeNatsChunk({
452
- type: 'SOURCES',
453
- payload: {
454
- sources: [{ index: 0, name: 'Invalid', path: '', documentType: '' }],
455
- videos: [{ ref: 'https://example.test/video' }],
456
- cards: [{ ref: '[CARD://blog:wrong-case]' }],
457
- },
458
- }),
459
- ).toBeNull();
460
- });
461
- });
@@ -12,8 +12,7 @@
12
12
  // rows straight to the accumulator, so restating them here would be two
13
13
  // declarations of one wire contract. Type-only import from a React-free
14
14
  // module; `nats-decoder.ts` already depends on the same file for MESSAGE_TYPE.
15
- import type { ChatRef } from '../components/chat/chat-ref.types';
16
- import type { AskOptionData, ChatSource } from '../components/chat/types/message.types';
15
+ import type { AskOptionData } from '../components/chat/types/message.types';
17
16
  // The wire-frame shapes these events carry through are defined ONCE in
18
17
  // `./frames.ts` — reuse them here rather than restating their fields.
19
18
  import type { ApprovalRequestField, DecisionResolvedFrame, UsageTelemetry } from './frames';
@@ -241,12 +240,6 @@ export interface ChatMetadataEvent extends ChatStreamEventBase {
241
240
  };
242
241
  }
243
242
 
244
- export interface SourcesEvent extends ChatStreamEventBase {
245
- type: 'sources';
246
- sources: ChatSource[];
247
- refs?: ChatRef[];
248
- }
249
-
250
243
  /** SSE usage frames — raw wire keys (snake_case) preserved. */
251
244
  export interface UsageEvent extends ChatStreamEventBase {
252
245
  type: 'usage';
@@ -369,7 +362,6 @@ export type ChatStreamEvent =
369
362
  | TicketEscalatedEvent
370
363
  | TicketEventEvent
371
364
  | ChatMetadataEvent
372
- | SourcesEvent
373
365
  | UsageEvent
374
366
  | TokenUsageEvent
375
367
  | CompactionEvent
@@ -20,7 +20,6 @@ import { MESSAGE_TYPE } from '../components/chat/types/message.types';
20
20
  import type { AskOptionData } from '../components/chat/types/message.types';
21
21
  import { ESCALATION_STATE, escalationResolvedStatus } from './events';
22
22
  import type { ApprovalToolCall, ChatStreamEvent } from './events';
23
- import { sourceMetadataEvent } from './source-metadata';
24
23
 
25
24
  /** Minimal structural view of a NATS chunk (see `ChunkData` in
26
25
  * `src/components/chat/types/network.types.ts`). */
@@ -126,12 +125,6 @@ export function decodeNatsChunk(chunk: unknown): ChatStreamEvent | null {
126
125
  }
127
126
  return null;
128
127
 
129
- case MESSAGE_TYPE.GUIDE:
130
- case MESSAGE_TYPE.SOURCES: {
131
- const event = sourceMetadataEvent(data.payload);
132
- return event ? { ...event, ...seq } : null;
133
- }
134
-
135
128
  // An ask card is only an ask card with something to pick: a question and at
136
129
  // least one option. Anything less is dropped rather than rendered as an
137
130
  // empty card the user can't answer — the backend already falls back to a
@@ -146,76 +146,6 @@ describe('ChatMessageList force-scroll decisions', () => {
146
146
  });
147
147
  });
148
148
 
149
- describe('ChatMessageList message footers', () => {
150
- const renderSourceFooter = (message: Message) =>
151
- message.sources?.map(source => <span key={`${message.id}-${source.index}`}>{source.name}</span>);
152
-
153
- it('keeps a sourced assistant footer after a later plain assistant message', () => {
154
- const sourcedAssistant: Message = {
155
- ...msg('a1', 'assistant'),
156
- sources: [
157
- {
158
- index: 1,
159
- name: 'Windows installation guide',
160
- path: '/docs/windows',
161
- documentType: 'openframe-docs',
162
- },
163
- ],
164
- };
165
- const plainAssistant = msg('a2', 'assistant');
166
-
167
- render(
168
- <ChatMessageList
169
- dialogId="d1"
170
- messages={[sourcedAssistant, plainAssistant]}
171
- renderAfterMessage={renderSourceFooter}
172
- />,
173
- );
174
-
175
- const sourceFooter = screen.getByText('Windows installation guide');
176
- expect(sourceFooter).toBeInTheDocument();
177
- expect(screen.getByText('body of a1')).toAppearBefore(sourceFooter);
178
- });
179
-
180
- it('renders separate source names when citation indices restart on each turn', () => {
181
- const firstAssistant: Message = {
182
- ...msg('a1', 'assistant'),
183
- sources: [
184
- {
185
- index: 1,
186
- name: 'First turn source',
187
- path: '/docs/first',
188
- documentType: 'openframe-docs',
189
- },
190
- ],
191
- };
192
- const secondAssistant: Message = {
193
- ...msg('a2', 'assistant'),
194
- sources: [
195
- {
196
- index: 1,
197
- name: 'Second turn source',
198
- path: '/docs/second',
199
- documentType: 'openframe-docs',
200
- },
201
- ],
202
- };
203
-
204
- render(
205
- <ChatMessageList
206
- dialogId="d1"
207
- messages={[firstAssistant, secondAssistant]}
208
- renderAfterMessage={renderSourceFooter}
209
- />,
210
- );
211
-
212
- const firstSourceFooter = screen.getByText('First turn source');
213
- const secondSourceFooter = screen.getByText('Second turn source');
214
- expect(screen.getByText('body of a1')).toAppearBefore(firstSourceFooter);
215
- expect(screen.getByText('body of a2')).toAppearBefore(secondSourceFooter);
216
- });
217
- });
218
-
219
149
  // The library's own `isAtBottom` lock is lost silently when the scroller's
220
150
  // box changes (source chips mounting below it) or when a card settling out
221
151
  // of its skeleton produces a resize-driven scroll the library reads as a
@@ -20,7 +20,6 @@
20
20
  import { render } from '@testing-library/react';
21
21
  import type React from 'react';
22
22
  import { describe, it, expect, vi } from 'vitest';
23
- import type { ChatRef } from '../chat-ref.types';
24
23
  import type { MessageSegment } from '../types';
25
24
 
26
25
  /** Every `componentOverrides` / `additionalRemarkPlugins` identity the
@@ -130,89 +129,4 @@ describe('ChatMessageEnhanced — override identity across a streamed turn', ()
130
129
  expect(seenRefs).toEqual([{ type: 'blog', id: 'abc' }]);
131
130
  expect(view.container.textContent).toContain('card:abc');
132
131
  });
133
-
134
- it('enriches exact video markers with YouTube, hashed Mux, and hashed MP4 metadata', () => {
135
- const seenRefs: Array<{ id: string; metadata?: Record<string, unknown> }> = [];
136
- const refs: ChatRef[] = [
137
- {
138
- type: 'video',
139
- id: 'MdFJNoJeqZQ',
140
- title: 'Install the agent',
141
- url: 'https://www.youtube.com/watch?v=MdFJNoJeqZQ',
142
- metadata: { youtubeUrl: 'MdFJNoJeqZQ' },
143
- },
144
- {
145
- type: 'video',
146
- id: 'mux-9b6586b494',
147
- title: 'Community demo',
148
- url: 'https://stream.mux.com/playback-id.m3u8',
149
- metadata: { videoUrl: 'https://stream.mux.com/playback-id.m3u8' },
150
- },
151
- {
152
- type: 'video',
153
- id: 'mp4-1b2047dc1b',
154
- title: 'Product overview',
155
- url: 'https://cdn.example.test/product-overview.mp4',
156
- metadata: { videoUrl: 'https://cdn.example.test/product-overview.mp4' },
157
- },
158
- ];
159
-
160
- render(
161
- <ChatMessageEnhanced
162
- role="assistant"
163
- content={[
164
- {
165
- type: 'text',
166
- text: 'Watch [card://video:MdFJNoJeqZQ] [card://video:mux-9b6586b494] [card://video:mp4-1b2047dc1b].',
167
- },
168
- ]}
169
- refs={refs}
170
- renderEntityCard={ref => {
171
- seenRefs.push({ id: ref.id, metadata: ref.metadata });
172
- return <div data-testid={`card-${ref.id}`}>{ref.title}</div>;
173
- }}
174
- NavLinkAnchor={NavLinkAnchor}
175
- />,
176
- );
177
-
178
- expect(seenRefs).toEqual([
179
- { id: 'MdFJNoJeqZQ', metadata: { youtubeUrl: 'MdFJNoJeqZQ' } },
180
- { id: 'mux-9b6586b494', metadata: { videoUrl: 'https://stream.mux.com/playback-id.m3u8' } },
181
- { id: 'mp4-1b2047dc1b', metadata: { videoUrl: 'https://cdn.example.test/product-overview.mp4' } },
182
- ]);
183
- });
184
-
185
- it('rerenders a completed marker when its enriched metadata arrives later', () => {
186
- const seenMetadata: Array<Record<string, unknown> | undefined> = [];
187
- const content: MessageSegment[] = [{ type: 'text', text: 'Watch [card://video:mux-9b6586b494].' }];
188
- const props = {
189
- role: 'assistant' as const,
190
- content,
191
- renderEntityCard: (ref: ChatRef) => {
192
- seenMetadata.push(ref.metadata);
193
- return <div>video</div>;
194
- },
195
- NavLinkAnchor,
196
- };
197
- const view = render(<ChatMessageEnhanced {...props} />);
198
-
199
- view.rerender(
200
- <ChatMessageEnhanced
201
- {...props}
202
- refs={[
203
- {
204
- type: 'video',
205
- id: 'mux-9b6586b494',
206
- title: 'Community demo',
207
- url: 'https://stream.mux.com/playback-id.m3u8',
208
- metadata: { videoUrl: 'https://stream.mux.com/playback-id.m3u8' },
209
- },
210
- ]}
211
- />,
212
- );
213
-
214
- expect(seenMetadata[seenMetadata.length - 1]).toEqual({
215
- videoUrl: 'https://stream.mux.com/playback-id.m3u8',
216
- });
217
- });
218
132
  });
@@ -103,7 +103,6 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
103
103
  renderContextItem,
104
104
  renderMention,
105
105
  renderEntityCard,
106
- refs,
107
106
  onAskSelect,
108
107
  NavLinkAnchor,
109
108
  ...props
@@ -128,11 +127,6 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
128
127
  // literal `[card://...]` URL.
129
128
  const hasMarkerSupport = !!renderEntityCard;
130
129
 
131
- const refsByKey = useMemo(
132
- () => new Map((refs ?? []).map(reference => [`${reference.type}:${reference.id}`, reference])),
133
- [refs],
134
- );
135
-
136
130
  const segments = useMemo(() => normalizeContent(content), [content]);
137
131
 
138
132
  const askRuns = useMemo(() => groupAskRuns(segments), [segments]);
@@ -186,14 +180,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
186
180
  // (and its open menu) survives across chunks. Invalidated per key when the
187
181
  // render fn identity changes.
188
182
  const renderedCardNodeCache = useRef(
189
- new Map<
190
- string,
191
- {
192
- render: ((ref: ChatRef) => React.ReactNode) | undefined;
193
- reference: ChatRef;
194
- node: React.ReactNode;
195
- }
196
- >(),
183
+ new Map<string, { render: ((ref: ChatRef) => React.ReactNode) | undefined; node: React.ReactNode }>(),
197
184
  );
198
185
 
199
186
  /**
@@ -255,17 +242,10 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
255
242
  // menu / re-fetching) on every stream chunk. Also dedups the same
256
243
  // key emitted twice within one message.
257
244
  let entry = cache.get(key);
258
- const enrichedRef = refsByKey.get(key);
259
- const refForRender: ChatRef = enrichedRef ?? {
260
- type: cardType,
261
- id: cardId,
262
- title: cardId,
263
- url: null,
264
- };
265
- if (!entry || entry.render !== render || entry.reference !== refForRender) {
266
- // Prefer same-turn SOURCES metadata when available. Otherwise
267
- // cards hydrate by id from the host's per-object APIs, so a
268
- // minimal {type, id} descriptor still mounts the loader.
245
+ if (!entry || entry.render !== render) {
246
+ // The marker is the ONLY data on the wire: cards hydrate by id
247
+ // from the host's per-object APIs, so a minimal {type, id}
248
+ // descriptor is all the renderer needs to mount the loader.
269
249
  // For types with nothing to fetch the host's render() returns
270
250
  // null and we fall through to the bare-cardId fallback in the
271
251
  // `<a card://…>` override below.
@@ -277,7 +257,13 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
277
257
  // the id rather than `undefined`) and `url` to null (matches
278
258
  // the no-link semantics fetch-mode cards rely on — they
279
259
  // resolve their own URL after fetch).
280
- entry = { render, reference: refForRender, node: render(refForRender) };
260
+ const refForRender: ChatRef = {
261
+ type: cardType,
262
+ id: cardId,
263
+ title: cardId,
264
+ url: null,
265
+ };
266
+ entry = { render, node: render(refForRender) };
281
267
  cache.set(key, entry);
282
268
  }
283
269
  const rendered = entry.node;
@@ -340,7 +326,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
340
326
  }
341
327
  });
342
328
  return { inlineByKey, partsBySegment, usedKeys };
343
- }, [hasMarkerSupport, refsByKey, renderEntityCard, segments]);
329
+ }, [hasMarkerSupport, renderEntityCard, segments]);
344
330
 
345
331
  // Drop cached nodes for markers no longer present so the cache can't grow
346
332
  // unbounded as a long message's markers change. Deliberately an EFFECT,
@@ -730,7 +716,6 @@ const MemoizedChatMessageEnhanced = memo(ChatMessageEnhanced, (prevProps, nextPr
730
716
  // equality holds across streaming chunks.
731
717
  prevProps.renderMention === nextProps.renderMention &&
732
718
  prevProps.renderEntityCard === nextProps.renderEntityCard &&
733
- prevProps.refs === nextProps.refs &&
734
719
  // Same stability contract as the renderers above: hosts pass a `useCallback`
735
720
  // (EmbeddableChat passes its memoized `handleSend`), so this holds across
736
721
  // streaming chunks instead of re-rendering every ask card per chunk.
@@ -1,7 +1,6 @@
1
1
  'use client';
2
2
 
3
3
  import {
4
- Fragment,
5
4
  useRef,
6
5
  useState,
7
6
  useCallback,
@@ -139,7 +138,6 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
139
138
  renderContextItem,
140
139
  renderMention,
141
140
  onAskSelect,
142
- renderAfterMessage,
143
141
  NavLinkAnchor,
144
142
  ...props
145
143
  },
@@ -988,31 +986,28 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
988
986
  (!Array.isArray(message.content) || message.content.every(s => s.type === 'text'));
989
987
  if (isEmptyPendingTurn) return null;
990
988
  return (
991
- <Fragment key={message.id}>
992
- <ChatMessageEnhanced
993
- ref={getRegisterMessageEl(message.id)}
994
- role={message.role}
995
- name={message.name}
996
- content={message.content}
997
- timestamp={message.timestamp}
998
- isTyping={index === messages.length - 1 && isTyping && message.role === 'assistant'}
999
- avatar={showAvatars ? message.avatar : null}
1000
- showAvatar={showAvatars}
1001
- assistantType={message.assistantType || assistantType}
1002
- approvalVariant={approvalVariant}
1003
- authorType={message.authorType}
1004
- assistantIcon={message.role !== 'user' ? assistantIcon : undefined}
1005
- contextItems={message.contextItems}
1006
- resolveContextIcon={resolveContextIcon}
1007
- renderContextItem={renderContextItem}
1008
- renderMention={renderMention}
1009
- renderEntityCard={renderEntityCard}
1010
- refs={message.refs}
1011
- onAskSelect={index > lastUserMessageIndex ? onAskSelect : undefined}
1012
- NavLinkAnchor={NavLinkAnchor}
1013
- />
1014
- {renderAfterMessage?.(message, index)}
1015
- </Fragment>
989
+ <ChatMessageEnhanced
990
+ key={message.id}
991
+ ref={getRegisterMessageEl(message.id)}
992
+ role={message.role}
993
+ name={message.name}
994
+ content={message.content}
995
+ timestamp={message.timestamp}
996
+ isTyping={index === messages.length - 1 && isTyping && message.role === 'assistant'}
997
+ avatar={showAvatars ? message.avatar : null}
998
+ showAvatar={showAvatars}
999
+ assistantType={message.assistantType || assistantType}
1000
+ approvalVariant={approvalVariant}
1001
+ authorType={message.authorType}
1002
+ assistantIcon={message.role !== 'user' ? assistantIcon : undefined}
1003
+ contextItems={message.contextItems}
1004
+ resolveContextIcon={resolveContextIcon}
1005
+ renderContextItem={renderContextItem}
1006
+ renderMention={renderMention}
1007
+ renderEntityCard={renderEntityCard}
1008
+ onAskSelect={index > lastUserMessageIndex ? onAskSelect : undefined}
1009
+ NavLinkAnchor={NavLinkAnchor}
1010
+ />
1016
1011
  );
1017
1012
  })}
1018
1013
  </div>
@@ -808,14 +808,13 @@ function SourceChips({
808
808
  const [expanded, setExpanded] = useState(false);
809
809
 
810
810
  if (cited.length === 0) {
811
- const visible = expanded ? uncited : uncited.slice(0, FALLBACK_TOP_RETRIEVED);
812
- const hiddenCount = Math.max(0, uncited.length - FALLBACK_TOP_RETRIEVED);
813
- if (visible.length === 0) return null;
811
+ const fallback = uncited.slice(0, FALLBACK_TOP_RETRIEVED);
812
+ if (fallback.length === 0) return null;
814
813
  return (
815
814
  <div className="mt-2 flex flex-col gap-1.5 border-t border-ods-border pt-2">
816
815
  <span className="uppercase text-ods-text-muted text-h6">Top retrieved sources</span>
817
- <div className={`flex flex-wrap gap-1.5 ${expanded ? 'max-h-[200px] overflow-y-auto overscroll-contain' : ''}`}>
818
- {visible.map(src => (
816
+ <div className="flex flex-wrap gap-1.5">
817
+ {fallback.map(src => (
819
818
  <SourceChip
820
819
  key={src.index}
821
820
  src={src}
@@ -825,16 +824,6 @@ function SourceChips({
825
824
  onDiscuss={onDiscuss}
826
825
  />
827
826
  ))}
828
- {hiddenCount > 0 && (
829
- <button
830
- onClick={() => setExpanded(!expanded)}
831
- className="inline-flex cursor-pointer items-center gap-1 rounded border border-ods-accent bg-ods-card px-2 py-0.5 text-ods-accent transition-colors text-h6 hover:bg-ods-accent/10"
832
- aria-expanded={expanded}
833
- aria-label={expanded ? 'Show fewer sources' : `Show ${hiddenCount} additional retrieved sources`}
834
- >
835
- {expanded ? 'Show less' : `+${hiddenCount} more retrieved ${hiddenCount === 1 ? 'source' : 'sources'}`}
836
- </button>
837
- )}
838
827
  </div>
839
828
  </div>
840
829
  );
@@ -1590,10 +1579,6 @@ function EmbeddableChatInner({
1590
1579
  // explicitly — see `Message.hidden` and `chat-message-list`'s skip.
1591
1580
  ...(m.hidden ? { hidden: true } : {}),
1592
1581
  ...(m.scrollAnchor ? { scrollAnchor: m.scrollAnchor } : {}),
1593
- // Preserve same-turn rich metadata. Source chips and entity-card
1594
- // renderers consume it after this raw-to-Message projection.
1595
- ...(m.sources && m.sources.length > 0 ? { sources: m.sources } : {}),
1596
- ...(m.refs && m.refs.length > 0 ? { refs: m.refs } : {}),
1597
1582
  // Forward attached context items so the user bubble renders its chips.
1598
1583
  ...(m.contextItems && m.contextItems.length > 0 ? { contextItems: m.contextItems } : {}),
1599
1584
  };
@@ -1844,41 +1829,24 @@ function EmbeddableChatInner({
1844
1829
  return out;
1845
1830
  }, [commandsById, enabledSet]);
1846
1831
 
1847
- const renderSourcesAfterMessage = useCallback(
1848
- (message: Message, index: number) => {
1849
- if (message.role !== 'assistant' || !message.sources?.length) return null;
1850
- if (chatLoading && index === messages.length - 1) return null;
1851
- const content =
1852
- typeof message.content === 'string'
1853
- ? message.content
1854
- : message.content
1855
- .filter(segment => segment.type === 'text')
1856
- .map(segment => segment.text)
1857
- .join('\n');
1858
- const citationOrder = [...content.matchAll(/\[(\d+)\]/g)].map(m => parseInt(m[1], 10));
1859
- const seenOrder = new Map<number, number>();
1860
- citationOrder.forEach(idx => {
1861
- if (!seenOrder.has(idx)) seenOrder.set(idx, seenOrder.size);
1862
- });
1863
- const cited = message.sources
1864
- .filter(s => seenOrder.has(s.index))
1865
- .sort((a, b) => (seenOrder.get(a.index) ?? 0) - (seenOrder.get(b.index) ?? 0));
1866
- const uncited = message.sources.filter(s => !seenOrder.has(s.index));
1867
- return (
1868
- <div className="flex-shrink-0 pb-2">
1869
- <SourceChips
1870
- cited={cited}
1871
- uncited={uncited}
1872
- baseRoute={resolvedBaseRoute}
1873
- chipBasePlatform={chipBasePlatform}
1874
- onClose={handleNavigationClose}
1875
- onDiscuss={discussRef}
1876
- />
1877
- </div>
1878
- );
1879
- },
1880
- [chatLoading, messages.length, resolvedBaseRoute, chipBasePlatform, handleNavigationClose, discussRef],
1881
- );
1832
+ // Find sources for the last assistant message; split into cited / uncited.
1833
+ const lastAssistantMsg = [...rawMessages].reverse().find(m => m.role === 'assistant');
1834
+ const lastSources = useMemo(() => {
1835
+ if (chatLoading) return undefined;
1836
+ const sources = lastAssistantMsg?.sources;
1837
+ if (!sources || sources.length === 0) return undefined;
1838
+ const content = lastAssistantMsg?.content || '';
1839
+ const citationOrder = [...content.matchAll(/\[(\d+)\]/g)].map(m => parseInt(m[1], 10));
1840
+ const seenOrder = new Map<number, number>();
1841
+ citationOrder.forEach(idx => {
1842
+ if (!seenOrder.has(idx)) seenOrder.set(idx, seenOrder.size);
1843
+ });
1844
+ const cited = sources
1845
+ .filter(s => seenOrder.has(s.index))
1846
+ .sort((a, b) => (seenOrder.get(a.index) ?? 0) - (seenOrder.get(b.index) ?? 0));
1847
+ const uncited = sources.filter(s => !seenOrder.has(s.index));
1848
+ return { cited, uncited };
1849
+ }, [lastAssistantMsg, chatLoading]);
1882
1850
 
1883
1851
  // Host node for in-panel Radix portals (see the body wrapper below).
1884
1852
  const [portalHost, setPortalHost] = useState<HTMLDivElement | null>(null);
@@ -2422,7 +2390,6 @@ function EmbeddableChatInner({
2422
2390
  resolveContextIcon={resolveContextIcon}
2423
2391
  renderContextItem={renderContextItem}
2424
2392
  renderMention={renderMention}
2425
- renderAfterMessage={renderSourcesAfterMessage}
2426
2393
  // Gated on `chatLoading` for the same reason the composer
2427
2394
  // is: no second send while a turn is in flight. Passive
2428
2395
  // demo hosts (previewMode) stay read-only.
@@ -2460,6 +2427,20 @@ function EmbeddableChatInner({
2460
2427
  onLoadMore={loadMoreMessages}
2461
2428
  />
2462
2429
  )}
2430
+ {lastSources &&
2431
+ (lastSources.cited.length > 0 || lastSources.uncited.length > 0) &&
2432
+ !chatLoading && (
2433
+ <div className="flex-shrink-0 pb-2">
2434
+ <SourceChips
2435
+ cited={lastSources.cited}
2436
+ uncited={lastSources.uncited}
2437
+ baseRoute={resolvedBaseRoute}
2438
+ chipBasePlatform={chipBasePlatform}
2439
+ onClose={handleNavigationClose}
2440
+ onDiscuss={discussRef}
2441
+ />
2442
+ </div>
2443
+ )}
2463
2444
  </div>
2464
2445
  ) : activeMode === 'mingo' ? (
2465
2446
  /* Figma node 7532:222444 — default (Mingo-mode) empty state: