@flamingo-stack/openframe-frontend-core 0.0.579-1952.5335.1 → 0.0.579
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 +47 -1
- package/dist/chat-protocol/events.d.ts.map +1 -1
- package/dist/chat-protocol/index.cjs +51 -0
- package/dist/chat-protocol/index.cjs.map +1 -1
- package/dist/chat-protocol/index.js +48 -1
- package/dist/chat-protocol/index.js.map +1 -1
- package/dist/chat-protocol/leading-frames.d.ts +9 -3
- package/dist/chat-protocol/leading-frames.d.ts.map +1 -1
- package/dist/chat-protocol/nats-decoder.d.ts +41 -0
- package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
- package/dist/{chunk-3B42TFEE.cjs → chunk-36TJYCF3.cjs} +27 -27
- package/dist/{chunk-3B42TFEE.cjs.map → chunk-36TJYCF3.cjs.map} +1 -1
- package/dist/{chunk-YYEVFC3K.cjs → chunk-37XJYOAY.cjs} +17 -17
- package/dist/{chunk-YYEVFC3K.cjs.map → chunk-37XJYOAY.cjs.map} +1 -1
- package/dist/{chunk-DBP7S75Q.js → chunk-3AX3ZEY3.js} +1481 -1318
- package/dist/chunk-3AX3ZEY3.js.map +1 -0
- package/dist/{chunk-DGB2FLFH.cjs → chunk-4FJSN2XW.cjs} +10 -10
- package/dist/{chunk-DGB2FLFH.cjs.map → chunk-4FJSN2XW.cjs.map} +1 -1
- package/dist/{chunk-WHSSI3S7.cjs → chunk-4HGRXFQP.cjs} +3 -3
- package/dist/{chunk-WHSSI3S7.cjs.map → chunk-4HGRXFQP.cjs.map} +1 -1
- package/dist/{chunk-SY5HCZZO.cjs → chunk-4LZ7B5H5.cjs} +5 -5
- package/dist/{chunk-SY5HCZZO.cjs.map → chunk-4LZ7B5H5.cjs.map} +1 -1
- package/dist/{chunk-XJ45MHWI.js → chunk-4WSKSNXL.js} +9 -9
- package/dist/{chunk-ZCEQLHU3.js → chunk-5Y6TPFW7.js} +5 -5
- package/dist/{chunk-EVTRRDIH.cjs → chunk-A6C5HOD7.cjs} +23 -23
- package/dist/{chunk-EVTRRDIH.cjs.map → chunk-A6C5HOD7.cjs.map} +1 -1
- package/dist/{chunk-RXZWUJEB.js → chunk-AKPLEWMH.js} +2 -2
- package/dist/{chunk-7WKFLOC7.cjs → chunk-BCJQ7CJE.cjs} +11 -11
- package/dist/{chunk-7WKFLOC7.cjs.map → chunk-BCJQ7CJE.cjs.map} +1 -1
- package/dist/{chunk-C4KL7OQI.js → chunk-BW4WDUSI.js} +3 -3
- package/dist/{chunk-L66XBQM6.js → chunk-CP5EYMNL.js} +276 -160
- package/dist/chunk-CP5EYMNL.js.map +1 -0
- package/dist/{chunk-QOPQ6BCA.js → chunk-D44S2QLI.js} +3 -3
- package/dist/{chunk-CGVPUA4X.js → chunk-DB3TV4CW.js} +6 -6
- package/dist/{chunk-PITZMTGE.cjs → chunk-FEVLXRTC.cjs} +506 -390
- package/dist/chunk-FEVLXRTC.cjs.map +1 -0
- package/dist/{chunk-TYJITV3H.js → chunk-H4BPHGU5.js} +3 -3
- package/dist/{chunk-C7Q2PVHD.cjs → chunk-H54EPP3P.cjs} +7 -7
- package/dist/{chunk-C7Q2PVHD.cjs.map → chunk-H54EPP3P.cjs.map} +1 -1
- package/dist/{chunk-NWAUHBYH.js → chunk-HGA74X35.js} +4 -4
- package/dist/{chunk-4C3MS2G6.cjs → chunk-HHJVIHVR.cjs} +81 -81
- package/dist/{chunk-4C3MS2G6.cjs.map → chunk-HHJVIHVR.cjs.map} +1 -1
- package/dist/{chunk-3XFMM34I.cjs → chunk-IFYQ7DHV.cjs} +48 -48
- package/dist/{chunk-3XFMM34I.cjs.map → chunk-IFYQ7DHV.cjs.map} +1 -1
- package/dist/{chunk-QYQXAXVR.cjs → chunk-IKDC3R7V.cjs} +4 -3
- package/dist/chunk-IKDC3R7V.cjs.map +1 -0
- package/dist/{chunk-W6JA6SFD.js → chunk-JRYS4VMV.js} +45 -45
- package/dist/chunk-JRYS4VMV.js.map +1 -0
- package/dist/{chunk-AT4WAFJO.js → chunk-L2CF2QZW.js} +7 -7
- package/dist/{chunk-2BFZHTSD.cjs → chunk-LEISUAHF.cjs} +18 -8
- package/dist/chunk-LEISUAHF.cjs.map +1 -0
- package/dist/{chunk-AAHKPPGQ.cjs → chunk-M5GWCMYE.cjs} +42 -42
- package/dist/{chunk-AAHKPPGQ.cjs.map → chunk-M5GWCMYE.cjs.map} +1 -1
- package/dist/{chunk-PQETCTJ4.js → chunk-MXOSTJAG.js} +4 -3
- package/dist/chunk-MXOSTJAG.js.map +1 -0
- package/dist/{chunk-653A7FPP.cjs → chunk-NPVDKOGD.cjs} +22 -22
- package/dist/{chunk-653A7FPP.cjs.map → chunk-NPVDKOGD.cjs.map} +1 -1
- package/dist/{chunk-2ML5QEDH.js → chunk-O2FTVZOD.js} +5 -5
- package/dist/{chunk-OCSAB6NT.cjs → chunk-OGZAFM6M.cjs} +5 -5
- package/dist/{chunk-OCSAB6NT.cjs.map → chunk-OGZAFM6M.cjs.map} +1 -1
- package/dist/{chunk-CSBNQJSI.js → chunk-PMENIZXI.js} +6 -6
- package/dist/{chunk-PFTEG6L7.js → chunk-PWOYEUIQ.js} +7 -7
- package/dist/{chunk-KXLJT5U5.js → chunk-QCP2KV7P.js} +5 -5
- package/dist/{chunk-6QDW4CDG.js → chunk-QGJYWLF4.js} +2 -2
- package/dist/{chunk-VLRXLTR4.cjs → chunk-RKGOVTXZ.cjs} +377 -214
- package/dist/chunk-RKGOVTXZ.cjs.map +1 -0
- package/dist/{chunk-HM2R5MDR.js → chunk-T5HDPHN6.js} +13 -3
- package/dist/chunk-T5HDPHN6.js.map +1 -0
- package/dist/{chunk-GP2QLZIN.cjs → chunk-YW4WBNYV.cjs} +37 -37
- package/dist/{chunk-GP2QLZIN.cjs.map → chunk-YW4WBNYV.cjs.map} +1 -1
- package/dist/{chunk-36NFJULV.cjs → chunk-ZI27ALYX.cjs} +41 -41
- package/dist/chunk-ZI27ALYX.cjs.map +1 -0
- package/dist/components/case-studies/index.cjs +12 -12
- package/dist/components/case-studies/index.js +6 -6
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/guide-display.d.ts +14 -0
- package/dist/components/chat/guide-display.d.ts.map +1 -0
- package/dist/components/chat/index.cjs +11 -3
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +10 -2
- package/dist/components/chat/stream/chat-stream-reducer.d.ts +9 -0
- 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 +6 -0
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +30 -2
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/types/network.types.d.ts +3 -1
- package/dist/components/chat/types/network.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +4 -1
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/utils/approval-display.d.ts +36 -8
- 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 +16 -4
- 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 +7 -7
- package/dist/components/contact/index.js +6 -6
- package/dist/components/docs/index.cjs +10 -10
- package/dist/components/docs/index.js +9 -9
- package/dist/components/embeds/index.cjs +7 -7
- package/dist/components/embeds/index.js +6 -6
- package/dist/components/faq/index.cjs +7 -7
- package/dist/components/faq/index.js +6 -6
- package/dist/components/features/board/board.d.ts.map +1 -1
- package/dist/components/features/index.cjs +8 -6
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +7 -5
- package/dist/components/features/sortable-list/index.d.ts +2 -1
- package/dist/components/features/sortable-list/index.d.ts.map +1 -1
- package/dist/components/features/sortable-list/sortable-list.d.ts +8 -1
- package/dist/components/features/sortable-list/sortable-list.d.ts.map +1 -1
- package/dist/components/features/sortable-list/sortable-move-buttons.d.ts +24 -0
- package/dist/components/features/sortable-list/sortable-move-buttons.d.ts.map +1 -0
- package/dist/components/features/sortable-list/use-sortable-item.d.ts +12 -4
- package/dist/components/features/sortable-list/use-sortable-item.d.ts.map +1 -1
- package/dist/components/features/ticket-status-config-list/ticket-status-config-list.d.ts +21 -5
- package/dist/components/features/ticket-status-config-list/ticket-status-config-list.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +26 -26
- package/dist/components/help-center-pages/index.js +17 -17
- package/dist/components/index.cjs +139 -129
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +26 -16
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +37 -37
- package/dist/components/meeting-scheduler/index.js +5 -5
- package/dist/components/navigation/index.cjs +9 -9
- package/dist/components/navigation/index.js +8 -8
- package/dist/components/onboarding-guides/index.cjs +9 -9
- package/dist/components/onboarding-guides/index.js +8 -8
- package/dist/components/tickets/index.cjs +10 -10
- package/dist/components/tickets/index.js +9 -9
- package/dist/components/ui/file-manager/index.cjs +4 -4
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/index.cjs +14 -6
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +13 -5
- package/dist/components/ui/input.d.ts +2 -0
- package/dist/components/ui/input.d.ts.map +1 -1
- package/dist/components/ui/textarea.d.ts +2 -0
- package/dist/components/ui/textarea.d.ts.map +1 -1
- package/dist/components/ui/ticket-status-config-row.d.ts +10 -4
- package/dist/components/ui/ticket-status-config-row.d.ts.map +1 -1
- package/dist/hooks/index.cjs +7 -3
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +6 -2
- package/dist/hooks/ui/index.d.ts +1 -0
- package/dist/hooks/ui/index.d.ts.map +1 -1
- package/dist/hooks/ui/use-drag-and-drop-enabled.d.ts +31 -0
- package/dist/hooks/ui/use-drag-and-drop-enabled.d.ts.map +1 -0
- package/dist/hooks/ui/use-media-query.d.ts.map +1 -1
- package/dist/index.cjs +25 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +23 -9
- 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 -4
- package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +80 -0
- package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +118 -1
- package/src/chat-protocol/decode.ts +4 -3
- package/src/chat-protocol/events.ts +51 -0
- package/src/chat-protocol/leading-frames.ts +9 -3
- package/src/chat-protocol/nats-decoder.ts +110 -1
- package/src/components/chat/__tests__/chat-pending-turn.test.tsx +19 -0
- package/src/components/chat/chat-message-enhanced.tsx +13 -1
- package/src/components/chat/guide-display.tsx +42 -0
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +90 -0
- package/src/components/chat/stream/chat-dialog-store.ts +1 -1
- package/src/components/chat/stream/chat-stream-reducer.ts +49 -11
- package/src/components/chat/stream/delta-batcher.ts +9 -4
- package/src/components/chat/stream/message-mutations.ts +2 -0
- package/src/components/chat/types/component.types.ts +9 -0
- package/src/components/chat/types/message.types.ts +32 -0
- package/src/components/chat/types/network.types.ts +3 -1
- package/src/components/chat/types/processing.types.ts +4 -1
- package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +93 -0
- package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +78 -1
- package/src/components/chat/utils/approval-display.ts +53 -8
- package/src/components/chat/utils/extract-incomplete-message-state.ts +1 -0
- package/src/components/chat/utils/index.ts +6 -1
- package/src/components/chat/utils/message-segment-accumulator.ts +37 -3
- package/src/components/chat/utils/process-historical-messages.ts +44 -7
- package/src/components/chat/utils/resolve-fetched-card-href.ts +1 -1
- package/src/components/features/board/.board.md +1 -0
- package/src/components/features/board/board.tsx +11 -1
- package/src/components/features/sortable-list/.sortable-list.md +23 -12
- package/src/components/features/sortable-list/index.ts +2 -1
- package/src/components/features/sortable-list/sortable-list.tsx +16 -5
- package/src/components/features/sortable-list/sortable-move-buttons.tsx +81 -0
- package/src/components/features/sortable-list/use-sortable-item.ts +25 -13
- package/src/components/features/ticket-status-config-list/ticket-status-config-list.tsx +55 -62
- package/src/components/ui/.ticket-status-config-row.md +11 -9
- package/src/components/ui/input.tsx +5 -2
- package/src/components/ui/textarea.tsx +5 -2
- package/src/components/ui/ticket-status-config-row.tsx +162 -70
- package/src/hooks/ui/.index.md +1 -0
- package/src/hooks/ui/.use-drag-and-drop-enabled.md +32 -0
- package/src/hooks/ui/index.ts +1 -0
- package/src/hooks/ui/use-drag-and-drop-enabled.ts +37 -0
- package/src/hooks/ui/use-media-query.ts +3 -2
- package/src/stories/ChatMessageEnhanced.stories.tsx +7 -3
- package/dist/chunk-2BFZHTSD.cjs.map +0 -1
- package/dist/chunk-36NFJULV.cjs.map +0 -1
- package/dist/chunk-DBP7S75Q.js.map +0 -1
- package/dist/chunk-HM2R5MDR.js.map +0 -1
- package/dist/chunk-L66XBQM6.js.map +0 -1
- package/dist/chunk-PITZMTGE.cjs.map +0 -1
- package/dist/chunk-PQETCTJ4.js.map +0 -1
- package/dist/chunk-QYQXAXVR.cjs.map +0 -1
- package/dist/chunk-VLRXLTR4.cjs.map +0 -1
- package/dist/chunk-W6JA6SFD.js.map +0 -1
- /package/dist/{chunk-XJ45MHWI.js.map → chunk-4WSKSNXL.js.map} +0 -0
- /package/dist/{chunk-ZCEQLHU3.js.map → chunk-5Y6TPFW7.js.map} +0 -0
- /package/dist/{chunk-RXZWUJEB.js.map → chunk-AKPLEWMH.js.map} +0 -0
- /package/dist/{chunk-C4KL7OQI.js.map → chunk-BW4WDUSI.js.map} +0 -0
- /package/dist/{chunk-QOPQ6BCA.js.map → chunk-D44S2QLI.js.map} +0 -0
- /package/dist/{chunk-CGVPUA4X.js.map → chunk-DB3TV4CW.js.map} +0 -0
- /package/dist/{chunk-TYJITV3H.js.map → chunk-H4BPHGU5.js.map} +0 -0
- /package/dist/{chunk-NWAUHBYH.js.map → chunk-HGA74X35.js.map} +0 -0
- /package/dist/{chunk-AT4WAFJO.js.map → chunk-L2CF2QZW.js.map} +0 -0
- /package/dist/{chunk-2ML5QEDH.js.map → chunk-O2FTVZOD.js.map} +0 -0
- /package/dist/{chunk-CSBNQJSI.js.map → chunk-PMENIZXI.js.map} +0 -0
- /package/dist/{chunk-PFTEG6L7.js.map → chunk-PWOYEUIQ.js.map} +0 -0
- /package/dist/{chunk-KXLJT5U5.js.map → chunk-QCP2KV7P.js.map} +0 -0
- /package/dist/{chunk-6QDW4CDG.js.map → chunk-QGJYWLF4.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flamingo-stack/openframe-frontend-core",
|
|
3
|
-
"version": "0.0.579
|
|
3
|
+
"version": "0.0.579",
|
|
4
4
|
"description": "Shared design system and components for all Flamingo platforms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -350,9 +350,6 @@
|
|
|
350
350
|
"@atlaskit/pragmatic-drag-and-drop": "^3.0.0",
|
|
351
351
|
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^3.0.1",
|
|
352
352
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.1.0",
|
|
353
|
-
"@dnd-kit/core": "^6.3.1",
|
|
354
|
-
"@dnd-kit/sortable": "^10.0.0",
|
|
355
|
-
"@dnd-kit/utilities": "^3.2.2",
|
|
356
353
|
"@eslint/js": "^9.39.0",
|
|
357
354
|
"@floating-ui/react": "^0.27.15",
|
|
358
355
|
"@headlessui/react": "^2.2.9",
|
|
@@ -254,6 +254,86 @@ 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,
|
|
257
337
|
"message_end": {
|
|
258
338
|
"type": "turn-end",
|
|
259
339
|
},
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { describe, it, expect } from 'vitest';
|
|
14
|
-
import {
|
|
14
|
+
import type { ChatStreamEvent } from '../events';
|
|
15
|
+
import { decodeNatsChunk, guideEventForNats } from '../nats-decoder';
|
|
15
16
|
|
|
16
17
|
/** Recorded corpus of realistic NATS chunk shapes, keyed by scenario name. */
|
|
17
18
|
const CORPUS: Record<string, unknown> = {
|
|
@@ -25,6 +26,73 @@ const CORPUS: Record<string, unknown> = {
|
|
|
25
26
|
thinking: { type: 'THINKING', text: 'Considering options…' },
|
|
26
27
|
thinking_missing_text: { type: 'THINKING' },
|
|
27
28
|
|
|
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
|
+
|
|
28
96
|
// ASK — the guide-routing clarification card. `text` is the intro sentence
|
|
29
97
|
// riding the same chunk; a card without a question or without usable options
|
|
30
98
|
// is dropped (nothing the user could answer).
|
|
@@ -319,6 +387,55 @@ describe('decodeNatsChunk — golden corpus', () => {
|
|
|
319
387
|
});
|
|
320
388
|
});
|
|
321
389
|
|
|
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
|
+
|
|
322
439
|
describe('decodeNatsChunk — seq envelope', () => {
|
|
323
440
|
it('lifts a numeric JetStream `streamSeq` into `seq`, and omits it otherwise (snapshot)', () => {
|
|
324
441
|
expect({
|
|
@@ -36,9 +36,10 @@
|
|
|
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
|
-
//
|
|
39
|
+
// The frame table is SHARED with the NATS decoder — guide answers re-stream the
|
|
40
|
+
// hub's frames verbatim inside `GUIDE` chunks — so it lives in its own module.
|
|
41
|
+
// `frameNum` comes from there too: the trailing usage frame's token counts must
|
|
42
|
+
// pass the SAME typeof gate the leading `usage:start` frame's already do.
|
|
42
43
|
import { mapLeadingFrame, frameNum } from './leading-frames';
|
|
43
44
|
|
|
44
45
|
export interface SseFrameDecoder {
|
|
@@ -54,6 +54,18 @@ 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
|
+
|
|
57
69
|
/** Clarification card (NATS `ASK` chunk) — the assistant asking which reading
|
|
58
70
|
* of an ambiguous question to answer. NOT a delta: the chunk carries the
|
|
59
71
|
* finished card, so consumers push it whole instead of coalescing. `text` is
|
|
@@ -122,6 +134,35 @@ export interface ApprovalRequestEvent extends ChatStreamEventBase {
|
|
|
122
134
|
fields?: ApprovalRequestField[];
|
|
123
135
|
toolCalls?: ApprovalToolCall[];
|
|
124
136
|
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;
|
|
125
166
|
}
|
|
126
167
|
|
|
127
168
|
/** An approval request was resolved (SSE `decision_resolved` frame /
|
|
@@ -137,6 +178,8 @@ export interface ApprovalResolvedEvent extends ChatStreamEventBase {
|
|
|
137
178
|
receiptText?: string;
|
|
138
179
|
result?: DecisionResolvedFrame['result'];
|
|
139
180
|
willAutoContinue?: boolean;
|
|
181
|
+
/** Set when the resolution came from a Product Guide frame — see {@link GuideOrigin}. */
|
|
182
|
+
origin?: GuideOrigin;
|
|
140
183
|
}
|
|
141
184
|
|
|
142
185
|
/** The client is offered a handoff of this ticket to a human technician
|
|
@@ -238,6 +281,13 @@ export interface ChatMetadataEvent extends ChatStreamEventBase {
|
|
|
238
281
|
routedModel?: string;
|
|
239
282
|
routedThinkingBudget: number | null;
|
|
240
283
|
};
|
|
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;
|
|
241
291
|
}
|
|
242
292
|
|
|
243
293
|
/** SSE usage frames — raw wire keys (snake_case) preserved. */
|
|
@@ -352,6 +402,7 @@ export type ChatStreamEvent =
|
|
|
352
402
|
| TurnEndEvent
|
|
353
403
|
| TextDeltaEvent
|
|
354
404
|
| ThinkingDeltaEvent
|
|
405
|
+
| GuideDeltaEvent
|
|
355
406
|
| AskEvent
|
|
356
407
|
| StatusEvent
|
|
357
408
|
| ToolExecutionEvent
|
|
@@ -2,9 +2,15 @@
|
|
|
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
|
-
*
|
|
5
|
+
* framing. The NATS decoder (`./nats-decoder.ts`) reaches it for `GUIDE` chunks
|
|
6
|
+
* carrying a `payload`: the saas-ai-agent re-streams the Product Guide's hub
|
|
7
|
+
* frames VERBATIM into the Mingo chunk stream (`GuideStreamingService`), so the
|
|
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.
|
|
8
14
|
*
|
|
9
15
|
* Server-safe: no React, no browser APIs.
|
|
10
16
|
*/
|
|
@@ -18,8 +18,9 @@
|
|
|
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, escalationResolvedStatus } from './events';
|
|
21
|
+
import { ESCALATION_STATE, GUIDE_ORIGIN, escalationResolvedStatus } from './events';
|
|
22
22
|
import type { ApprovalToolCall, ChatStreamEvent } from './events';
|
|
23
|
+
import { escapeThinkingTags, mapLeadingFrame } from './leading-frames';
|
|
23
24
|
|
|
24
25
|
/** Minimal structural view of a NATS chunk (see `ChunkData` in
|
|
25
26
|
* `src/components/chat/types/network.types.ts`). */
|
|
@@ -47,6 +48,11 @@ function numOr(value: unknown, fallback = 0): number {
|
|
|
47
48
|
return typeof value === 'number' ? value : fallback;
|
|
48
49
|
}
|
|
49
50
|
|
|
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
|
+
|
|
50
56
|
/** Coerce the wire's `toolCalls[]` into the batch-approval shape, dropping
|
|
51
57
|
* non-object entries and defaulting every field. */
|
|
52
58
|
function normalizeToolCalls(raw: unknown): ApprovalToolCall[] {
|
|
@@ -77,6 +83,94 @@ export function normalizeAskOptions(raw: unknown): AskOptionData[] {
|
|
|
77
83
|
.filter(option => option.label.length > 0);
|
|
78
84
|
}
|
|
79
85
|
|
|
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
|
+
|
|
80
174
|
/**
|
|
81
175
|
* Parse one raw NATS chunk into a normalized event. Returns `null` for
|
|
82
176
|
* unknown/malformed chunks — an unrecognized `type`, a missing required
|
|
@@ -125,6 +219,21 @@ export function decodeNatsChunk(chunk: unknown): ChatStreamEvent | null {
|
|
|
125
219
|
}
|
|
126
220
|
return null;
|
|
127
221
|
|
|
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
|
+
|
|
128
237
|
// An ask card is only an ask card with something to pick: a question and at
|
|
129
238
|
// least one option. Anything less is dropped rather than rendered as an
|
|
130
239
|
// empty card the user can't answer — the backend already falls back to a
|
|
@@ -203,6 +203,25 @@ 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
|
+
|
|
206
225
|
it('renders a completed empty-ish assistant row when NOT typing', () => {
|
|
207
226
|
render(<ChatMessageList dialogId="d" messages={pending} />);
|
|
208
227
|
expect(names()).toEqual(['Michael Assraf', 'Mingo']);
|
|
@@ -16,6 +16,7 @@ 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';
|
|
19
20
|
import { remarkCardLinks } from './remark-card-links';
|
|
20
21
|
import { remarkMentionChips } from './remark-mention-chips';
|
|
21
22
|
import { remarkStripCitations } from './remark-strip-citations';
|
|
@@ -225,7 +226,12 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
225
226
|
// duplicates so two siblings never collide on the same React key.
|
|
226
227
|
const emittedBlockKeys = new Set<string>();
|
|
227
228
|
segments.forEach((segment, segIdx) => {
|
|
228
|
-
|
|
229
|
+
// Both markdown-bearing segment types take part: a `guide` body is
|
|
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;
|
|
229
235
|
const text = segment.text;
|
|
230
236
|
const parts: SegmentPart[] = [];
|
|
231
237
|
let cursor = 0;
|
|
@@ -602,6 +608,12 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
602
608
|
{renderSegmentBody(index, segment.text, segmentIsStreaming)}
|
|
603
609
|
</div>
|
|
604
610
|
);
|
|
611
|
+
} else if (segment.type === 'guide') {
|
|
612
|
+
return (
|
|
613
|
+
<GuideDisplay key={index}>
|
|
614
|
+
{renderSegmentBody(index, segment.text, segmentIsStreaming)}
|
|
615
|
+
</GuideDisplay>
|
|
616
|
+
);
|
|
605
617
|
} else if (segment.type === 'ask') {
|
|
606
618
|
// Only the run's head draws — the tail segments are pages of
|
|
607
619
|
// the card already rendered above them.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
|
|
5
|
+
import { cn } from '../../utils/cn';
|
|
6
|
+
import { CompassIcon } from '../icons-v2-generated';
|
|
7
|
+
import type { GuideDisplayProps } from './types';
|
|
8
|
+
|
|
9
|
+
const GUIDE_LABEL = 'OpenFrame Guide';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `GUIDE` segment — the assistant's how-to/documentation answer, framed as a
|
|
13
|
+
* titled card instead of a bare paragraph.
|
|
14
|
+
*
|
|
15
|
+
* Chrome only: the caller renders the markdown body and passes it as children,
|
|
16
|
+
* so a guide goes through the SAME body pipeline as a text segment
|
|
17
|
+
* (`[card://]` splitting + hoisted entity cards + mention chips). Rendering the
|
|
18
|
+
* raw markdown here instead would bypass that plan and degrade every card
|
|
19
|
+
* marker inside a guide to a bare title.
|
|
20
|
+
*/
|
|
21
|
+
const GuideDisplay = forwardRef<HTMLDivElement, GuideDisplayProps>(({ className, children, ...props }, ref) => {
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
ref={ref}
|
|
25
|
+
className={cn(
|
|
26
|
+
'flex flex-col gap-[var(--spacing-system-m)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-m)]',
|
|
27
|
+
className,
|
|
28
|
+
)}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
<div className="flex items-center gap-[var(--spacing-system-xs)]">
|
|
32
|
+
<CompassIcon size={16} className="shrink-0 text-ods-open-yellow" />
|
|
33
|
+
<span className="text-ods-open-yellow text-h5">{GUIDE_LABEL}</span>
|
|
34
|
+
</div>
|
|
35
|
+
<div className="w-full min-w-0 break-words text-ods-text-primary text-h4">{children}</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
GuideDisplay.displayName = 'GuideDisplay';
|
|
41
|
+
|
|
42
|
+
export { GuideDisplay };
|
|
@@ -10,6 +10,7 @@ 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';
|
|
13
14
|
export * from './ask-display';
|
|
14
15
|
export * from './error-message-display';
|
|
15
16
|
export * from './chat-container';
|