@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.
- package/dist/chat-protocol/events.d.ts +8 -2
- package/dist/chat-protocol/events.d.ts.map +1 -1
- package/dist/chat-protocol/index.cjs +188 -0
- package/dist/chat-protocol/index.cjs.map +1 -1
- package/dist/chat-protocol/index.js +188 -0
- package/dist/chat-protocol/index.js.map +1 -1
- package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
- package/dist/chat-protocol/source-metadata.d.ts +5 -0
- package/dist/chat-protocol/source-metadata.d.ts.map +1 -0
- package/dist/{chunk-GKPY7VLK.js → chunk-47NIV4FC.js} +4 -4
- package/dist/{chunk-D7YISHXC.js → chunk-4MVGFYRE.js} +534 -190
- package/dist/chunk-4MVGFYRE.js.map +1 -0
- package/dist/{chunk-I5CJJFEJ.js → chunk-4VDGIODG.js} +3 -3
- package/dist/{chunk-PAUJQV3C.cjs → chunk-627JVTID.cjs} +15 -15
- package/dist/{chunk-PAUJQV3C.cjs.map → chunk-627JVTID.cjs.map} +1 -1
- package/dist/{chunk-3OCYNMZ5.js → chunk-CQNILR7U.js} +3 -3
- package/dist/{chunk-BONXSRHZ.cjs → chunk-DPQGO4PH.cjs} +21 -21
- package/dist/{chunk-BONXSRHZ.cjs.map → chunk-DPQGO4PH.cjs.map} +1 -1
- package/dist/{chunk-A6X7EXP6.cjs → chunk-DVAAXDZV.cjs} +10 -10
- package/dist/{chunk-A6X7EXP6.cjs.map → chunk-DVAAXDZV.cjs.map} +1 -1
- package/dist/{chunk-CN36E4PK.js → chunk-EJEHIAOV.js} +2 -2
- package/dist/{chunk-G45RGWHF.cjs → chunk-EKKR5ZFB.cjs} +887 -543
- package/dist/chunk-EKKR5ZFB.cjs.map +1 -0
- package/dist/{chunk-IYDHV2CS.cjs → chunk-FIEOWSXH.cjs} +43 -43
- package/dist/{chunk-IYDHV2CS.cjs.map → chunk-FIEOWSXH.cjs.map} +1 -1
- package/dist/{chunk-CJOWLRUU.js → chunk-G4ND6ZZK.js} +2 -2
- package/dist/{chunk-KWTV3KGY.cjs → chunk-GDHQTVBP.cjs} +9 -9
- package/dist/{chunk-KWTV3KGY.cjs.map → chunk-GDHQTVBP.cjs.map} +1 -1
- package/dist/{chunk-ADANP3ZA.cjs → chunk-GOHHFX4O.cjs} +33 -33
- package/dist/{chunk-ADANP3ZA.cjs.map → chunk-GOHHFX4O.cjs.map} +1 -1
- package/dist/{chunk-KVNGYFFP.js → chunk-GU5TWGAJ.js} +3 -3
- package/dist/{chunk-EGYX55HT.cjs → chunk-HQKWJFFX.cjs} +159 -159
- package/dist/{chunk-EGYX55HT.cjs.map → chunk-HQKWJFFX.cjs.map} +1 -1
- package/dist/{chunk-NXZC6UTN.js → chunk-HZ6VUTLA.js} +2 -2
- package/dist/{chunk-WUCYI3BC.cjs → chunk-KH5KDKCE.cjs} +24 -24
- package/dist/{chunk-WUCYI3BC.cjs.map → chunk-KH5KDKCE.cjs.map} +1 -1
- package/dist/{chunk-JDINUWND.cjs → chunk-MQFA5DXU.cjs} +69 -69
- package/dist/{chunk-JDINUWND.cjs.map → chunk-MQFA5DXU.cjs.map} +1 -1
- package/dist/{chunk-5CHW7AMP.cjs → chunk-NHW2AM2X.cjs} +10 -10
- package/dist/{chunk-5CHW7AMP.cjs.map → chunk-NHW2AM2X.cjs.map} +1 -1
- package/dist/{chunk-KK3XU6YJ.cjs → chunk-NIHPN72N.cjs} +4 -4
- package/dist/{chunk-KK3XU6YJ.cjs.map → chunk-NIHPN72N.cjs.map} +1 -1
- package/dist/{chunk-EGUU3QCU.cjs → chunk-NNRRRWCD.cjs} +3 -3
- package/dist/{chunk-EGUU3QCU.cjs.map → chunk-NNRRRWCD.cjs.map} +1 -1
- package/dist/{chunk-4ZKAMPBX.js → chunk-OAY342OE.js} +2 -2
- package/dist/{chunk-TOYHZHTK.js → chunk-PDFH7CXB.js} +2 -2
- package/dist/{chunk-NIDPX52C.js → chunk-QIHBS5PM.js} +5 -5
- package/dist/{chunk-CNKVEVUN.js → chunk-SPOIDC23.js} +5 -5
- package/dist/{chunk-MENQLD57.cjs → chunk-T5FJOWHA.cjs} +5 -5
- package/dist/{chunk-MENQLD57.cjs.map → chunk-T5FJOWHA.cjs.map} +1 -1
- package/dist/{chunk-5FNWOF46.js → chunk-WZZSFOWG.js} +6 -6
- package/dist/{chunk-FVRRYJ6P.cjs → chunk-XIKSZSHC.cjs} +22 -22
- package/dist/{chunk-FVRRYJ6P.cjs.map → chunk-XIKSZSHC.cjs.map} +1 -1
- package/dist/{chunk-C6RJRSIM.cjs → chunk-XTMNO7MC.cjs} +3 -3
- package/dist/{chunk-C6RJRSIM.cjs.map → chunk-XTMNO7MC.cjs.map} +1 -1
- package/dist/{chunk-FQBR6SCA.js → chunk-XW4GOMK6.js} +3 -3
- package/dist/{chunk-77Y7R243.js → chunk-YEEMXD25.js} +5 -5
- package/dist/{chunk-LGBNHWDN.js → chunk-YTVTZ7JO.js} +3 -3
- package/dist/components/case-studies/index.cjs +9 -9
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +4 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-slash-commands.d.ts +3 -3
- package/dist/components/chat/hooks/use-slash-commands.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +2 -28
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/chat/remark-strip-citations.d.ts.map +1 -1
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
- package/dist/components/chat/tool-execution-display.d.ts.map +1 -1
- package/dist/components/chat/types/component.types.d.ts +4 -0
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +35 -1
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +3 -3
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
- package/dist/components/chat/utils/chat-source-metadata.d.ts +5 -0
- package/dist/components/chat/utils/chat-source-metadata.d.ts.map +1 -0
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +4 -4
- package/dist/components/faq/index.js +3 -3
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/index.cjs +36 -36
- package/dist/components/index.js +13 -13
- package/dist/components/meeting-scheduler/index.cjs +21 -21
- package/dist/components/meeting-scheduler/index.js +2 -2
- package/dist/components/navigation/index.cjs +6 -6
- package/dist/components/navigation/index.js +5 -5
- package/dist/components/onboarding-guides/index.cjs +6 -6
- package/dist/components/onboarding-guides/index.js +5 -5
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/hover-dropdown.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/index.cjs +7 -7
- package/dist/index.js +6 -6
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +2 -1
- package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +130 -0
- package/src/chat-protocol/__tests__/source-metadata.test.ts +74 -0
- package/src/chat-protocol/events.ts +9 -1
- package/src/chat-protocol/nats-decoder.ts +7 -0
- package/src/chat-protocol/source-metadata.ts +202 -0
- package/src/components/chat/__tests__/chat-message-list.test.tsx +70 -0
- package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +86 -0
- package/src/components/chat/__tests__/embeddable-chat-sources.test.tsx +411 -0
- package/src/components/chat/__tests__/remark-strip-citations.test.ts +52 -0
- package/src/components/chat/__tests__/tool-execution-display.test.tsx +93 -0
- package/src/components/chat/chat-message-enhanced.tsx +28 -13
- package/src/components/chat/chat-message-list.tsx +27 -22
- package/src/components/chat/embeddable-chat.tsx +55 -36
- package/src/components/chat/entity-cards/__tests__/dispatch-video-refs.test.tsx +74 -0
- package/src/components/chat/entity-cards/dispatch.tsx +86 -17
- package/src/components/chat/hooks/__tests__/use-slash-commands.test.ts +64 -0
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +7 -0
- package/src/components/chat/hooks/use-slash-commands.ts +9 -5
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +2 -29
- package/src/components/chat/remark-strip-citations.ts +5 -3
- package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +56 -0
- package/src/components/chat/stream/chat-stream-reducer.ts +31 -0
- package/src/components/chat/tool-execution-display.tsx +5 -2
- package/src/components/chat/types/component.types.ts +4 -0
- package/src/components/chat/types/message.types.ts +39 -0
- package/src/components/chat/types/unified-chat-state.types.ts +4 -3
- package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +57 -0
- package/src/components/chat/utils/chat-source-metadata.ts +19 -0
- package/src/components/chat/utils/process-historical-messages.ts +19 -1
- package/src/components/ui/hover-dropdown.tsx +8 -3
- package/dist/chunk-D7YISHXC.js.map +0 -1
- package/dist/chunk-G45RGWHF.cjs.map +0 -1
- /package/dist/{chunk-GKPY7VLK.js.map → chunk-47NIV4FC.js.map} +0 -0
- /package/dist/{chunk-I5CJJFEJ.js.map → chunk-4VDGIODG.js.map} +0 -0
- /package/dist/{chunk-3OCYNMZ5.js.map → chunk-CQNILR7U.js.map} +0 -0
- /package/dist/{chunk-CN36E4PK.js.map → chunk-EJEHIAOV.js.map} +0 -0
- /package/dist/{chunk-CJOWLRUU.js.map → chunk-G4ND6ZZK.js.map} +0 -0
- /package/dist/{chunk-KVNGYFFP.js.map → chunk-GU5TWGAJ.js.map} +0 -0
- /package/dist/{chunk-NXZC6UTN.js.map → chunk-HZ6VUTLA.js.map} +0 -0
- /package/dist/{chunk-4ZKAMPBX.js.map → chunk-OAY342OE.js.map} +0 -0
- /package/dist/{chunk-TOYHZHTK.js.map → chunk-PDFH7CXB.js.map} +0 -0
- /package/dist/{chunk-NIDPX52C.js.map → chunk-QIHBS5PM.js.map} +0 -0
- /package/dist/{chunk-CNKVEVUN.js.map → chunk-SPOIDC23.js.map} +0 -0
- /package/dist/{chunk-5FNWOF46.js.map → chunk-WZZSFOWG.js.map} +0 -0
- /package/dist/{chunk-FQBR6SCA.js.map → chunk-XW4GOMK6.js.map} +0 -0
- /package/dist/{chunk-77Y7R243.js.map → chunk-YEEMXD25.js.map} +0 -0
- /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
|
|
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
|
-
|
|
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
|
-
{!
|
|
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
|
-
{
|
|
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
|
|
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.
|
|
164
|
-
//
|
|
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
|
-
|
|
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
|