@flamingo-stack/openframe-frontend-core 0.0.579 → 0.0.580-1958.5358.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/decode.d.ts.map +1 -1
- package/dist/chat-protocol/events.d.ts +1 -47
- package/dist/chat-protocol/events.d.ts.map +1 -1
- package/dist/chat-protocol/index.cjs +0 -51
- package/dist/chat-protocol/index.cjs.map +1 -1
- package/dist/chat-protocol/index.js +1 -48
- package/dist/chat-protocol/index.js.map +1 -1
- package/dist/chat-protocol/leading-frames.d.ts +3 -9
- package/dist/chat-protocol/leading-frames.d.ts.map +1 -1
- package/dist/chat-protocol/nats-decoder.d.ts +0 -41
- package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
- package/dist/{chunk-OGZAFM6M.cjs → chunk-26NL6WZU.cjs} +5 -5
- package/dist/{chunk-OGZAFM6M.cjs.map → chunk-26NL6WZU.cjs.map} +1 -1
- package/dist/{chunk-QCP2KV7P.js → chunk-4SYELUXP.js} +3 -3
- package/dist/{chunk-4HGRXFQP.cjs → chunk-6PLACYFA.cjs} +3 -3
- package/dist/{chunk-4HGRXFQP.cjs.map → chunk-6PLACYFA.cjs.map} +1 -1
- package/dist/{chunk-FEVLXRTC.cjs → chunk-7LGVUXWE.cjs} +344 -282
- package/dist/chunk-7LGVUXWE.cjs.map +1 -0
- package/dist/{chunk-AKPLEWMH.js → chunk-A4BZ2CP4.js} +2 -2
- package/dist/{chunk-HHJVIHVR.cjs → chunk-COSV5FC2.cjs} +69 -69
- package/dist/{chunk-HHJVIHVR.cjs.map → chunk-COSV5FC2.cjs.map} +1 -1
- package/dist/{chunk-4WSKSNXL.js → chunk-CX62MVCV.js} +6 -6
- package/dist/{chunk-O2FTVZOD.js → chunk-DZAPMQ6U.js} +3 -3
- package/dist/{chunk-YW4WBNYV.cjs → chunk-EXUKW6GT.cjs} +24 -24
- package/dist/{chunk-YW4WBNYV.cjs.map → chunk-EXUKW6GT.cjs.map} +1 -1
- package/dist/{chunk-PWOYEUIQ.js → chunk-FN4PUTHZ.js} +5 -5
- package/dist/{chunk-L2CF2QZW.js → chunk-JDP5BGVO.js} +5 -5
- package/dist/{chunk-5Y6TPFW7.js → chunk-JFVT6XRN.js} +5 -5
- package/dist/{chunk-BCJQ7CJE.cjs → chunk-JHLZTU7R.cjs} +9 -9
- package/dist/{chunk-BCJQ7CJE.cjs.map → chunk-JHLZTU7R.cjs.map} +1 -1
- package/dist/{chunk-D44S2QLI.js → chunk-OSIIXSYG.js} +3 -3
- package/dist/{chunk-DB3TV4CW.js → chunk-PGWXVNYS.js} +3 -3
- package/dist/{chunk-H54EPP3P.cjs → chunk-PP3LCLWI.cjs} +4 -4
- package/dist/{chunk-H54EPP3P.cjs.map → chunk-PP3LCLWI.cjs.map} +1 -1
- package/dist/{chunk-M5GWCMYE.cjs → chunk-QIDD3LTS.cjs} +32 -32
- package/dist/{chunk-M5GWCMYE.cjs.map → chunk-QIDD3LTS.cjs.map} +1 -1
- package/dist/{chunk-3AX3ZEY3.js → chunk-QZSNH7XU.js} +1317 -1480
- package/dist/chunk-QZSNH7XU.js.map +1 -0
- package/dist/{chunk-37XJYOAY.cjs → chunk-RF65N4QA.cjs} +10 -10
- package/dist/{chunk-37XJYOAY.cjs.map → chunk-RF65N4QA.cjs.map} +1 -1
- package/dist/{chunk-36TJYCF3.cjs → chunk-S5BS2CZA.cjs} +21 -21
- package/dist/{chunk-36TJYCF3.cjs.map → chunk-S5BS2CZA.cjs.map} +1 -1
- package/dist/{chunk-QGJYWLF4.js → chunk-SN6SL4HR.js} +2 -2
- package/dist/{chunk-4FJSN2XW.cjs → chunk-TDM5FQZG.cjs} +10 -10
- package/dist/{chunk-4FJSN2XW.cjs.map → chunk-TDM5FQZG.cjs.map} +1 -1
- package/dist/{chunk-HGA74X35.js → chunk-TXNAZKWW.js} +3 -3
- package/dist/{chunk-IFYQ7DHV.cjs → chunk-UNVW4BVF.cjs} +43 -43
- package/dist/{chunk-IFYQ7DHV.cjs.map → chunk-UNVW4BVF.cjs.map} +1 -1
- package/dist/{chunk-CP5EYMNL.js → chunk-V5RWTIRS.js} +202 -140
- package/dist/{chunk-CP5EYMNL.js.map → chunk-V5RWTIRS.js.map} +1 -1
- package/dist/{chunk-RKGOVTXZ.cjs → chunk-VYQNM75R.cjs} +167 -330
- package/dist/chunk-VYQNM75R.cjs.map +1 -0
- package/dist/{chunk-PMENIZXI.js → chunk-WEUWTEQL.js} +4 -4
- package/dist/{chunk-H4BPHGU5.js → chunk-WW435OOF.js} +2 -2
- package/dist/{chunk-4LZ7B5H5.cjs → chunk-YB75SLEH.cjs} +3 -3
- package/dist/{chunk-4LZ7B5H5.cjs.map → chunk-YB75SLEH.cjs.map} +1 -1
- package/dist/{chunk-A6C5HOD7.cjs → chunk-YLYU6DDT.cjs} +15 -15
- package/dist/{chunk-A6C5HOD7.cjs.map → chunk-YLYU6DDT.cjs.map} +1 -1
- package/dist/{chunk-BW4WDUSI.js → chunk-ZNCYSM6R.js} +2 -2
- package/dist/{chunk-NPVDKOGD.cjs → chunk-ZQ2RBAXO.cjs} +22 -22
- package/dist/{chunk-NPVDKOGD.cjs.map → chunk-ZQ2RBAXO.cjs.map} +1 -1
- 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/index.cjs +2 -10
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +0 -1
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +1 -9
- package/dist/components/chat/stream/chat-stream-reducer.d.ts +0 -9
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
- package/dist/components/chat/stream/delta-batcher.d.ts +3 -3
- package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -1
- package/dist/components/chat/stream/message-mutations.d.ts.map +1 -1
- package/dist/components/chat/types/component.types.d.ts +0 -6
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +2 -30
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/types/network.types.d.ts +1 -3
- package/dist/components/chat/types/network.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +1 -4
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/utils/approval-display.d.ts +8 -36
- package/dist/components/chat/utils/approval-display.d.ts.map +1 -1
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
- package/dist/components/chat/utils/index.d.ts +1 -1
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts +4 -16
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/chat/utils/resolve-fetched-card-href.d.ts +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/features/push-button-selector.d.ts +29 -1
- package/dist/components/features/push-button-selector.d.ts.map +1 -1
- 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 +121 -129
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +13 -21
- package/dist/components/index.js.map +1 -1
- 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 -11
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +2 -10
- package/dist/index.cjs +7 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +6 -14
- package/dist/index.js.map +1 -1
- 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__/__snapshots__/nats-decoder-golden.test.ts.snap +0 -80
- package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +1 -118
- package/src/chat-protocol/decode.ts +3 -4
- package/src/chat-protocol/events.ts +0 -51
- package/src/chat-protocol/leading-frames.ts +3 -9
- package/src/chat-protocol/nats-decoder.ts +1 -110
- package/src/components/chat/__tests__/chat-pending-turn.test.tsx +0 -19
- package/src/components/chat/chat-message-enhanced.tsx +1 -13
- package/src/components/chat/index.ts +0 -1
- package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +0 -90
- package/src/components/chat/stream/chat-dialog-store.ts +1 -1
- package/src/components/chat/stream/chat-stream-reducer.ts +11 -49
- package/src/components/chat/stream/delta-batcher.ts +4 -9
- package/src/components/chat/stream/message-mutations.ts +0 -2
- package/src/components/chat/types/component.types.ts +0 -9
- package/src/components/chat/types/message.types.ts +0 -32
- package/src/components/chat/types/network.types.ts +1 -3
- package/src/components/chat/types/processing.types.ts +1 -4
- package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +0 -93
- package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +1 -78
- package/src/components/chat/utils/approval-display.ts +8 -53
- package/src/components/chat/utils/extract-incomplete-message-state.ts +0 -1
- package/src/components/chat/utils/index.ts +1 -6
- package/src/components/chat/utils/message-segment-accumulator.ts +3 -37
- package/src/components/chat/utils/process-historical-messages.ts +7 -44
- package/src/components/chat/utils/resolve-fetched-card-href.ts +1 -1
- package/src/components/features/.push-button-selector.md +1 -1
- package/src/components/features/push-button-selector.tsx +130 -6
- package/src/stories/ChatMessageEnhanced.stories.tsx +3 -7
- package/dist/chunk-3AX3ZEY3.js.map +0 -1
- package/dist/chunk-FEVLXRTC.cjs.map +0 -1
- package/dist/chunk-RKGOVTXZ.cjs.map +0 -1
- package/dist/components/chat/guide-display.d.ts +0 -14
- package/dist/components/chat/guide-display.d.ts.map +0 -1
- package/src/components/chat/guide-display.tsx +0 -42
- /package/dist/{chunk-QCP2KV7P.js.map → chunk-4SYELUXP.js.map} +0 -0
- /package/dist/{chunk-AKPLEWMH.js.map → chunk-A4BZ2CP4.js.map} +0 -0
- /package/dist/{chunk-4WSKSNXL.js.map → chunk-CX62MVCV.js.map} +0 -0
- /package/dist/{chunk-O2FTVZOD.js.map → chunk-DZAPMQ6U.js.map} +0 -0
- /package/dist/{chunk-PWOYEUIQ.js.map → chunk-FN4PUTHZ.js.map} +0 -0
- /package/dist/{chunk-L2CF2QZW.js.map → chunk-JDP5BGVO.js.map} +0 -0
- /package/dist/{chunk-5Y6TPFW7.js.map → chunk-JFVT6XRN.js.map} +0 -0
- /package/dist/{chunk-D44S2QLI.js.map → chunk-OSIIXSYG.js.map} +0 -0
- /package/dist/{chunk-DB3TV4CW.js.map → chunk-PGWXVNYS.js.map} +0 -0
- /package/dist/{chunk-QGJYWLF4.js.map → chunk-SN6SL4HR.js.map} +0 -0
- /package/dist/{chunk-HGA74X35.js.map → chunk-TXNAZKWW.js.map} +0 -0
- /package/dist/{chunk-PMENIZXI.js.map → chunk-WEUWTEQL.js.map} +0 -0
- /package/dist/{chunk-H4BPHGU5.js.map → chunk-WW435OOF.js.map} +0 -0
- /package/dist/{chunk-BW4WDUSI.js.map → chunk-ZNCYSM6R.js.map} +0 -0
package/package.json
CHANGED
|
@@ -254,86 +254,6 @@ exports[`decodeNatsChunk — golden corpus > maps the full recorded corpus to no
|
|
|
254
254
|
},
|
|
255
255
|
"type": "tool-execution",
|
|
256
256
|
},
|
|
257
|
-
"guide": {
|
|
258
|
-
"text": "## Enroll a device
|
|
259
|
-
1. Open Settings",
|
|
260
|
-
"type": "guide-delta",
|
|
261
|
-
},
|
|
262
|
-
"guide_empty_string": {
|
|
263
|
-
"text": "",
|
|
264
|
-
"type": "guide-delta",
|
|
265
|
-
},
|
|
266
|
-
"guide_frame_approval_batch": {
|
|
267
|
-
"approvalType": "chat",
|
|
268
|
-
"origin": "guide",
|
|
269
|
-
"requestId": "batch:prop-1",
|
|
270
|
-
"status": "pending",
|
|
271
|
-
"toolCalls": [
|
|
272
|
-
{
|
|
273
|
-
"requiresApproval": true,
|
|
274
|
-
"toolCallArguments": null,
|
|
275
|
-
"toolExecutionRequestId": "prop-1",
|
|
276
|
-
"toolName": "create_ticket",
|
|
277
|
-
"toolTitle": "Open a ticket",
|
|
278
|
-
},
|
|
279
|
-
],
|
|
280
|
-
"type": "approval-request",
|
|
281
|
-
},
|
|
282
|
-
"guide_frame_approval_request": {
|
|
283
|
-
"approvalType": "create_ticket",
|
|
284
|
-
"command": "Open a support ticket",
|
|
285
|
-
"fields": [
|
|
286
|
-
{
|
|
287
|
-
"label": "Subject",
|
|
288
|
-
"value": "Agent will not enroll",
|
|
289
|
-
},
|
|
290
|
-
],
|
|
291
|
-
"origin": "guide",
|
|
292
|
-
"requestId": "prop-1",
|
|
293
|
-
"status": "pending",
|
|
294
|
-
"type": "approval-request",
|
|
295
|
-
},
|
|
296
|
-
"guide_frame_decision_resolved": {
|
|
297
|
-
"ok": true,
|
|
298
|
-
"origin": "guide",
|
|
299
|
-
"requestId": "prop-1",
|
|
300
|
-
"status": "approved",
|
|
301
|
-
"type": "approval-resolved",
|
|
302
|
-
"willAutoContinue": false,
|
|
303
|
-
},
|
|
304
|
-
"guide_frame_metadata": {
|
|
305
|
-
"conversationId": "conv-1",
|
|
306
|
-
"origin": "guide",
|
|
307
|
-
"type": "metadata",
|
|
308
|
-
},
|
|
309
|
-
"guide_frame_payload_array": null,
|
|
310
|
-
"guide_frame_payload_empty": null,
|
|
311
|
-
"guide_frame_payload_not_object": null,
|
|
312
|
-
"guide_frame_routing": null,
|
|
313
|
-
"guide_frame_status_thinking": null,
|
|
314
|
-
"guide_frame_text_leading": {
|
|
315
|
-
"text": "I'll look that up in the docs.",
|
|
316
|
-
"type": "guide-delta",
|
|
317
|
-
},
|
|
318
|
-
"guide_frame_text_wins": {
|
|
319
|
-
"text": "body",
|
|
320
|
-
"type": "guide-delta",
|
|
321
|
-
},
|
|
322
|
-
"guide_frame_thinking": {
|
|
323
|
-
"text": "The user asked about <policies> setup",
|
|
324
|
-
"type": "thinking-delta",
|
|
325
|
-
},
|
|
326
|
-
"guide_frame_tool_error": {
|
|
327
|
-
"title": "Could not reach HubSpot",
|
|
328
|
-
"type": "error",
|
|
329
|
-
},
|
|
330
|
-
"guide_frame_usage_start": null,
|
|
331
|
-
"guide_frame_with_seq": {
|
|
332
|
-
"seq": 77,
|
|
333
|
-
"text": "Checking the docs…",
|
|
334
|
-
"type": "guide-delta",
|
|
335
|
-
},
|
|
336
|
-
"guide_missing_text": null,
|
|
337
257
|
"message_end": {
|
|
338
258
|
"type": "turn-end",
|
|
339
259
|
},
|
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { describe, it, expect } from 'vitest';
|
|
14
|
-
import
|
|
15
|
-
import { decodeNatsChunk, guideEventForNats } from '../nats-decoder';
|
|
14
|
+
import { decodeNatsChunk } from '../nats-decoder';
|
|
16
15
|
|
|
17
16
|
/** Recorded corpus of realistic NATS chunk shapes, keyed by scenario name. */
|
|
18
17
|
const CORPUS: Record<string, unknown> = {
|
|
@@ -26,73 +25,6 @@ const CORPUS: Record<string, unknown> = {
|
|
|
26
25
|
thinking: { type: 'THINKING', text: 'Considering options…' },
|
|
27
26
|
thinking_missing_text: { type: 'THINKING' },
|
|
28
27
|
|
|
29
|
-
guide: { type: 'GUIDE', text: '## Enroll a device\n1. Open Settings' },
|
|
30
|
-
guide_empty_string: { type: 'GUIDE', text: '' },
|
|
31
|
-
guide_missing_text: { type: 'GUIDE' },
|
|
32
|
-
|
|
33
|
-
// GUIDE frames — hub frames the agent re-streams verbatim inside the same
|
|
34
|
-
// chunk type, under `payload`. They cross over by DEFAULT; only the dialog's
|
|
35
|
-
// own accounting and phase stop at the seam. See `guideEventForNats`.
|
|
36
|
-
guide_frame_text_leading: {
|
|
37
|
-
type: 'GUIDE',
|
|
38
|
-
payload: { kind: 'text-leading', text: "I'll look that up in the docs." },
|
|
39
|
-
},
|
|
40
|
-
guide_frame_thinking: {
|
|
41
|
-
type: 'GUIDE',
|
|
42
|
-
payload: { kind: 'thinking-delta', text: 'The user asked about <policies> setup' },
|
|
43
|
-
},
|
|
44
|
-
guide_frame_tool_error: {
|
|
45
|
-
type: 'GUIDE',
|
|
46
|
-
payload: { kind: 'tool_error', message: 'Could not reach HubSpot' },
|
|
47
|
-
},
|
|
48
|
-
// Dropped: would blank the model badge / relabel the turn with the hub's model.
|
|
49
|
-
guide_frame_routing: {
|
|
50
|
-
type: 'GUIDE',
|
|
51
|
-
payload: { kind: 'routing', routedComplexity: 'deep', routedModel: 'claude-x', routedThinkingBudget: 8000 },
|
|
52
|
-
},
|
|
53
|
-
guide_frame_metadata: {
|
|
54
|
-
type: 'GUIDE',
|
|
55
|
-
payload: { model: 'claude-x', modelLabel: 'Guide model', conversationId: 'conv-1' },
|
|
56
|
-
},
|
|
57
|
-
// Dropped: the dialog owns its own phase and token accounting.
|
|
58
|
-
guide_frame_status_thinking: { type: 'GUIDE', payload: { status: 'thinking' } },
|
|
59
|
-
guide_frame_usage_start: { type: 'GUIDE', payload: { kind: 'usage', stage: 'start', input_tokens: 12 } },
|
|
60
|
-
// Crosses over stamped `origin: 'guide'` — the marker that keeps the card
|
|
61
|
-
// inline and sends its buttons to the hub's confirm route.
|
|
62
|
-
guide_frame_approval_request: {
|
|
63
|
-
type: 'GUIDE',
|
|
64
|
-
payload: {
|
|
65
|
-
kind: 'approval_request',
|
|
66
|
-
proposalId: 'prop-1',
|
|
67
|
-
toolName: 'create_ticket',
|
|
68
|
-
title: 'Open a support ticket',
|
|
69
|
-
fields: [{ label: 'Subject', value: 'Agent will not enroll' }],
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
guide_frame_approval_batch: {
|
|
73
|
-
type: 'GUIDE',
|
|
74
|
-
payload: {
|
|
75
|
-
kind: 'approval_batch',
|
|
76
|
-
batchId: 'batch:prop-1',
|
|
77
|
-
proposals: [{ proposalId: 'prop-1', toolName: 'create_ticket', title: 'Open a ticket' }],
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
guide_frame_decision_resolved: {
|
|
81
|
-
type: 'GUIDE',
|
|
82
|
-
payload: { kind: 'decision_resolved', proposalId: 'prop-1', ok: true, action: 'approved', willAutoContinue: false },
|
|
83
|
-
},
|
|
84
|
-
// Malformed payloads degrade to a no-op, like every other chunk shape.
|
|
85
|
-
guide_frame_payload_not_object: { type: 'GUIDE', payload: 'text-leading' },
|
|
86
|
-
guide_frame_payload_array: { type: 'GUIDE', payload: [{ kind: 'text-leading', text: 'hi' }] },
|
|
87
|
-
guide_frame_payload_empty: { type: 'GUIDE', payload: {} },
|
|
88
|
-
// `text` wins when both are present — the body is what the agent persists.
|
|
89
|
-
guide_frame_text_wins: { type: 'GUIDE', text: 'body', payload: { kind: 'thinking-delta', text: 'ignored' } },
|
|
90
|
-
guide_frame_with_seq: {
|
|
91
|
-
type: 'GUIDE',
|
|
92
|
-
streamSeq: 77,
|
|
93
|
-
payload: { kind: 'text-leading', text: 'Checking the docs…' },
|
|
94
|
-
},
|
|
95
|
-
|
|
96
28
|
// ASK — the guide-routing clarification card. `text` is the intro sentence
|
|
97
29
|
// riding the same chunk; a card without a question or without usable options
|
|
98
30
|
// is dropped (nothing the user could answer).
|
|
@@ -387,55 +319,6 @@ describe('decodeNatsChunk — golden corpus', () => {
|
|
|
387
319
|
});
|
|
388
320
|
});
|
|
389
321
|
|
|
390
|
-
describe('guideEventForNats — the two kernels reconciled', () => {
|
|
391
|
-
// The property that matters for maintenance: `leading-frames` is the ONE
|
|
392
|
-
// place a hub frame kind is taught to the client, so an event kind that
|
|
393
|
-
// reaches this adapter must cross over WITHOUT an edit here. These use event
|
|
394
|
-
// types the frame table cannot produce today on purpose — they stand in for
|
|
395
|
-
// whatever the hub ships next.
|
|
396
|
-
it('passes an unfamiliar event through unchanged', () => {
|
|
397
|
-
const ask: ChatStreamEvent = {
|
|
398
|
-
type: 'ask',
|
|
399
|
-
question: 'Which workspace?',
|
|
400
|
-
options: [{ label: 'Acme' }],
|
|
401
|
-
};
|
|
402
|
-
expect(guideEventForNats(ask)).toEqual(ask);
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
it('stamps origin on the events that carry it, so the card routes to the hub', () => {
|
|
406
|
-
expect(
|
|
407
|
-
guideEventForNats({ type: 'approval-request', requestId: 'prop-1', approvalType: 'create_ticket' }),
|
|
408
|
-
).toMatchObject({ approvalType: 'create_ticket', origin: 'guide' });
|
|
409
|
-
expect(guideEventForNats({ type: 'approval-resolved', requestId: 'prop-1', status: 'approved' })).toMatchObject({
|
|
410
|
-
origin: 'guide',
|
|
411
|
-
});
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
it('stops the events the agent owns for this dialog', () => {
|
|
415
|
-
expect(guideEventForNats({ type: 'usage', stage: 'start', input_tokens: 10 })).toBeNull();
|
|
416
|
-
expect(guideEventForNats({ type: 'status', phase: 'thinking' })).toBeNull();
|
|
417
|
-
expect(
|
|
418
|
-
guideEventForNats({
|
|
419
|
-
type: 'token-usage',
|
|
420
|
-
inputTokensSize: 1,
|
|
421
|
-
outputTokensSize: 2,
|
|
422
|
-
totalTokensSize: 3,
|
|
423
|
-
contextSize: 4,
|
|
424
|
-
}),
|
|
425
|
-
).toBeNull();
|
|
426
|
-
expect(guideEventForNats({ type: 'dialog-closed' })).toBeNull();
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
it('keeps metadata only for the hub conversation id every confirm must quote back', () => {
|
|
430
|
-
expect(guideEventForNats({ type: 'metadata', conversationId: 'conv-1', modelName: 'hub-model' })).toEqual({
|
|
431
|
-
type: 'metadata',
|
|
432
|
-
conversationId: 'conv-1',
|
|
433
|
-
origin: 'guide',
|
|
434
|
-
});
|
|
435
|
-
expect(guideEventForNats({ type: 'metadata', modelName: 'hub-model' })).toBeNull();
|
|
436
|
-
});
|
|
437
|
-
});
|
|
438
|
-
|
|
439
322
|
describe('decodeNatsChunk — seq envelope', () => {
|
|
440
323
|
it('lifts a numeric JetStream `streamSeq` into `seq`, and omits it otherwise (snapshot)', () => {
|
|
441
324
|
expect({
|
|
@@ -36,10 +36,9 @@
|
|
|
36
36
|
import type { ChatStreamEvent } from './events';
|
|
37
37
|
import type { UsageTelemetry } from './frames';
|
|
38
38
|
import { FRAME_TERMINATOR, END_OF_LEADING, TRAILER_SENTINEL } from './frames';
|
|
39
|
-
// The frame table
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// pass the SAME typeof gate the leading `usage:start` frame's already do.
|
|
39
|
+
// The frame table lives in its own module so it is reusable outside this
|
|
40
|
+
// decoder. `frameNum` comes from there too: the trailing usage frame's token
|
|
41
|
+
// counts must pass the SAME typeof gate the leading `usage:start` frame's do.
|
|
43
42
|
import { mapLeadingFrame, frameNum } from './leading-frames';
|
|
44
43
|
|
|
45
44
|
export interface SseFrameDecoder {
|
|
@@ -54,18 +54,6 @@ export interface ThinkingDeltaEvent extends ChatStreamEventBase {
|
|
|
54
54
|
text: string;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/** Guide-body delta (NATS `GUIDE` chunk) — the assistant's how-to /
|
|
58
|
-
* documentation answer, rendered as a titled "OpenFrame Guide" card rather
|
|
59
|
-
* than bare prose. Same APPEND-ONLY contract as `text-delta` /
|
|
60
|
-
* `thinking-delta`: each event carries the next verbatim slice and consumers
|
|
61
|
-
* coalesce into the trailing `guide` segment. NATS-only today — the SSE
|
|
62
|
-
* frame grammar has no guide frame — but it lives in the shared union
|
|
63
|
-
* because the reducer is transport-agnostic. */
|
|
64
|
-
export interface GuideDeltaEvent extends ChatStreamEventBase {
|
|
65
|
-
type: 'guide-delta';
|
|
66
|
-
text: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
57
|
/** Clarification card (NATS `ASK` chunk) — the assistant asking which reading
|
|
70
58
|
* of an ambiguous question to answer. NOT a delta: the chunk carries the
|
|
71
59
|
* finished card, so consumers push it whole instead of coalescing. `text` is
|
|
@@ -134,35 +122,6 @@ export interface ApprovalRequestEvent extends ChatStreamEventBase {
|
|
|
134
122
|
fields?: ApprovalRequestField[];
|
|
135
123
|
toolCalls?: ApprovalToolCall[];
|
|
136
124
|
status?: 'pending';
|
|
137
|
-
/** Set when the card came from a Product Guide frame — see {@link GuideOrigin}. */
|
|
138
|
-
origin?: GuideOrigin;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Marks an event whose payload is a Product Guide frame, whatever transport
|
|
143
|
-
* carried it. It exists because ONE stream can now mix both worlds: the agent
|
|
144
|
-
* re-streams the hub's frames into a NATS dialog, so a card typed the hub's way
|
|
145
|
-
* (`approvalType` = the tool name, resolved through the hub's confirm route)
|
|
146
|
-
* travels beside cards typed the agent's way (`approvalType` = an approval TIER
|
|
147
|
-
* routed to human escalation).
|
|
148
|
-
*
|
|
149
|
-
* Consumers read it to keep the guide half behaving exactly as it does in the
|
|
150
|
-
* hub's own chat — NOT to give it special treatment. Without it the NATS kernel
|
|
151
|
-
* would have to guess from `approvalType`, and every tool the hub adds would
|
|
152
|
-
* silently fall into the escalation path.
|
|
153
|
-
*/
|
|
154
|
-
export type GuideOrigin = 'guide';
|
|
155
|
-
|
|
156
|
-
/** The only value of {@link GuideOrigin}. Lives beside the type, and beside the
|
|
157
|
-
* predicate below, because both decoders and every consumer that branches on
|
|
158
|
-
* provenance must compare against the same token — a bare `'guide'` literal
|
|
159
|
-
* typo silently disables the branch instead of failing to compile. */
|
|
160
|
-
export const GUIDE_ORIGIN: GuideOrigin = 'guide';
|
|
161
|
-
|
|
162
|
-
/** True for anything stamped as coming from the Product Guide — a stream event
|
|
163
|
-
* or the `data` of a segment built from one. */
|
|
164
|
-
export function isGuideOrigin(source: { origin?: GuideOrigin | string } | null | undefined): boolean {
|
|
165
|
-
return source?.origin === GUIDE_ORIGIN;
|
|
166
125
|
}
|
|
167
126
|
|
|
168
127
|
/** An approval request was resolved (SSE `decision_resolved` frame /
|
|
@@ -178,8 +137,6 @@ export interface ApprovalResolvedEvent extends ChatStreamEventBase {
|
|
|
178
137
|
receiptText?: string;
|
|
179
138
|
result?: DecisionResolvedFrame['result'];
|
|
180
139
|
willAutoContinue?: boolean;
|
|
181
|
-
/** Set when the resolution came from a Product Guide frame — see {@link GuideOrigin}. */
|
|
182
|
-
origin?: GuideOrigin;
|
|
183
140
|
}
|
|
184
141
|
|
|
185
142
|
/** The client is offered a handoff of this ticket to a human technician
|
|
@@ -281,13 +238,6 @@ export interface ChatMetadataEvent extends ChatStreamEventBase {
|
|
|
281
238
|
routedModel?: string;
|
|
282
239
|
routedThinkingBudget: number | null;
|
|
283
240
|
};
|
|
284
|
-
/**
|
|
285
|
-
* Set when the metadata came from a Product Guide frame — see
|
|
286
|
-
* {@link GuideOrigin}. Such an event carries ONLY `conversationId`: it exists
|
|
287
|
-
* to record the hub's conversation id (every confirm-tool call must quote it
|
|
288
|
-
* back), NOT to describe the dialog's model, which stays the agent's.
|
|
289
|
-
*/
|
|
290
|
-
origin?: GuideOrigin;
|
|
291
241
|
}
|
|
292
242
|
|
|
293
243
|
/** SSE usage frames — raw wire keys (snake_case) preserved. */
|
|
@@ -402,7 +352,6 @@ export type ChatStreamEvent =
|
|
|
402
352
|
| TurnEndEvent
|
|
403
353
|
| TextDeltaEvent
|
|
404
354
|
| ThinkingDeltaEvent
|
|
405
|
-
| GuideDeltaEvent
|
|
406
355
|
| AskEvent
|
|
407
356
|
| StatusEvent
|
|
408
357
|
| ToolExecutionEvent
|
|
@@ -2,15 +2,9 @@
|
|
|
2
2
|
* Leading-frame → `ChatStreamEvent` table — the ONE mapping both transports read.
|
|
3
3
|
*
|
|
4
4
|
* The SSE decoder (`./decode.ts`) reaches it while walking the hub's byte
|
|
5
|
-
* framing.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* bytes a guide answer is made of are the same frames on both transports — only
|
|
9
|
-
* the envelope differs. Decoding them twice would drift the moment the hub adds
|
|
10
|
-
* a frame kind, which is why this table lives outside either decoder.
|
|
11
|
-
*
|
|
12
|
-
* The NATS side does NOT take every event this table can produce; see
|
|
13
|
-
* `guideFrameEvent` in `./nats-decoder.ts` for which ones cross over and why.
|
|
5
|
+
* framing. It lives outside that decoder so a second transport can reuse the
|
|
6
|
+
* table rather than re-implement it and drift the moment the hub adds a frame
|
|
7
|
+
* kind.
|
|
14
8
|
*
|
|
15
9
|
* Server-safe: no React, no browser APIs.
|
|
16
10
|
*/
|
|
@@ -18,9 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
import { MESSAGE_TYPE } from '../components/chat/types/message.types';
|
|
20
20
|
import type { AskOptionData } from '../components/chat/types/message.types';
|
|
21
|
-
import { ESCALATION_STATE,
|
|
21
|
+
import { ESCALATION_STATE, escalationResolvedStatus } from './events';
|
|
22
22
|
import type { ApprovalToolCall, ChatStreamEvent } from './events';
|
|
23
|
-
import { escapeThinkingTags, mapLeadingFrame } from './leading-frames';
|
|
24
23
|
|
|
25
24
|
/** Minimal structural view of a NATS chunk (see `ChunkData` in
|
|
26
25
|
* `src/components/chat/types/network.types.ts`). */
|
|
@@ -48,11 +47,6 @@ function numOr(value: unknown, fallback = 0): number {
|
|
|
48
47
|
return typeof value === 'number' ? value : fallback;
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
// `GUIDE_ORIGIN` moved to `./events`, beside the `GuideOrigin` type it is the
|
|
52
|
-
// only value of — the SSE half reads it too, so it never belonged to the NATS
|
|
53
|
-
// decoder. Re-exported here because that is the import path consumers know.
|
|
54
|
-
export { GUIDE_ORIGIN } from './events';
|
|
55
|
-
|
|
56
50
|
/** Coerce the wire's `toolCalls[]` into the batch-approval shape, dropping
|
|
57
51
|
* non-object entries and defaulting every field. */
|
|
58
52
|
function normalizeToolCalls(raw: unknown): ApprovalToolCall[] {
|
|
@@ -83,94 +77,6 @@ export function normalizeAskOptions(raw: unknown): AskOptionData[] {
|
|
|
83
77
|
.filter(option => option.label.length > 0);
|
|
84
78
|
}
|
|
85
79
|
|
|
86
|
-
/**
|
|
87
|
-
* Events whose meaning belongs to the AGENT's dialog rather than to the guide
|
|
88
|
-
* turn embedded inside it. The hub emits its own copies of these, and the Mingo
|
|
89
|
-
* dialog already has authoritative ones: letting the hub's through would
|
|
90
|
-
* double-count the dialog's tokens or fight the agent's own phase chunks.
|
|
91
|
-
*
|
|
92
|
-
* Note this is a DENYLIST, and that is the point. `./leading-frames` is the ONE
|
|
93
|
-
* place a hub frame kind is taught to the client; whatever it learns to emit —
|
|
94
|
-
* including kinds that do not exist yet — must reach a Mingo dialog without a
|
|
95
|
-
* second edit here. An allowlist would silently swallow every card the hub adds
|
|
96
|
-
* next, and the guide half would quietly lag the guide chat.
|
|
97
|
-
*/
|
|
98
|
-
const AGENT_OWNED_EVENTS: ReadonlySet<ChatStreamEvent['type']> = new Set([
|
|
99
|
-
'usage',
|
|
100
|
-
'token-usage',
|
|
101
|
-
'status',
|
|
102
|
-
'compaction',
|
|
103
|
-
'participant',
|
|
104
|
-
'dialog-closed',
|
|
105
|
-
]);
|
|
106
|
-
|
|
107
|
-
/** Event types that declare `origin` (see `GuideOrigin`). Data rather than a
|
|
108
|
-
* branch per type, so a future hub card opts in by adding the field to its
|
|
109
|
-
* interface and its name here — the pass-through below needs no edit. */
|
|
110
|
-
const ORIGIN_BEARING_EVENTS: ReadonlySet<ChatStreamEvent['type']> = new Set(['approval-request', 'approval-resolved']);
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Adapt ONE Product Guide event to the NATS kernel.
|
|
114
|
-
*
|
|
115
|
-
* The guide half of a Mingo dialog is the hub's own stream, so it is decoded by
|
|
116
|
-
* the shared table in `./leading-frames` and then passed through here — the ONE
|
|
117
|
-
* place that reconciles the two kernels. Both entry points use it: frames
|
|
118
|
-
* re-streamed by the agent inside `GUIDE` chunks, and the SSE response of the
|
|
119
|
-
* hub's confirm-tool route, which a host replays into the same dialog.
|
|
120
|
-
*
|
|
121
|
-
* Pass-through is the DEFAULT; only these rules alter an event, and each exists
|
|
122
|
-
* because the two kernels genuinely disagree:
|
|
123
|
-
*
|
|
124
|
-
* - `text-delta` becomes `guide-delta`: the body of a guide turn belongs
|
|
125
|
-
* inside the "OpenFrame Guide" card, and a `text` segment would strand part
|
|
126
|
-
* of the same answer outside it.
|
|
127
|
-
* - thinking is escaped HERE. The SSE kernel escapes `<` on the way in, the
|
|
128
|
-
* NATS kernel does not, and the guide's thinking is full of XML-ish tokens
|
|
129
|
-
* that would otherwise render as markup.
|
|
130
|
-
* - `metadata` survives ONLY to carry `conversationId`, which the hub mints
|
|
131
|
-
* and every confirm-tool call must quote back. Everything else on that
|
|
132
|
-
* frame (the hub's model, routing) is dropped: `applyNats` rebuilds the
|
|
133
|
-
* dialog's live model from a metadata event, so letting it through would
|
|
134
|
-
* relabel a Mingo turn with the hub's model, and a routing frame — which
|
|
135
|
-
* carries no model at all — would blank the badge mid-answer.
|
|
136
|
-
* - dialog-level events stop here (`AGENT_OWNED_EVENTS`).
|
|
137
|
-
*
|
|
138
|
-
* Everything else crosses over as the hub typed it, gaining only `origin`.
|
|
139
|
-
* `approvalType` in particular stays the TOOL NAME, exactly as in the hub's own
|
|
140
|
-
* chat: the NATS kernel gates approvals on approval TIER and escalates the rest,
|
|
141
|
-
* which is an agent-side concept a hub proposal has no tier for. Rewriting the
|
|
142
|
-
* tool name into a fake tier to slip past that gate would make the guide half of
|
|
143
|
-
* the stream diverge from the guide chat itself.
|
|
144
|
-
*/
|
|
145
|
-
export function guideEventForNats(event: ChatStreamEvent): ChatStreamEvent | null {
|
|
146
|
-
switch (event.type) {
|
|
147
|
-
case 'text-delta':
|
|
148
|
-
return { type: 'guide-delta', text: event.text };
|
|
149
|
-
case 'thinking-delta':
|
|
150
|
-
return { type: 'thinking-delta', text: escapeThinkingTags(event.text) };
|
|
151
|
-
case 'metadata':
|
|
152
|
-
return typeof event.conversationId === 'string' && event.conversationId
|
|
153
|
-
? { type: 'metadata', conversationId: event.conversationId, origin: GUIDE_ORIGIN }
|
|
154
|
-
: null;
|
|
155
|
-
default:
|
|
156
|
-
if (AGENT_OWNED_EVENTS.has(event.type)) return null;
|
|
157
|
-
return ORIGIN_BEARING_EVENTS.has(event.type) ? ({ ...event, origin: GUIDE_ORIGIN } as ChatStreamEvent) : event;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Decode one re-streamed Product Guide frame (a `GUIDE` chunk's `payload`) into
|
|
163
|
-
* a NATS event. The frame grammar is decoded by the shared table, never by a
|
|
164
|
-
* second copy here; the kernel reconciliation is `guideEventForNats`.
|
|
165
|
-
*/
|
|
166
|
-
export function guideFrameEvent(frame: Record<string, unknown>): ChatStreamEvent | null {
|
|
167
|
-
const events: ChatStreamEvent[] = [];
|
|
168
|
-
// Every branch of the table pushes at most one event, so the first is the one.
|
|
169
|
-
mapLeadingFrame(frame, events);
|
|
170
|
-
const event = events[0];
|
|
171
|
-
return event ? guideEventForNats(event) : null;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
80
|
/**
|
|
175
81
|
* Parse one raw NATS chunk into a normalized event. Returns `null` for
|
|
176
82
|
* unknown/malformed chunks — an unrecognized `type`, a missing required
|
|
@@ -219,21 +125,6 @@ export function decodeNatsChunk(chunk: unknown): ChatStreamEvent | null {
|
|
|
219
125
|
}
|
|
220
126
|
return null;
|
|
221
127
|
|
|
222
|
-
// Two shapes share this chunk type. `text` is the answer body the agent
|
|
223
|
-
// streams (and persists). `payload` is a Product Guide frame re-streamed
|
|
224
|
-
// verbatim from the hub — decoded through the shared frame table, with the
|
|
225
|
-
// narrowing `guideFrameEvent` documents.
|
|
226
|
-
case MESSAGE_TYPE.GUIDE: {
|
|
227
|
-
if (typeof data.text === 'string') {
|
|
228
|
-
return { type: 'guide-delta', text: data.text, ...seq };
|
|
229
|
-
}
|
|
230
|
-
const frame = data.payload;
|
|
231
|
-
if (!frame || typeof frame !== 'object' || Array.isArray(frame)) return null;
|
|
232
|
-
const event = guideFrameEvent(frame as Record<string, unknown>);
|
|
233
|
-
if (!event) return null;
|
|
234
|
-
return { ...event, ...seq };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
128
|
// An ask card is only an ask card with something to pick: a question and at
|
|
238
129
|
// least one option. Anything less is dropped rather than rendered as an
|
|
239
130
|
// empty card the user can't answer — the backend already falls back to a
|
|
@@ -203,25 +203,6 @@ describe('a thinking turn has exactly ONE pending affordance', () => {
|
|
|
203
203
|
expect(names()).toEqual(['Michael Assraf', 'Mingo']);
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
-
it('renders the assistant row when it carries ONLY a guide segment', () => {
|
|
207
|
-
// A `guide` body is the whole visible answer for a how-to question. If
|
|
208
|
-
// the empty-pending-turn predicate ever counted it as "nothing visible"
|
|
209
|
-
// (e.g. by widening its text-only clause), a guide-only turn would render
|
|
210
|
-
// NOTHING until the turn ended — the footer loader would be the sole
|
|
211
|
-
// affordance while a fully-formed guide card sat in state.
|
|
212
|
-
const withGuide: Message[] = [
|
|
213
|
-
pending[0],
|
|
214
|
-
{
|
|
215
|
-
id: 'a1',
|
|
216
|
-
role: 'assistant',
|
|
217
|
-
name: 'Mingo',
|
|
218
|
-
content: [{ type: 'guide', text: '## Enroll a device' }],
|
|
219
|
-
},
|
|
220
|
-
];
|
|
221
|
-
render(<ChatMessageList dialogId="d" messages={withGuide} isTyping />);
|
|
222
|
-
expect(names()).toEqual(['Michael Assraf', 'Mingo']);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
206
|
it('renders a completed empty-ish assistant row when NOT typing', () => {
|
|
226
207
|
render(<ChatMessageList dialogId="d" messages={pending} />);
|
|
227
208
|
expect(names()).toEqual(['Michael Assraf', 'Mingo']);
|
|
@@ -16,7 +16,6 @@ import { ContextCompactionDisplay } from './context-compaction-display';
|
|
|
16
16
|
import { BlockCard, type BlockCardProps } from './entity-cards/block-card';
|
|
17
17
|
import { ErrorMessageDisplay } from './error-message-display';
|
|
18
18
|
import { EscalationOfferMessage } from './escalation-offer-message';
|
|
19
|
-
import { GuideDisplay } from './guide-display';
|
|
20
19
|
import { remarkCardLinks } from './remark-card-links';
|
|
21
20
|
import { remarkMentionChips } from './remark-mention-chips';
|
|
22
21
|
import { remarkStripCitations } from './remark-strip-citations';
|
|
@@ -226,12 +225,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
226
225
|
// duplicates so two siblings never collide on the same React key.
|
|
227
226
|
const emittedBlockKeys = new Set<string>();
|
|
228
227
|
segments.forEach((segment, segIdx) => {
|
|
229
|
-
|
|
230
|
-
// authored by the same LLM and carries the same `[card://]` markers,
|
|
231
|
-
// so skipping it here left its markers with no `inlineByKey` entry and
|
|
232
|
-
// no hoisted card — the `<a card://>` override silently degraded them
|
|
233
|
-
// to a bare title / id.
|
|
234
|
-
if (segment.type !== 'text' && segment.type !== 'guide') return;
|
|
228
|
+
if (segment.type !== 'text') return;
|
|
235
229
|
const text = segment.text;
|
|
236
230
|
const parts: SegmentPart[] = [];
|
|
237
231
|
let cursor = 0;
|
|
@@ -608,12 +602,6 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
608
602
|
{renderSegmentBody(index, segment.text, segmentIsStreaming)}
|
|
609
603
|
</div>
|
|
610
604
|
);
|
|
611
|
-
} else if (segment.type === 'guide') {
|
|
612
|
-
return (
|
|
613
|
-
<GuideDisplay key={index}>
|
|
614
|
-
{renderSegmentBody(index, segment.text, segmentIsStreaming)}
|
|
615
|
-
</GuideDisplay>
|
|
616
|
-
);
|
|
617
605
|
} else if (segment.type === 'ask') {
|
|
618
606
|
// Only the run's head draws — the tail segments are pages of
|
|
619
607
|
// the card already rendered above them.
|
|
@@ -10,7 +10,6 @@ export * from './ai-assistant-info';
|
|
|
10
10
|
export * from './context-compaction-display';
|
|
11
11
|
export * from './expand-chevron';
|
|
12
12
|
export * from './thinking-display';
|
|
13
|
-
export * from './guide-display';
|
|
14
13
|
export * from './ask-display';
|
|
15
14
|
export * from './error-message-display';
|
|
16
15
|
export * from './chat-container';
|
|
@@ -886,96 +886,6 @@ describe('createChatStreamReducer — mergeApprovalStatuses precedence', () => {
|
|
|
886
886
|
});
|
|
887
887
|
});
|
|
888
888
|
|
|
889
|
-
// ─── GUIDE segments (#1583 threaded through the unified reader) ─────────────
|
|
890
|
-
//
|
|
891
|
-
// A `GUIDE` chunk is an APPEND-ONLY body stream like TEXT/THINKING, but it
|
|
892
|
-
// renders as a titled "OpenFrame Guide" card. It must therefore coalesce the
|
|
893
|
-
// same way (extend the trailing `guide` segment, never push a new one per
|
|
894
|
-
// chunk) and satisfy the same reducer invariants — seq idempotency,
|
|
895
|
-
// referential stability, and post-MESSAGE_END append mode.
|
|
896
|
-
|
|
897
|
-
describe('createChatStreamReducer — guide segments', () => {
|
|
898
|
-
it('coalesces consecutive guide deltas into ONE trailing guide segment', () => {
|
|
899
|
-
const r = createChatStreamReducer({ transport: 'nats' });
|
|
900
|
-
r.apply({ type: 'turn-start', seq: 1 });
|
|
901
|
-
r.apply({ type: 'guide-delta', text: '## Enroll a device\n', seq: 2 });
|
|
902
|
-
r.apply({ type: 'guide-delta', text: '1. Open Settings', seq: 3 });
|
|
903
|
-
const last = r.state.messages[r.state.messages.length - 1];
|
|
904
|
-
expect(last.segments).toEqual([{ type: 'guide', text: '## Enroll a device\n1. Open Settings' }]);
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
it('starts a NEW guide segment when a non-guide segment interrupts', () => {
|
|
908
|
-
const r = createChatStreamReducer({ transport: 'nats' });
|
|
909
|
-
r.apply({ type: 'turn-start', seq: 1 });
|
|
910
|
-
r.apply({ type: 'guide-delta', text: 'step one', seq: 2 });
|
|
911
|
-
r.apply({ type: 'text-delta', text: 'aside', seq: 3 });
|
|
912
|
-
r.apply({ type: 'guide-delta', text: 'step two', seq: 4 });
|
|
913
|
-
const last = r.state.messages[r.state.messages.length - 1];
|
|
914
|
-
expect(last.segments).toEqual([
|
|
915
|
-
{ type: 'guide', text: 'step one' },
|
|
916
|
-
{ type: 'text', text: 'aside' },
|
|
917
|
-
{ type: 'guide', text: 'step two' },
|
|
918
|
-
]);
|
|
919
|
-
});
|
|
920
|
-
|
|
921
|
-
it('drops a redelivered guide delta via the shared seq gate (no double text)', () => {
|
|
922
|
-
const r = createChatStreamReducer({ transport: 'nats' });
|
|
923
|
-
r.apply({ type: 'turn-start', seq: 1 });
|
|
924
|
-
r.apply({ type: 'guide-delta', text: 'a', seq: 2 });
|
|
925
|
-
r.apply({ type: 'guide-delta', text: 'b', seq: 3 });
|
|
926
|
-
r.apply({ type: 'guide-delta', text: 'b', seq: 3 });
|
|
927
|
-
r.apply({ type: 'guide-delta', text: 'a', seq: 2 });
|
|
928
|
-
const last = r.state.messages[r.state.messages.length - 1];
|
|
929
|
-
expect(last.segments).toEqual([{ type: 'guide', text: 'ab' }]);
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
it('reclones ONLY the touched message — earlier rows keep identity', () => {
|
|
933
|
-
const r = createChatStreamReducer({ transport: 'nats' });
|
|
934
|
-
r.apply({ type: 'participant', kind: 'message-request', text: 'how do I enroll?', seq: 1 });
|
|
935
|
-
r.apply({ type: 'turn-start', seq: 2 });
|
|
936
|
-
r.apply({ type: 'guide-delta', text: 'first', seq: 3 });
|
|
937
|
-
const userRow = r.state.messages[0];
|
|
938
|
-
r.apply({ type: 'guide-delta', text: ' more', seq: 4 });
|
|
939
|
-
expect(r.state.messages[0]).toBe(userRow);
|
|
940
|
-
});
|
|
941
|
-
|
|
942
|
-
it('post-MESSAGE_END continuation emits a DELTA guide segment with append:true', () => {
|
|
943
|
-
const effects: Array<{ name: string; args: unknown[] }> = [];
|
|
944
|
-
const r = createChatStreamReducer({
|
|
945
|
-
transport: 'nats',
|
|
946
|
-
onEffect: e => effects.push(e),
|
|
947
|
-
});
|
|
948
|
-
r.apply({ type: 'turn-start', seq: 1 });
|
|
949
|
-
r.apply({ type: 'guide-delta', text: 'in-stream ', seq: 2 });
|
|
950
|
-
r.apply({ type: 'turn-end', seq: 3 });
|
|
951
|
-
r.apply({ type: 'guide-delta', text: 'post-end', seq: 4 });
|
|
952
|
-
|
|
953
|
-
const appendEmits = effects.filter(
|
|
954
|
-
e => e.name === 'onSegmentsUpdate' && (e.args[1] as { append?: boolean } | undefined)?.append === true,
|
|
955
|
-
);
|
|
956
|
-
expect(appendEmits).toHaveLength(1);
|
|
957
|
-
expect(appendEmits[0].args[0]).toEqual([{ type: 'guide', text: 'post-end' }]);
|
|
958
|
-
// …and the bubble carries ONE coalesced guide segment, not two.
|
|
959
|
-
const last = r.state.messages[r.state.messages.length - 1];
|
|
960
|
-
expect(last.segments).toEqual([{ type: 'guide', text: 'in-stream post-end' }]);
|
|
961
|
-
});
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
/**
|
|
965
|
-
* `setMessages` is a RAW setter and must not be used to hydrate history.
|
|
966
|
-
*
|
|
967
|
-
* It assigns the thread and nothing else — in particular it does not mark the
|
|
968
|
-
* reducer resumed, which is what makes a later chunk with no preceding
|
|
969
|
-
* `turn-start` take the APPEND branch. Hydrating with it leaves the reducer
|
|
970
|
-
* looking cold, so the first such chunk goes down the cumulative path and
|
|
971
|
-
* REPLACES the restored bubble's segments. When that bubble holds a pending
|
|
972
|
-
* approval card, the control the agent is blocked on disappears with it and
|
|
973
|
-
* there is no way back.
|
|
974
|
-
*
|
|
975
|
-
* `initializeWithState` derives `resumed` from the restored thread, which is
|
|
976
|
-
* why every hydration path must go through it (the SSE adapter always did; the
|
|
977
|
-
* NATS adapter used `setMessages` and hit exactly this).
|
|
978
|
-
*/
|
|
979
889
|
describe('createChatStreamReducer — history hydration must declare resumed', () => {
|
|
980
890
|
const hydrated = () => [
|
|
981
891
|
{
|