@flamingo-stack/openframe-frontend-core 0.0.583 → 0.0.584-1973.5420.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 +177 -0
- package/dist/chat-protocol/index.cjs.map +1 -1
- package/dist/chat-protocol/index.js +177 -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 +3 -0
- package/dist/chat-protocol/source-metadata.d.ts.map +1 -0
- package/dist/{chunk-EATPU5EF.cjs → chunk-3MAG3EG4.cjs} +9 -9
- package/dist/{chunk-EATPU5EF.cjs.map → chunk-3MAG3EG4.cjs.map} +1 -1
- package/dist/{chunk-IKUSKKFZ.cjs → chunk-4TZOFXPH.cjs} +712 -432
- package/dist/chunk-4TZOFXPH.cjs.map +1 -0
- package/dist/{chunk-WMRXB6RS.js → chunk-54WRMPCY.js} +3 -3
- package/dist/{chunk-WTC6QQNX.cjs → chunk-7IVT56YY.cjs} +3 -3
- package/dist/{chunk-WTC6QQNX.cjs.map → chunk-7IVT56YY.cjs.map} +1 -1
- package/dist/{chunk-PFKWJ6PJ.js → chunk-AQW2PDRS.js} +2 -2
- package/dist/{chunk-DROXHJVZ.js → chunk-AVVKXNGI.js} +2 -2
- package/dist/{chunk-YNLIXABT.cjs → chunk-CE6CA6EM.cjs} +43 -43
- package/dist/{chunk-YNLIXABT.cjs.map → chunk-CE6CA6EM.cjs.map} +1 -1
- package/dist/{chunk-TRD2YICZ.cjs → chunk-CGLQLBMQ.cjs} +21 -21
- package/dist/{chunk-TRD2YICZ.cjs.map → chunk-CGLQLBMQ.cjs.map} +1 -1
- package/dist/{chunk-XWBND4G5.js → chunk-CO4WRST6.js} +301 -21
- package/dist/chunk-CO4WRST6.js.map +1 -0
- package/dist/{chunk-IJVWKLJK.cjs → chunk-DHLD3JXN.cjs} +69 -69
- package/dist/{chunk-IJVWKLJK.cjs.map → chunk-DHLD3JXN.cjs.map} +1 -1
- package/dist/{chunk-M6ZUEBDK.cjs → chunk-F2EN6MXN.cjs} +159 -159
- package/dist/{chunk-M6ZUEBDK.cjs.map → chunk-F2EN6MXN.cjs.map} +1 -1
- package/dist/{chunk-J3EEL6YC.js → chunk-FH2FH337.js} +5 -5
- package/dist/{chunk-L26MD32B.js → chunk-GDHI3JN5.js} +3 -3
- package/dist/{chunk-VWHEXQ64.js → chunk-GFIH73Q3.js} +3 -3
- package/dist/{chunk-R5A3MGMY.js → chunk-H6GYY5CB.js} +3 -3
- package/dist/{chunk-FCLM2RXI.js → chunk-HGJAUYHW.js} +2 -2
- package/dist/{chunk-RBDVHLT5.cjs → chunk-HKKQCVVY.cjs} +22 -22
- package/dist/{chunk-RBDVHLT5.cjs.map → chunk-HKKQCVVY.cjs.map} +1 -1
- package/dist/{chunk-KDWNMFX4.cjs → chunk-JEMGFSC2.cjs} +24 -24
- package/dist/{chunk-KDWNMFX4.cjs.map → chunk-JEMGFSC2.cjs.map} +1 -1
- package/dist/{chunk-BTBW4L2W.cjs → chunk-JX6RSSZP.cjs} +33 -33
- package/dist/{chunk-BTBW4L2W.cjs.map → chunk-JX6RSSZP.cjs.map} +1 -1
- package/dist/{chunk-WHNC7C76.js → chunk-KN2ZCZQ7.js} +5 -5
- package/dist/{chunk-WBTKXNKZ.cjs → chunk-MYVHYVQM.cjs} +10 -10
- package/dist/{chunk-WBTKXNKZ.cjs.map → chunk-MYVHYVQM.cjs.map} +1 -1
- package/dist/{chunk-3TNM6SDP.cjs → chunk-PI3BNDCW.cjs} +15 -15
- package/dist/{chunk-3TNM6SDP.cjs.map → chunk-PI3BNDCW.cjs.map} +1 -1
- package/dist/{chunk-XZ3MKTJZ.js → chunk-PWI66GZF.js} +2 -2
- package/dist/{chunk-IFPGXSMZ.cjs → chunk-RTI3TYMX.cjs} +3 -3
- package/dist/{chunk-IFPGXSMZ.cjs.map → chunk-RTI3TYMX.cjs.map} +1 -1
- package/dist/{chunk-6BI3AYBY.cjs → chunk-SS2TCIKA.cjs} +4 -4
- package/dist/{chunk-6BI3AYBY.cjs.map → chunk-SS2TCIKA.cjs.map} +1 -1
- package/dist/{chunk-HIHPWVKP.js → chunk-T35JA5XV.js} +5 -5
- package/dist/{chunk-EZ5VU5WS.js → chunk-TLZ7HTXP.js} +2 -2
- package/dist/{chunk-OCXFFGFA.cjs → chunk-UZUG7QZ3.cjs} +10 -10
- package/dist/{chunk-OCXFFGFA.cjs.map → chunk-UZUG7QZ3.cjs.map} +1 -1
- package/dist/{chunk-3RR67XC2.cjs → chunk-W46U5J3H.cjs} +5 -5
- package/dist/{chunk-3RR67XC2.cjs.map → chunk-W46U5J3H.cjs.map} +1 -1
- package/dist/{chunk-IOP7BCRQ.js → chunk-WFF5JMQ2.js} +6 -6
- package/dist/{chunk-AJLF6MBD.js → chunk-X4HLHN36.js} +3 -3
- package/dist/{chunk-PMA7DVQR.js → chunk-ZWB64HOD.js} +4 -4
- 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/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-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/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 +2 -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/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 +1 -1
- package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +130 -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 +187 -0
- package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +86 -0
- package/src/components/chat/__tests__/embeddable-chat-sources.test.tsx +171 -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 +1 -0
- package/src/components/chat/embeddable-chat.tsx +10 -1
- package/src/components/chat/entity-cards/__tests__/dispatch-video-refs.test.tsx +74 -0
- package/src/components/chat/entity-cards/dispatch.tsx +48 -11
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +7 -0
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +2 -29
- 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 +2 -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/dist/chunk-IKUSKKFZ.cjs.map +0 -1
- package/dist/chunk-XWBND4G5.js.map +0 -1
- /package/dist/{chunk-WMRXB6RS.js.map → chunk-54WRMPCY.js.map} +0 -0
- /package/dist/{chunk-PFKWJ6PJ.js.map → chunk-AQW2PDRS.js.map} +0 -0
- /package/dist/{chunk-DROXHJVZ.js.map → chunk-AVVKXNGI.js.map} +0 -0
- /package/dist/{chunk-J3EEL6YC.js.map → chunk-FH2FH337.js.map} +0 -0
- /package/dist/{chunk-L26MD32B.js.map → chunk-GDHI3JN5.js.map} +0 -0
- /package/dist/{chunk-VWHEXQ64.js.map → chunk-GFIH73Q3.js.map} +0 -0
- /package/dist/{chunk-R5A3MGMY.js.map → chunk-H6GYY5CB.js.map} +0 -0
- /package/dist/{chunk-FCLM2RXI.js.map → chunk-HGJAUYHW.js.map} +0 -0
- /package/dist/{chunk-WHNC7C76.js.map → chunk-KN2ZCZQ7.js.map} +0 -0
- /package/dist/{chunk-XZ3MKTJZ.js.map → chunk-PWI66GZF.js.map} +0 -0
- /package/dist/{chunk-HIHPWVKP.js.map → chunk-T35JA5XV.js.map} +0 -0
- /package/dist/{chunk-EZ5VU5WS.js.map → chunk-TLZ7HTXP.js.map} +0 -0
- /package/dist/{chunk-IOP7BCRQ.js.map → chunk-WFF5JMQ2.js.map} +0 -0
- /package/dist/{chunk-AJLF6MBD.js.map → chunk-X4HLHN36.js.map} +0 -0
- /package/dist/{chunk-PMA7DVQR.js.map → chunk-ZWB64HOD.js.map} +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { decodeNatsChunk } from '../../../chat-protocol/nats-decoder';
|
|
4
|
+
import { createChatStreamReducer } from '../stream/chat-stream-reducer';
|
|
5
|
+
import { ToolExecutionDisplay } from '../tool-execution-display';
|
|
6
|
+
import type { ToolExecutionData } from '../types';
|
|
7
|
+
|
|
8
|
+
const execution = (overrides: Partial<ToolExecutionData>): ToolExecutionData => ({
|
|
9
|
+
type: 'EXECUTING_TOOL',
|
|
10
|
+
integratedToolType: 'OPENFRAME',
|
|
11
|
+
toolFunction: 'search_docs',
|
|
12
|
+
...overrides,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
vi.stubGlobal(
|
|
17
|
+
'ResizeObserver',
|
|
18
|
+
class {
|
|
19
|
+
observe(): void {}
|
|
20
|
+
disconnect(): void {}
|
|
21
|
+
unobserve(): void {}
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterAll(() => {
|
|
27
|
+
vi.unstubAllGlobals();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('ToolExecutionDisplay remote read activity', () => {
|
|
31
|
+
it('keeps a completed remote read concise through the real wire pipeline', () => {
|
|
32
|
+
const reducer = createChatStreamReducer({ transport: 'nats' });
|
|
33
|
+
for (const chunk of [
|
|
34
|
+
{
|
|
35
|
+
type: 'EXECUTING_TOOL',
|
|
36
|
+
integratedToolType: null,
|
|
37
|
+
toolFunction: 'remote_read',
|
|
38
|
+
title: 'Searching product documentation',
|
|
39
|
+
parameters: null,
|
|
40
|
+
toolExecutionRequestId: 'remote-read-1',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: 'EXECUTED_TOOL',
|
|
44
|
+
integratedToolType: null,
|
|
45
|
+
toolFunction: 'remote_read',
|
|
46
|
+
result: null,
|
|
47
|
+
success: true,
|
|
48
|
+
toolExecutionRequestId: 'remote-read-1',
|
|
49
|
+
},
|
|
50
|
+
]) {
|
|
51
|
+
const event = decodeNatsChunk(chunk);
|
|
52
|
+
if (event) reducer.apply(event);
|
|
53
|
+
}
|
|
54
|
+
const toolSegment = reducer.state.messages
|
|
55
|
+
.flatMap(message => message.segments ?? [])
|
|
56
|
+
.find(segment => segment.type === 'tool_execution');
|
|
57
|
+
|
|
58
|
+
expect(toolSegment?.type).toBe('tool_execution');
|
|
59
|
+
if (toolSegment?.type !== 'tool_execution') return;
|
|
60
|
+
render(<ToolExecutionDisplay message={toolSegment.data} />);
|
|
61
|
+
|
|
62
|
+
expect(screen.getByText('Searching product documentation')).toBeInTheDocument();
|
|
63
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
64
|
+
expect(screen.queryByText('Remote read')).not.toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('keeps a typed local tool expandable when its body is absent', () => {
|
|
68
|
+
render(
|
|
69
|
+
<ToolExecutionDisplay
|
|
70
|
+
message={execution({
|
|
71
|
+
integratedToolType: 'OPENFRAME',
|
|
72
|
+
toolTitle: 'Refresh device inventory',
|
|
73
|
+
})}
|
|
74
|
+
/>,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('keeps a backend-described remote read concise while running', () => {
|
|
81
|
+
render(
|
|
82
|
+
<ToolExecutionDisplay
|
|
83
|
+
message={execution({
|
|
84
|
+
integratedToolType: undefined,
|
|
85
|
+
toolTitle: 'Searching product documentation',
|
|
86
|
+
})}
|
|
87
|
+
/>,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(screen.getByText('Searching product documentation')).toBeInTheDocument();
|
|
91
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -103,6 +103,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
103
103
|
renderContextItem,
|
|
104
104
|
renderMention,
|
|
105
105
|
renderEntityCard,
|
|
106
|
+
refs,
|
|
106
107
|
onAskSelect,
|
|
107
108
|
NavLinkAnchor,
|
|
108
109
|
...props
|
|
@@ -127,6 +128,11 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
127
128
|
// literal `[card://...]` URL.
|
|
128
129
|
const hasMarkerSupport = !!renderEntityCard;
|
|
129
130
|
|
|
131
|
+
const refsByKey = useMemo(
|
|
132
|
+
() => new Map((refs ?? []).map(reference => [`${reference.type}:${reference.id}`, reference])),
|
|
133
|
+
[refs],
|
|
134
|
+
);
|
|
135
|
+
|
|
130
136
|
const segments = useMemo(() => normalizeContent(content), [content]);
|
|
131
137
|
|
|
132
138
|
const askRuns = useMemo(() => groupAskRuns(segments), [segments]);
|
|
@@ -180,7 +186,14 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
180
186
|
// (and its open menu) survives across chunks. Invalidated per key when the
|
|
181
187
|
// render fn identity changes.
|
|
182
188
|
const renderedCardNodeCache = useRef(
|
|
183
|
-
new Map<
|
|
189
|
+
new Map<
|
|
190
|
+
string,
|
|
191
|
+
{
|
|
192
|
+
render: ((ref: ChatRef) => React.ReactNode) | undefined;
|
|
193
|
+
reference: ChatRef;
|
|
194
|
+
node: React.ReactNode;
|
|
195
|
+
}
|
|
196
|
+
>(),
|
|
184
197
|
);
|
|
185
198
|
|
|
186
199
|
/**
|
|
@@ -242,10 +255,17 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
242
255
|
// menu / re-fetching) on every stream chunk. Also dedups the same
|
|
243
256
|
// key emitted twice within one message.
|
|
244
257
|
let entry = cache.get(key);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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.
|
|
249
269
|
// For types with nothing to fetch the host's render() returns
|
|
250
270
|
// null and we fall through to the bare-cardId fallback in the
|
|
251
271
|
// `<a card://…>` override below.
|
|
@@ -257,13 +277,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
257
277
|
// the id rather than `undefined`) and `url` to null (matches
|
|
258
278
|
// the no-link semantics fetch-mode cards rely on — they
|
|
259
279
|
// resolve their own URL after fetch).
|
|
260
|
-
|
|
261
|
-
type: cardType,
|
|
262
|
-
id: cardId,
|
|
263
|
-
title: cardId,
|
|
264
|
-
url: null,
|
|
265
|
-
};
|
|
266
|
-
entry = { render, node: render(refForRender) };
|
|
280
|
+
entry = { render, reference: refForRender, node: render(refForRender) };
|
|
267
281
|
cache.set(key, entry);
|
|
268
282
|
}
|
|
269
283
|
const rendered = entry.node;
|
|
@@ -326,7 +340,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
326
340
|
}
|
|
327
341
|
});
|
|
328
342
|
return { inlineByKey, partsBySegment, usedKeys };
|
|
329
|
-
}, [hasMarkerSupport, renderEntityCard, segments]);
|
|
343
|
+
}, [hasMarkerSupport, refsByKey, renderEntityCard, segments]);
|
|
330
344
|
|
|
331
345
|
// Drop cached nodes for markers no longer present so the cache can't grow
|
|
332
346
|
// unbounded as a long message's markers change. Deliberately an EFFECT,
|
|
@@ -716,6 +730,7 @@ const MemoizedChatMessageEnhanced = memo(ChatMessageEnhanced, (prevProps, nextPr
|
|
|
716
730
|
// equality holds across streaming chunks.
|
|
717
731
|
prevProps.renderMention === nextProps.renderMention &&
|
|
718
732
|
prevProps.renderEntityCard === nextProps.renderEntityCard &&
|
|
733
|
+
prevProps.refs === nextProps.refs &&
|
|
719
734
|
// Same stability contract as the renderers above: hosts pass a `useCallback`
|
|
720
735
|
// (EmbeddableChat passes its memoized `handleSend`), so this holds across
|
|
721
736
|
// streaming chunks instead of re-rendering every ask card per chunk.
|
|
@@ -1005,6 +1005,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
1005
1005
|
renderContextItem={renderContextItem}
|
|
1006
1006
|
renderMention={renderMention}
|
|
1007
1007
|
renderEntityCard={renderEntityCard}
|
|
1008
|
+
refs={message.refs}
|
|
1008
1009
|
onAskSelect={index > lastUserMessageIndex ? onAskSelect : undefined}
|
|
1009
1010
|
NavLinkAnchor={NavLinkAnchor}
|
|
1010
1011
|
/>
|
|
@@ -1579,6 +1579,10 @@ function EmbeddableChatInner({
|
|
|
1579
1579
|
// explicitly — see `Message.hidden` and `chat-message-list`'s skip.
|
|
1580
1580
|
...(m.hidden ? { hidden: true } : {}),
|
|
1581
1581
|
...(m.scrollAnchor ? { scrollAnchor: m.scrollAnchor } : {}),
|
|
1582
|
+
// Preserve same-turn rich metadata. Source chips and entity-card
|
|
1583
|
+
// renderers consume it after this raw-to-Message projection.
|
|
1584
|
+
...(m.sources && m.sources.length > 0 ? { sources: m.sources } : {}),
|
|
1585
|
+
...(m.refs && m.refs.length > 0 ? { refs: m.refs } : {}),
|
|
1582
1586
|
// Forward attached context items so the user bubble renders its chips.
|
|
1583
1587
|
...(m.contextItems && m.contextItems.length > 0 ? { contextItems: m.contextItems } : {}),
|
|
1584
1588
|
};
|
|
@@ -1835,7 +1839,12 @@ function EmbeddableChatInner({
|
|
|
1835
1839
|
if (chatLoading) return undefined;
|
|
1836
1840
|
const sources = lastAssistantMsg?.sources;
|
|
1837
1841
|
if (!sources || sources.length === 0) return undefined;
|
|
1838
|
-
const content =
|
|
1842
|
+
const content =
|
|
1843
|
+
lastAssistantMsg.content ||
|
|
1844
|
+
(lastAssistantMsg.segments ?? [])
|
|
1845
|
+
.filter(segment => segment.type === 'text')
|
|
1846
|
+
.map(segment => segment.text)
|
|
1847
|
+
.join('\n');
|
|
1839
1848
|
const citationOrder = [...content.matchAll(/\[(\d+)\]/g)].map(m => parseInt(m[1], 10));
|
|
1840
1849
|
const seenOrder = new Map<number, number>();
|
|
1841
1850
|
citationOrder.forEach(idx => {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import type { ChatRef } from '../../chat-ref.types';
|
|
4
|
+
import { BlockCard } from '../block-card';
|
|
5
|
+
import { ChatVideoEntityCard } from '../chat-video-entity-card';
|
|
6
|
+
import { renderChatInlineEntityCard } from '../dispatch';
|
|
7
|
+
|
|
8
|
+
describe('renderChatInlineEntityCard video refs', () => {
|
|
9
|
+
it.each([
|
|
10
|
+
{
|
|
11
|
+
id: 'MdFJNoJeqZQ',
|
|
12
|
+
url: 'https://www.youtube.com/watch?v=MdFJNoJeqZQ',
|
|
13
|
+
metadata: { youtubeUrl: 'MdFJNoJeqZQ' },
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'mux-9b6586b494',
|
|
17
|
+
url: 'https://stream.mux.com/playback-id.m3u8',
|
|
18
|
+
metadata: { videoUrl: 'https://stream.mux.com/playback-id.m3u8' },
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'mp4-1b2047dc1b',
|
|
22
|
+
url: 'https://cdn.example.test/product-overview.mp4',
|
|
23
|
+
metadata: { videoUrl: 'https://cdn.example.test/product-overview.mp4' },
|
|
24
|
+
},
|
|
25
|
+
])('passes playable metadata for $id to the video card', ({ id, url, metadata }) => {
|
|
26
|
+
const ref: ChatRef = { type: 'video', id, title: 'Video', url, metadata };
|
|
27
|
+
|
|
28
|
+
const result = renderChatInlineEntityCard(ref);
|
|
29
|
+
|
|
30
|
+
expect(React.isValidElement(result)).toBe(true);
|
|
31
|
+
const block = result as React.ReactElement<React.ComponentProps<typeof BlockCard>>;
|
|
32
|
+
expect(block.type).toBe(BlockCard);
|
|
33
|
+
const video = block.props.children as React.ReactElement<React.ComponentProps<typeof ChatVideoEntityCard>>;
|
|
34
|
+
expect(video.type).toBe(ChatVideoEntityCard);
|
|
35
|
+
expect(video.props.chatRef.metadata).toMatchObject(metadata);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('lets a validated top-level URL override conflicting route metadata', () => {
|
|
39
|
+
const ref: ChatRef = {
|
|
40
|
+
type: 'video',
|
|
41
|
+
id: 'mux-9b6586b494',
|
|
42
|
+
title: 'Video',
|
|
43
|
+
url: 'https://stream.mux.com/trusted-playback.m3u8',
|
|
44
|
+
metadata: {
|
|
45
|
+
videoUrl: 'https://cdn.example.test/untrusted.mp4',
|
|
46
|
+
youtubeUrl: 'untrustedId',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const result = renderChatInlineEntityCard(ref);
|
|
51
|
+
const block = result as React.ReactElement<React.ComponentProps<typeof BlockCard>>;
|
|
52
|
+
const video = block.props.children as React.ReactElement<React.ComponentProps<typeof ChatVideoEntityCard>>;
|
|
53
|
+
|
|
54
|
+
expect(video.props.chatRef.metadata).toEqual({
|
|
55
|
+
videoUrl: 'https://stream.mux.com/trusted-playback.m3u8',
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('keeps a direct-video URL direct when its query mentions a YouTube URL', () => {
|
|
60
|
+
const directUrl = 'https://cdn.example.test/product-overview.mp4?source=youtube.com/watch?v=MdFJNoJeqZQ';
|
|
61
|
+
const ref: ChatRef = {
|
|
62
|
+
type: 'video',
|
|
63
|
+
id: 'mp4-direct',
|
|
64
|
+
title: 'Product overview',
|
|
65
|
+
url: directUrl,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const result = renderChatInlineEntityCard(ref);
|
|
69
|
+
const block = result as React.ReactElement<React.ComponentProps<typeof BlockCard>>;
|
|
70
|
+
const video = block.props.children as React.ReactElement<React.ComponentProps<typeof ChatVideoEntityCard>>;
|
|
71
|
+
|
|
72
|
+
expect(video.props.chatRef.metadata).toEqual({ videoUrl: directUrl });
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
formatTimeWithTimezone,
|
|
42
42
|
formatDurationFromRange,
|
|
43
43
|
} from '../../../utils/format';
|
|
44
|
+
import { extractYouTubeId } from '../../features/video';
|
|
44
45
|
import { MingoIcon } from '../../icons';
|
|
45
46
|
import { ArrowRightUpIcon } from '../../icons-v2-generated/arrows/arrow-right-up-icon';
|
|
46
47
|
import { ClickupLogoIcon } from '../../icons-v2-generated/brand-logos/clickup-logo-icon';
|
|
@@ -422,6 +423,15 @@ function hubspotStatusToVariant(status: string | undefined): MingoInfoCardStatus
|
|
|
422
423
|
* which renders the marker as plain text (same anti-hallucination
|
|
423
424
|
* posture as every other card type).
|
|
424
425
|
*/
|
|
426
|
+
function validatedHttpsUrl(value: unknown): string | null {
|
|
427
|
+
if (typeof value !== 'string') return null;
|
|
428
|
+
try {
|
|
429
|
+
return new URL(value).protocol === 'https:' ? value : null;
|
|
430
|
+
} catch {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
425
435
|
function decodeVideoMarkerId(id: string): { videoUrl?: string; youtubeUrl?: string } | null {
|
|
426
436
|
// Prefix checks FIRST: an id like `mux-1234567` (11 chars) would also
|
|
427
437
|
// satisfy the bare-YouTube-id pattern below.
|
|
@@ -439,8 +449,8 @@ function decodeVideoMarkerId(id: string): { videoUrl?: string; youtubeUrl?: stri
|
|
|
439
449
|
const url = atob(b64);
|
|
440
450
|
// https-only — the id is model-emitted text; never let a marker
|
|
441
451
|
// smuggle a non-https src into a player.
|
|
442
|
-
|
|
443
|
-
return { videoUrl:
|
|
452
|
+
const safeUrl = validatedHttpsUrl(url);
|
|
453
|
+
return safeUrl ? { videoUrl: safeUrl } : null;
|
|
444
454
|
} catch {
|
|
445
455
|
return null;
|
|
446
456
|
}
|
|
@@ -449,6 +459,29 @@ function decodeVideoMarkerId(id: string): { videoUrl?: string; youtubeUrl?: stri
|
|
|
449
459
|
return null;
|
|
450
460
|
}
|
|
451
461
|
|
|
462
|
+
function validatedVideoMetadata(chatRef: ChatRef): Record<string, unknown> | null {
|
|
463
|
+
const input = chatRef.metadata ?? {};
|
|
464
|
+
const metadata: Record<string, unknown> = {};
|
|
465
|
+
for (const key of ['videoUrl', 'highlightVideoUrl', 'videoPoster', 'highlightVideoPoster'] as const) {
|
|
466
|
+
const url = validatedHttpsUrl(input[key]);
|
|
467
|
+
if (url) metadata[key] = url;
|
|
468
|
+
}
|
|
469
|
+
const youtubeUrl = input.youtubeUrl;
|
|
470
|
+
if (typeof youtubeUrl === 'string' && /^[A-Za-z0-9_-]{11}$/.test(youtubeUrl)) {
|
|
471
|
+
metadata.youtubeUrl = youtubeUrl;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const safeUrl = validatedHttpsUrl(chatRef.url);
|
|
475
|
+
if (safeUrl) {
|
|
476
|
+
const youtubeId = extractYouTubeId(safeUrl);
|
|
477
|
+
delete metadata.videoUrl;
|
|
478
|
+
delete metadata.youtubeUrl;
|
|
479
|
+
metadata[youtubeId ? 'youtubeUrl' : 'videoUrl'] = youtubeId ?? safeUrl;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return Object.keys(metadata).length > 0 ? metadata : null;
|
|
483
|
+
}
|
|
484
|
+
|
|
452
485
|
/** Read hero-video metadata off a FETCHED item — the API-driven
|
|
453
486
|
* replacement for the retired refs-frame `metadata.videoUrl` check. Two item
|
|
454
487
|
* shapes: ChatRef-shaped items (per-object hydration routes) carry the
|
|
@@ -1983,24 +2016,28 @@ export function renderChatInlineEntityCard(
|
|
|
1983
2016
|
/>
|
|
1984
2017
|
);
|
|
1985
2018
|
|
|
1986
|
-
//
|
|
1987
|
-
//
|
|
1988
|
-
//
|
|
1989
|
-
//
|
|
2019
|
+
// SOURCES metadata may enrich an opaque video marker with its playable URL.
|
|
2020
|
+
// Self-describing marker ids remain transport-independent through
|
|
2021
|
+
// `decodeVideoMarkerId`; undecodable ids without matching metadata fall
|
|
2022
|
+
// through to the loader and degrade to plain text.
|
|
1990
2023
|
// Hero videos on FETCHED entity types (webinars, releases, …) are
|
|
1991
2024
|
// handled post-fetch inside ChatCardLoader (`itemVideoMetadata`) — the
|
|
1992
|
-
// marker
|
|
2025
|
+
// marker metadata is not required for that post-fetch path.
|
|
1993
2026
|
if (chatRef.type === 'video') {
|
|
2027
|
+
const enrichedMetadata = validatedVideoMetadata(chatRef);
|
|
1994
2028
|
const decoded = decodeVideoMarkerId(chatRef.id);
|
|
1995
|
-
|
|
2029
|
+
const playableMetadata = enrichedMetadata ?? decoded;
|
|
2030
|
+
if (playableMetadata) {
|
|
1996
2031
|
const displayRef: ChatRef = {
|
|
1997
2032
|
...chatRef,
|
|
1998
2033
|
title: chatRef.title && chatRef.title !== chatRef.id ? chatRef.title : 'Video',
|
|
1999
2034
|
url:
|
|
2000
2035
|
chatRef.url ??
|
|
2001
|
-
|
|
2002
|
-
(
|
|
2003
|
-
|
|
2036
|
+
(typeof playableMetadata.videoUrl === 'string' ? playableMetadata.videoUrl : null) ??
|
|
2037
|
+
(typeof playableMetadata.youtubeUrl === 'string'
|
|
2038
|
+
? `https://www.youtube.com/watch?v=${playableMetadata.youtubeUrl}`
|
|
2039
|
+
: null),
|
|
2040
|
+
metadata: playableMetadata,
|
|
2004
2041
|
};
|
|
2005
2042
|
return (
|
|
2006
2043
|
<BlockCard inline={<ChatInlineVideoPill chatRef={displayRef} />}>
|
|
@@ -61,6 +61,7 @@ import type {
|
|
|
61
61
|
MessageSegment,
|
|
62
62
|
NatsMessageType,
|
|
63
63
|
ChatApprovalStatus,
|
|
64
|
+
ChatSource,
|
|
64
65
|
} from '../types';
|
|
65
66
|
import type { DialogItem } from '../types/component.types';
|
|
66
67
|
import type {
|
|
@@ -352,6 +353,8 @@ export function mapProcessedToUnified(
|
|
|
352
353
|
role: 'user' | 'assistant' | 'error';
|
|
353
354
|
content: string | MessageSegment[];
|
|
354
355
|
name?: string;
|
|
356
|
+
sources?: ChatSource[];
|
|
357
|
+
refs?: ChatRef[];
|
|
355
358
|
}>,
|
|
356
359
|
): UnifiedChatMessage[] {
|
|
357
360
|
const out: UnifiedChatMessage[] = [];
|
|
@@ -363,12 +366,16 @@ export function mapProcessedToUnified(
|
|
|
363
366
|
role: m.role,
|
|
364
367
|
content: '',
|
|
365
368
|
segments: m.content,
|
|
369
|
+
...(m.sources ? { sources: m.sources } : {}),
|
|
370
|
+
...(m.refs ? { refs: m.refs } : {}),
|
|
366
371
|
});
|
|
367
372
|
} else {
|
|
368
373
|
out.push({
|
|
369
374
|
id: m.id,
|
|
370
375
|
role: m.role,
|
|
371
376
|
content: m.content,
|
|
377
|
+
...(m.sources ? { sources: m.sources } : {}),
|
|
378
|
+
...(m.refs ? { refs: m.refs } : {}),
|
|
372
379
|
});
|
|
373
380
|
}
|
|
374
381
|
}
|
|
@@ -63,7 +63,7 @@ import type { ChatDialogStore } from '../stream/chat-dialog-store';
|
|
|
63
63
|
import type { ChatStreamReducerOptions } from '../stream/chat-stream-reducer';
|
|
64
64
|
import { useChatStreamReducer } from '../stream/use-chat-stream-reducer';
|
|
65
65
|
import type { DialogItem } from '../types/component.types';
|
|
66
|
-
import type { Message, MessageSegment } from '../types/message.types';
|
|
66
|
+
import type { ChatSource, Message, MessageSegment } from '../types/message.types';
|
|
67
67
|
import type {
|
|
68
68
|
UnifiedChatState,
|
|
69
69
|
UnifiedChatMessage,
|
|
@@ -81,6 +81,7 @@ import { useSlashCommandRegistry, type SlashCommandSummary } from './use-slash-c
|
|
|
81
81
|
// Canonical home of the per-turn meta row moved to the stream module in
|
|
82
82
|
// Phase 3; re-exported here to keep the legacy import path stable.
|
|
83
83
|
export type { ChatTurnMeta } from '../stream/chat-stream-reducer';
|
|
84
|
+
export type { ChatSource } from '../types/message.types';
|
|
84
85
|
|
|
85
86
|
// ─── Stable no-op references for the Guide-mode dialog-management stubs ──
|
|
86
87
|
// Plain module-scope constants so the adapter's return identity stays
|
|
@@ -128,34 +129,6 @@ const noopBumpMetaTick = (): void => {};
|
|
|
128
129
|
* hub-side platform-utils, not in lib). */
|
|
129
130
|
export type DocSource = string;
|
|
130
131
|
|
|
131
|
-
export interface ChatSource {
|
|
132
|
-
index: number;
|
|
133
|
-
name: string;
|
|
134
|
-
path: string;
|
|
135
|
-
documentType: string;
|
|
136
|
-
externalUrl?: string;
|
|
137
|
-
/** Platform that owns the destination at `externalUrl`. */
|
|
138
|
-
targetPlatform?: string | null;
|
|
139
|
-
/** Primary-key value for single-row chips. */
|
|
140
|
-
id?: string;
|
|
141
|
-
/** Per-row items for grouped chips. */
|
|
142
|
-
items?: Array<{
|
|
143
|
-
id: string;
|
|
144
|
-
documentType: string;
|
|
145
|
-
name: string;
|
|
146
|
-
externalUrl?: string;
|
|
147
|
-
targetPlatform?: string | null;
|
|
148
|
-
/** In-app doc-tree path for markdown / data-room-doc rows so the
|
|
149
|
-
* grouped chip's anchor can trigger an in-page doc-tree swap via
|
|
150
|
-
* `handleChatNavClick` (parity with single-row chips + cards). */
|
|
151
|
-
path?: string | null;
|
|
152
|
-
}>;
|
|
153
|
-
/** RagTableConfig.id for this source. */
|
|
154
|
-
sourceRepo?: string;
|
|
155
|
-
/** Optional display label override returned by the chat API. */
|
|
156
|
-
label?: string;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
132
|
export interface DocChatMessage {
|
|
160
133
|
id: string;
|
|
161
134
|
role: 'user' | 'assistant';
|
|
@@ -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
|