@flamingo-stack/openframe-frontend-core 0.0.532 → 0.0.533

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/dist/chat-protocol/confirm-tool.d.ts +50 -0
  2. package/dist/chat-protocol/confirm-tool.d.ts.map +1 -0
  3. package/dist/chat-protocol/decode.d.ts +1 -9
  4. package/dist/chat-protocol/decode.d.ts.map +1 -1
  5. package/dist/chat-protocol/events.d.ts +35 -0
  6. package/dist/chat-protocol/events.d.ts.map +1 -1
  7. package/dist/chat-protocol/index.cjs +72 -3
  8. package/dist/chat-protocol/index.cjs.map +1 -1
  9. package/dist/chat-protocol/index.d.ts +1 -0
  10. package/dist/chat-protocol/index.d.ts.map +1 -1
  11. package/dist/chat-protocol/index.js +67 -4
  12. package/dist/chat-protocol/index.js.map +1 -1
  13. package/dist/chat-protocol/leading-frames.d.ts +38 -0
  14. package/dist/chat-protocol/leading-frames.d.ts.map +1 -0
  15. package/dist/chat-protocol/nats-decoder.d.ts +41 -0
  16. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
  17. package/dist/{chunk-QEMI34VA.cjs → chunk-2MYDGLJS.cjs} +87 -87
  18. package/dist/{chunk-QEMI34VA.cjs.map → chunk-2MYDGLJS.cjs.map} +1 -1
  19. package/dist/{chunk-TQ7EPKKO.js → chunk-2TUOBFPK.js} +2 -2
  20. package/dist/{chunk-XBYEPSNZ.js → chunk-2XGHEJZK.js} +4 -4
  21. package/dist/{chunk-OAEJWP7H.js → chunk-46FKXLNE.js} +2 -2
  22. package/dist/{chunk-ECR37JHH.js → chunk-4QKXNWH5.js} +2 -2
  23. package/dist/{chunk-GWFC3NE5.cjs → chunk-4ZRHVOEB.cjs} +31 -31
  24. package/dist/{chunk-GWFC3NE5.cjs.map → chunk-4ZRHVOEB.cjs.map} +1 -1
  25. package/dist/{chunk-AZBS4PZG.js → chunk-5U67WTAS.js} +2 -2
  26. package/dist/{chunk-3G7FDE5E.js → chunk-6SMLYZLH.js} +4 -4
  27. package/dist/{chunk-7SGBF2I2.js → chunk-6W3ULZ4X.js} +6 -6
  28. package/dist/{chunk-ALXLWF75.js → chunk-6YXN5RHW.js} +2 -2
  29. package/dist/{chunk-2QP67D2L.cjs → chunk-DFQLUK75.cjs} +3 -3
  30. package/dist/{chunk-2QP67D2L.cjs.map → chunk-DFQLUK75.cjs.map} +1 -1
  31. package/dist/{chunk-ETF2QXXU.cjs → chunk-ENPGPZIB.cjs} +61 -61
  32. package/dist/{chunk-ETF2QXXU.cjs.map → chunk-ENPGPZIB.cjs.map} +1 -1
  33. package/dist/{chunk-HJDCDOJM.cjs → chunk-GNOK5J34.cjs} +37 -37
  34. package/dist/{chunk-HJDCDOJM.cjs.map → chunk-GNOK5J34.cjs.map} +1 -1
  35. package/dist/{chunk-4GL2XJ5C.cjs → chunk-J4BE4FIR.cjs} +4 -4
  36. package/dist/{chunk-4GL2XJ5C.cjs.map → chunk-J4BE4FIR.cjs.map} +1 -1
  37. package/dist/{chunk-HXHA3DCI.cjs → chunk-JDW2IL7A.cjs} +11 -11
  38. package/dist/{chunk-HXHA3DCI.cjs.map → chunk-JDW2IL7A.cjs.map} +1 -1
  39. package/dist/{chunk-TMZS63G2.cjs → chunk-KMB66NYX.cjs} +7 -7
  40. package/dist/{chunk-TMZS63G2.cjs.map → chunk-KMB66NYX.cjs.map} +1 -1
  41. package/dist/{chunk-5W3RCZIK.js → chunk-LE3BJ5H2.js} +2 -2
  42. package/dist/{chunk-WINT757E.js → chunk-LIUARTOZ.js} +206 -64
  43. package/dist/chunk-LIUARTOZ.js.map +1 -0
  44. package/dist/{chunk-65NR333L.js → chunk-MVGL5NNJ.js} +2 -2
  45. package/dist/{chunk-LYWSHNK4.cjs → chunk-NAV4FRNL.cjs} +26 -26
  46. package/dist/{chunk-LYWSHNK4.cjs.map → chunk-NAV4FRNL.cjs.map} +1 -1
  47. package/dist/{chunk-2MBS3YEY.cjs → chunk-OLOKQQAP.cjs} +5 -5
  48. package/dist/{chunk-2MBS3YEY.cjs.map → chunk-OLOKQQAP.cjs.map} +1 -1
  49. package/dist/{chunk-QYNLVIGQ.cjs → chunk-QZ2NAPLF.cjs} +9 -9
  50. package/dist/{chunk-QYNLVIGQ.cjs.map → chunk-QZ2NAPLF.cjs.map} +1 -1
  51. package/dist/{chunk-L6EKEYF7.cjs → chunk-RKITTO5V.cjs} +14 -14
  52. package/dist/{chunk-L6EKEYF7.cjs.map → chunk-RKITTO5V.cjs.map} +1 -1
  53. package/dist/{chunk-UP4RA54U.js → chunk-U555TMPC.js} +5 -5
  54. package/dist/{chunk-GVLA2BPW.cjs → chunk-UPHN2VRZ.cjs} +259 -117
  55. package/dist/chunk-UPHN2VRZ.cjs.map +1 -0
  56. package/dist/{chunk-IZOGFZ7N.js → chunk-XN4JKIJY.js} +2 -2
  57. package/dist/{chunk-KYQZBQYV.js → chunk-YLB6XXO6.js} +2 -2
  58. package/dist/{chunk-JZ5XDZYD.cjs → chunk-Z3NOI7X7.cjs} +26 -26
  59. package/dist/{chunk-JZ5XDZYD.cjs.map → chunk-Z3NOI7X7.cjs.map} +1 -1
  60. package/dist/components/case-studies/index.cjs +8 -8
  61. package/dist/components/case-studies/index.js +2 -2
  62. package/dist/components/chat/embeddable-chat.d.ts +13 -0
  63. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  64. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  65. package/dist/components/chat/index.cjs +10 -2
  66. package/dist/components/chat/index.cjs.map +1 -1
  67. package/dist/components/chat/index.js +9 -1
  68. package/dist/components/chat/stream/chat-stream-reducer.d.ts +9 -0
  69. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
  70. package/dist/components/chat/types/message.types.d.ts +15 -0
  71. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  72. package/dist/components/chat/types/network.types.d.ts +4 -0
  73. package/dist/components/chat/types/network.types.d.ts.map +1 -1
  74. package/dist/components/chat/types/processing.types.d.ts +7 -1
  75. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  76. package/dist/components/chat/utils/approval-display.d.ts +49 -0
  77. package/dist/components/chat/utils/approval-display.d.ts.map +1 -0
  78. package/dist/components/chat/utils/index.d.ts +1 -0
  79. package/dist/components/chat/utils/index.d.ts.map +1 -1
  80. package/dist/components/chat/utils/message-segment-accumulator.d.ts +25 -3
  81. package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
  82. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  83. package/dist/components/contact/index.cjs +3 -3
  84. package/dist/components/contact/index.js +2 -2
  85. package/dist/components/docs/index.cjs +5 -5
  86. package/dist/components/docs/index.js +4 -4
  87. package/dist/components/embeds/index.cjs +3 -3
  88. package/dist/components/embeds/index.js +2 -2
  89. package/dist/components/faq/index.cjs +3 -3
  90. package/dist/components/faq/index.js +2 -2
  91. package/dist/components/features/index.cjs +2 -2
  92. package/dist/components/features/index.js +1 -1
  93. package/dist/components/help-center-pages/index.cjs +22 -22
  94. package/dist/components/help-center-pages/index.js +13 -13
  95. package/dist/components/index.cjs +140 -132
  96. package/dist/components/index.cjs.map +1 -1
  97. package/dist/components/index.js +19 -11
  98. package/dist/components/index.js.map +1 -1
  99. package/dist/components/meeting-scheduler/index.cjs +34 -34
  100. package/dist/components/meeting-scheduler/index.js +3 -3
  101. package/dist/components/navigation/index.cjs +2 -2
  102. package/dist/components/navigation/index.js +1 -1
  103. package/dist/components/onboarding-guides/index.cjs +5 -5
  104. package/dist/components/onboarding-guides/index.js +4 -4
  105. package/dist/components/related-content/index.cjs +3 -3
  106. package/dist/components/related-content/index.js +2 -2
  107. package/dist/components/tickets/index.cjs +6 -6
  108. package/dist/components/tickets/index.js +5 -5
  109. package/dist/components/ui/index.cjs +10 -2
  110. package/dist/components/ui/index.cjs.map +1 -1
  111. package/dist/components/ui/index.js +9 -1
  112. package/dist/index.cjs +10 -2
  113. package/dist/index.cjs.map +1 -1
  114. package/dist/index.js +9 -1
  115. package/dist/utils/index.cjs.map +1 -1
  116. package/dist/utils/index.js.map +1 -1
  117. package/package.json +1 -1
  118. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +70 -0
  119. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +108 -1
  120. package/src/chat-protocol/confirm-tool.ts +67 -0
  121. package/src/chat-protocol/decode.ts +7 -153
  122. package/src/chat-protocol/events.ts +40 -0
  123. package/src/chat-protocol/index.ts +3 -0
  124. package/src/chat-protocol/leading-frames.ts +175 -0
  125. package/src/chat-protocol/nats-decoder.ts +111 -3
  126. package/src/components/chat/embeddable-chat.tsx +18 -0
  127. package/src/components/chat/hooks/use-sse-chat-adapter.ts +7 -16
  128. package/src/components/chat/stream/chat-stream-reducer.ts +49 -3
  129. package/src/components/chat/types/message.types.ts +15 -0
  130. package/src/components/chat/types/network.types.ts +4 -0
  131. package/src/components/chat/types/processing.types.ts +12 -1
  132. package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +92 -0
  133. package/src/components/chat/utils/approval-display.ts +70 -0
  134. package/src/components/chat/utils/index.ts +9 -0
  135. package/src/components/chat/utils/message-segment-accumulator.ts +62 -32
  136. package/src/components/chat/utils/process-historical-messages.ts +40 -6
  137. package/dist/chunk-GVLA2BPW.cjs.map +0 -1
  138. package/dist/chunk-WINT757E.js.map +0 -1
  139. /package/dist/{chunk-TQ7EPKKO.js.map → chunk-2TUOBFPK.js.map} +0 -0
  140. /package/dist/{chunk-XBYEPSNZ.js.map → chunk-2XGHEJZK.js.map} +0 -0
  141. /package/dist/{chunk-OAEJWP7H.js.map → chunk-46FKXLNE.js.map} +0 -0
  142. /package/dist/{chunk-ECR37JHH.js.map → chunk-4QKXNWH5.js.map} +0 -0
  143. /package/dist/{chunk-AZBS4PZG.js.map → chunk-5U67WTAS.js.map} +0 -0
  144. /package/dist/{chunk-3G7FDE5E.js.map → chunk-6SMLYZLH.js.map} +0 -0
  145. /package/dist/{chunk-7SGBF2I2.js.map → chunk-6W3ULZ4X.js.map} +0 -0
  146. /package/dist/{chunk-ALXLWF75.js.map → chunk-6YXN5RHW.js.map} +0 -0
  147. /package/dist/{chunk-5W3RCZIK.js.map → chunk-LE3BJ5H2.js.map} +0 -0
  148. /package/dist/{chunk-65NR333L.js.map → chunk-MVGL5NNJ.js.map} +0 -0
  149. /package/dist/{chunk-UP4RA54U.js.map → chunk-U555TMPC.js.map} +0 -0
  150. /package/dist/{chunk-IZOGFZ7N.js.map → chunk-XN4JKIJY.js.map} +0 -0
  151. /package/dist/{chunk-KYQZBQYV.js.map → chunk-YLB6XXO6.js.map} +0 -0
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Leading-frame → `ChatStreamEvent` table — the ONE mapping both transports read.
3
+ *
4
+ * The SSE decoder (`./decode.ts`) reaches it while walking the hub's byte
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.
14
+ *
15
+ * Server-safe: no React, no browser APIs.
16
+ */
17
+
18
+ import type { ChatStreamEvent } from './events'
19
+
20
+ /**
21
+ * Escape `<` so markdown renderers that pass HTML through (rehypeRaw)
22
+ * don't treat XML-like tokens in Claude's thinking output as elements.
23
+ * `<` → `&lt;` preserves the visible character without breaking
24
+ * blockquote `>` markers. Per-character, so it distributes over
25
+ * concatenation: escape(a + b) === escape(a) + escape(b) — callers may
26
+ * apply it per-delta or on the accumulated string interchangeably.
27
+ */
28
+ export function escapeThinkingTags(text: string): string {
29
+ return text.replace(/</g, '&lt;')
30
+ }
31
+
32
+ /**
33
+ * Map one parsed leading frame to normalized events, replicating the
34
+ * legacy parser's else-if chain ORDER and its exact truthiness/typeof
35
+ * gates. Frames that matched a branch but failed its inner validation
36
+ * (e.g. `routing` without a string `routedComplexity`) produce NO event,
37
+ * exactly like the legacy no-op.
38
+ *
39
+ * Every branch pushes AT MOST ONE event — `guideFrameEvent` relies on that
40
+ * to hand a single event back to `decodeNatsChunk`'s per-chunk contract.
41
+ */
42
+ export function mapLeadingFrame(meta: any, out: ChatStreamEvent[]): void {
43
+ if (meta.status === 'thinking') {
44
+ out.push({ type: 'status', phase: 'thinking' })
45
+ } else if (meta.kind === 'thinking-delta' && typeof meta.text === 'string') {
46
+ // Wire is ALREADY delta — emit verbatim, append-only contract.
47
+ out.push({ type: 'thinking-delta', text: meta.text })
48
+ } else if (meta.kind === 'usage' && meta.stage === 'start') {
49
+ out.push({
50
+ type: 'usage',
51
+ stage: 'start',
52
+ input_tokens: meta.input_tokens,
53
+ cache_read_input_tokens: meta.cache_read_input_tokens,
54
+ cache_creation_input_tokens: meta.cache_creation_input_tokens,
55
+ })
56
+ } else if (meta.kind === 'decision_resolved' && typeof meta.action === 'string') {
57
+ const status = meta.action === 'rejected' ? 'rejected' : 'approved'
58
+ const toolName = typeof meta.tool_name === 'string' ? meta.tool_name : undefined
59
+ const result = meta.result ?? null
60
+ const card = meta.card ?? null
61
+ out.push({
62
+ type: 'approval-resolved',
63
+ status,
64
+ ok: meta.ok === true,
65
+ willAutoContinue: meta.willAutoContinue === true,
66
+ ...(toolName ? { toolName } : {}),
67
+ ...(result ? { result } : {}),
68
+ ...(card?.marker ? { marker: card.marker } : {}),
69
+ ...(card?.ref ? { cardRef: card.ref } : {}),
70
+ ...(card?.type ? { cardType: card.type } : {}),
71
+ ...(typeof meta.receiptText === 'string' ? { receiptText: meta.receiptText } : {}),
72
+ requestId: typeof meta.proposalId === 'string' ? meta.proposalId : undefined,
73
+ })
74
+ } else if (meta.kind === 'approval_batch' && meta.batchId && Array.isArray(meta.proposals)) {
75
+ // Server-grouped multi-proposal turn → ONE batch event carrying a
76
+ // tool-call row per proposal. `toolExecutionRequestId` is the row's
77
+ // PROPOSAL id (each row resolves through its own per-proposal
78
+ // confirm); the batch's `requestId` is the stable anchor the shell
79
+ // uses for status flips. Field rows ride as the row's expandable
80
+ // args so per-proposal detail stays reachable inside the batch.
81
+ const toolCalls = (meta.proposals as Array<Record<string, any>>)
82
+ .filter((p) => p && typeof p.proposalId === 'string')
83
+ .map((p) => {
84
+ const rawFields = Array.isArray(p.fields)
85
+ ? (p.fields as Array<{ label?: string; value?: string }>).filter(
86
+ (f) => f && f.label && f.value,
87
+ )
88
+ : []
89
+ // Prefer human-readable identity labels for the row's
90
+ // disambiguator; opaque-id labels ("Task", "Ticket") only as
91
+ // the last-resort first field.
92
+ const detail =
93
+ rawFields.find((f) => /^(title|subject|name)$/i.test(String(f.label))) ??
94
+ rawFields[0]
95
+ const base =
96
+ typeof p.title === 'string' && p.title.length > 0 ? p.title : String(p.toolName ?? 'Tool call')
97
+ return {
98
+ toolExecutionRequestId: String(p.proposalId),
99
+ toolName: String(p.toolName ?? 'tool'),
100
+ toolTitle: detail ? `${base} — ${detail.value}` : base,
101
+ requiresApproval: true,
102
+ toolCallArguments:
103
+ rawFields.length > 0
104
+ ? Object.fromEntries(rawFields.map((f) => [String(f.label), String(f.value)]))
105
+ : null,
106
+ }
107
+ })
108
+ if (toolCalls.length > 0) {
109
+ out.push({
110
+ type: 'approval-request',
111
+ requestId: String(meta.batchId),
112
+ approvalType: 'chat',
113
+ toolCalls,
114
+ status: 'pending',
115
+ })
116
+ }
117
+ } else if (meta.kind === 'approval_request' && meta.proposalId) {
118
+ const proposalId = String(meta.proposalId)
119
+ const toolName = String(meta.toolName ?? 'tool')
120
+ const headline =
121
+ typeof meta.title === 'string' && meta.title.length > 0 ? meta.title : toolName
122
+ const rawFields = Array.isArray(meta.fields)
123
+ ? (meta.fields as Array<{ label?: string; value?: string }>)
124
+ : []
125
+ const fields: Array<{ label: string; value: string }> = []
126
+ for (const f of rawFields) {
127
+ if (!f || !f.label || !f.value) continue
128
+ fields.push({ label: f.label, value: f.value })
129
+ }
130
+ out.push({
131
+ type: 'approval-request',
132
+ requestId: proposalId,
133
+ approvalType: toolName,
134
+ command: headline,
135
+ fields,
136
+ status: 'pending',
137
+ })
138
+ } else if (meta.kind === 'text-leading' && typeof meta.text === 'string') {
139
+ out.push({ type: 'text-delta', text: meta.text, leading: true })
140
+ } else if (meta.kind === 'tool_error') {
141
+ const msg =
142
+ typeof meta.message === 'string' && meta.message.length > 0
143
+ ? meta.message
144
+ : 'Could not complete the requested action right now.'
145
+ out.push({ type: 'error', title: msg })
146
+ } else if (meta.kind === 'routing') {
147
+ if (typeof meta.routedComplexity === 'string') {
148
+ out.push({
149
+ type: 'metadata',
150
+ routing: {
151
+ routedComplexity: meta.routedComplexity,
152
+ ...(typeof meta.routedModel === 'string' ? { routedModel: meta.routedModel } : {}),
153
+ routedThinkingBudget:
154
+ typeof meta.routedThinkingBudget === 'number' ? meta.routedThinkingBudget : null,
155
+ },
156
+ })
157
+ }
158
+ } else {
159
+ // Catch-all metadata-ish frame. Raw values pass through UNVALIDATED
160
+ // (possibly undefined) so the consumer can replicate the legacy
161
+ // presence/truthiness gates exactly — including the `model`-presence
162
+ // trigger whose value is never stored.
163
+ out.push({
164
+ type: 'metadata',
165
+ sources: meta.sources,
166
+ refs: meta.refs,
167
+ provider: meta.provider,
168
+ modelLabel: meta.modelLabel,
169
+ modelName: meta.model,
170
+ contextWindowMaxTokens: meta.contextWindowMaxTokens,
171
+ scrollAnchor: meta.scrollAnchor,
172
+ conversationId: meta.conversationId,
173
+ })
174
+ }
175
+ }
@@ -18,13 +18,19 @@
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`). */
26
27
  type NatsChunk = Record<string, any>
27
28
 
29
+ // `GUIDE_ORIGIN` moved to `./events`, beside the `GuideOrigin` type it is the
30
+ // only value of — the SSE half reads it too, so it never belonged to the NATS
31
+ // decoder. Re-exported here because that is the import path consumers know.
32
+ export { GUIDE_ORIGIN } from './events'
33
+
28
34
  /** Coerce the wire's `toolCalls[]` into the batch-approval shape, dropping
29
35
  * non-object entries and defaulting every field. */
30
36
  function normalizeToolCalls(raw: unknown): ApprovalToolCall[] {
@@ -61,6 +67,99 @@ export function normalizeAskOptions(raw: unknown): AskOptionData[] {
61
67
  .filter((option) => option.label.length > 0)
62
68
  }
63
69
 
70
+ /**
71
+ * Events whose meaning belongs to the AGENT's dialog rather than to the guide
72
+ * turn embedded inside it. The hub emits its own copies of these, and the Mingo
73
+ * dialog already has authoritative ones: letting the hub's through would
74
+ * double-count the dialog's tokens or fight the agent's own phase chunks.
75
+ *
76
+ * Note this is a DENYLIST, and that is the point. `./leading-frames` is the ONE
77
+ * place a hub frame kind is taught to the client; whatever it learns to emit —
78
+ * including kinds that do not exist yet — must reach a Mingo dialog without a
79
+ * second edit here. An allowlist would silently swallow every card the hub adds
80
+ * next, and the guide half would quietly lag the guide chat.
81
+ */
82
+ const AGENT_OWNED_EVENTS: ReadonlySet<ChatStreamEvent['type']> = new Set([
83
+ 'usage',
84
+ 'token-usage',
85
+ 'status',
86
+ 'compaction',
87
+ 'participant',
88
+ 'dialog-closed',
89
+ ])
90
+
91
+ /** Event types that declare `origin` (see `GuideOrigin`). Data rather than a
92
+ * branch per type, so a future hub card opts in by adding the field to its
93
+ * interface and its name here — the pass-through below needs no edit. */
94
+ const ORIGIN_BEARING_EVENTS: ReadonlySet<ChatStreamEvent['type']> = new Set([
95
+ 'approval-request',
96
+ 'approval-resolved',
97
+ ])
98
+
99
+ /**
100
+ * Adapt ONE Product Guide event to the NATS kernel.
101
+ *
102
+ * The guide half of a Mingo dialog is the hub's own stream, so it is decoded by
103
+ * the shared table in `./leading-frames` and then passed through here — the ONE
104
+ * place that reconciles the two kernels. Both entry points use it: frames
105
+ * re-streamed by the agent inside `GUIDE` chunks, and the SSE response of the
106
+ * hub's confirm-tool route, which a host replays into the same dialog.
107
+ *
108
+ * Pass-through is the DEFAULT; only these rules alter an event, and each exists
109
+ * because the two kernels genuinely disagree:
110
+ *
111
+ * - `text-delta` becomes `guide-delta`: the body of a guide turn belongs
112
+ * inside the "OpenFrame Guide" card, and a `text` segment would strand part
113
+ * of the same answer outside it.
114
+ * - thinking is escaped HERE. The SSE kernel escapes `<` on the way in, the
115
+ * NATS kernel does not, and the guide's thinking is full of XML-ish tokens
116
+ * that would otherwise render as markup.
117
+ * - `metadata` survives ONLY to carry `conversationId`, which the hub mints
118
+ * and every confirm-tool call must quote back. Everything else on that
119
+ * frame (the hub's model, routing) is dropped: `applyNats` rebuilds the
120
+ * dialog's live model from a metadata event, so letting it through would
121
+ * relabel a Mingo turn with the hub's model, and a routing frame — which
122
+ * carries no model at all — would blank the badge mid-answer.
123
+ * - dialog-level events stop here (`AGENT_OWNED_EVENTS`).
124
+ *
125
+ * Everything else crosses over as the hub typed it, gaining only `origin`.
126
+ * `approvalType` in particular stays the TOOL NAME, exactly as in the hub's own
127
+ * chat: the NATS kernel gates approvals on approval TIER and escalates the rest,
128
+ * which is an agent-side concept a hub proposal has no tier for. Rewriting the
129
+ * tool name into a fake tier to slip past that gate would make the guide half of
130
+ * the stream diverge from the guide chat itself.
131
+ */
132
+ export function guideEventForNats(event: ChatStreamEvent): ChatStreamEvent | null {
133
+ switch (event.type) {
134
+ case 'text-delta':
135
+ return { type: 'guide-delta', text: event.text }
136
+ case 'thinking-delta':
137
+ return { type: 'thinking-delta', text: escapeThinkingTags(event.text) }
138
+ case 'metadata':
139
+ return typeof event.conversationId === 'string' && event.conversationId
140
+ ? { type: 'metadata', conversationId: event.conversationId, origin: GUIDE_ORIGIN }
141
+ : null
142
+ default:
143
+ if (AGENT_OWNED_EVENTS.has(event.type)) return null
144
+ return ORIGIN_BEARING_EVENTS.has(event.type)
145
+ ? ({ ...event, origin: GUIDE_ORIGIN } as ChatStreamEvent)
146
+ : event
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Decode one re-streamed Product Guide frame (a `GUIDE` chunk's `payload`) into
152
+ * a NATS event. The frame grammar is decoded by the shared table, never by a
153
+ * second copy here; the kernel reconciliation is `guideEventForNats`.
154
+ */
155
+ export function guideFrameEvent(frame: Record<string, unknown>): ChatStreamEvent | null {
156
+ const events: ChatStreamEvent[] = []
157
+ // Every branch of the table pushes at most one event, so the first is the one.
158
+ mapLeadingFrame(frame, events)
159
+ const event = events[0]
160
+ return event ? guideEventForNats(event) : null
161
+ }
162
+
64
163
  /**
65
164
  * Parse one raw NATS chunk into a normalized event. Returns `null` for
66
165
  * unknown/malformed chunks — an unrecognized `type`, a missing required
@@ -111,11 +210,20 @@ export function decodeNatsChunk(chunk: unknown): ChatStreamEvent | null {
111
210
  }
112
211
  return null
113
212
 
114
- case MESSAGE_TYPE.GUIDE:
213
+ // Two shapes share this chunk type. `text` is the answer body the agent
214
+ // streams (and persists). `payload` is a Product Guide frame re-streamed
215
+ // verbatim from the hub — decoded through the shared frame table, with the
216
+ // narrowing `guideFrameEvent` documents.
217
+ case MESSAGE_TYPE.GUIDE: {
115
218
  if (typeof data.text === 'string') {
116
219
  return { type: 'guide-delta', text: data.text, ...seq }
117
220
  }
118
- return null
221
+ const frame = data.payload
222
+ if (!frame || typeof frame !== 'object' || Array.isArray(frame)) return null
223
+ const event = guideFrameEvent(frame as Record<string, unknown>)
224
+ if (!event) return null
225
+ return { ...event, ...seq }
226
+ }
119
227
 
120
228
  // An ask card is only an ask card with something to pick: a question and at
121
229
  // least one option. Anything less is dropped rather than rendered as an
@@ -82,6 +82,7 @@ import {
82
82
  type UseUnifiedChatModes,
83
83
  } from './hooks/use-unified-chat'
84
84
  import type { UnifiedChatState } from './types/unified-chat-state.types'
85
+ import type { MessageSegment } from './types/message.types'
85
86
  import type {
86
87
  UseNatsChatAdapterConfig,
87
88
  FetchDialogsParams,
@@ -209,6 +210,19 @@ export interface EmbeddableChatProps {
209
210
  */
210
211
  mingoState?: UnifiedChatState
211
212
 
213
+ /**
214
+ * Approval cards to pin as the sticky footer under the thread.
215
+ *
216
+ * Hosts that lift PENDING approvals out of the message list (mingo does:
217
+ * a pending card is filtered out of its bubble so an interrupted retry cannot
218
+ * render the same request twice) must hand them back here — otherwise the
219
+ * card exists in the reducer, is stripped on the way to the view, and is
220
+ * displayed nowhere at all. `ChatMessageList` renders them with the same
221
+ * component the inline path uses, so the approve/reject handlers stamped on
222
+ * the segments keep working.
223
+ */
224
+ pendingApprovals?: MessageSegment[]
225
+
212
226
  /**
213
227
  * Dialog-management capabilities for injected Mingo mode (`mingoState`).
214
228
  *
@@ -909,6 +923,7 @@ function EmbeddableChatInner({
909
923
  tableIdForDocumentType,
910
924
  modes,
911
925
  mingoState,
926
+ pendingApprovals,
912
927
  mingoDialogCapabilities,
913
928
  activeMode: controlledActiveMode,
914
929
  onActiveModeChange,
@@ -2350,6 +2365,9 @@ function EmbeddableChatInner({
2350
2365
  <ChatMessageList
2351
2366
  messages={messages}
2352
2367
  isTyping={chatLoading}
2368
+ // Sticky footer for approvals the host lifted out of the
2369
+ // thread — see the prop's docblock.
2370
+ pendingApprovals={pendingApprovals}
2353
2371
  // Real drawer: the library's smart follow. Passive in-page
2354
2372
  // demo (previewMode): deterministic hard pin instead — a
2355
2373
  // scripted assistant-only stream from a cold mount never
@@ -59,6 +59,7 @@ import type { MessageSegment } from '../types/message.types'
59
59
  import { useSlashCommandRegistry, type SlashCommandSummary } from './use-slash-commands'
60
60
  import { chatAuthedFetch } from '../utils/chat-authed-fetch'
61
61
  import type { ScrollAnchor } from '../utils/scroll-anchor'
62
+ import { buildConfirmToolBody, readServerErrorMessage } from '../../../chat-protocol/confirm-tool'
62
63
  import { createSseFrameDecoder } from '../../../chat-protocol/decode'
63
64
  import type { ChatStreamEvent } from '../../../chat-protocol/events'
64
65
  import {
@@ -437,13 +438,11 @@ export function useSseChatAdapter(
437
438
  ? endpointsRef.current.approvalToolUrl
438
439
  : endpointsRef.current.chatStreamUrl
439
440
  const requestBody = approvalAction
440
- ? {
441
- proposal_id: approvalAction.proposalId,
442
- action: approvalAction.action,
443
- // Always present here an approval can only happen inside an
444
- // established conversation (the proposal turn captured the id).
445
- conversationId,
446
- }
441
+ ? // Shared with every other transport that resolves a hub proposal —
442
+ // the body shape is the hub's, not this adapter's. `conversationId`
443
+ // is always present here: an approval can only happen inside an
444
+ // established conversation (the proposal turn captured the id).
445
+ buildConfirmToolBody({ ...approvalAction, conversationId })
447
446
  : {
448
447
  messages: [{ role: 'user', content: text }],
449
448
  ...(commandOverride ? { commandOverride } : {}),
@@ -476,15 +475,7 @@ export function useSseChatAdapter(
476
475
  // `{error, code}`) — a bare "Chat request failed: 409" told
477
476
  // the user nothing when e.g. a batch approval expired; the
478
477
  // server ships real copy ("This approval expired — ...").
479
- let serverMessage: string | null = null
480
- try {
481
- const errBody = (await response.json()) as { error?: unknown }
482
- if (typeof errBody?.error === 'string' && errBody.error.length > 0) {
483
- serverMessage = errBody.error
484
- }
485
- } catch {
486
- /* non-JSON error body — fall through to the generic copy */
487
- }
478
+ const serverMessage = await readServerErrorMessage(response)
488
479
  throw new Error(serverMessage ?? `Chat request failed: ${response.status}`)
489
480
  }
490
481
  const reader = response.body?.getReader()
@@ -58,6 +58,8 @@ import {
58
58
  import { getCommandText } from '../utils/tool-call-helpers'
59
59
  import { parseScrollAnchor, type ScrollAnchor } from '../utils/scroll-anchor'
60
60
  import { escapeThinkingTags } from '../../../chat-protocol/decode'
61
+ import { isGuideOrigin } from '../../../chat-protocol/events'
62
+ import { approvalDisplaysInline, guideApprovalOrigin } from '../utils/approval-display'
61
63
  import { buildChatRefKey } from '../types/chat.types'
62
64
  import type {
63
65
  ApprovalBatchSegment,
@@ -162,6 +164,15 @@ export interface ChatReducerState {
162
164
  contextWindowMaxTokens: number | null
163
165
  } | null
164
166
  approvalStatuses: Record<string, ChatApprovalStatus>
167
+ /**
168
+ * Hub conversation id for the Product Guide half of this dialog, learned from
169
+ * a guide metadata frame. The hub mints it and requires it back on every
170
+ * confirm-tool call, so a host resolving a guide approval card reads it from
171
+ * here. Null until a guide turn has streamed — an approval that arrives from
172
+ * history (after a reload) has no live frame to learn it from, which is why
173
+ * the agent should also expose it per dialog.
174
+ */
175
+ guideConversationId: string | null
165
176
  }
166
177
 
167
178
  /** Escalated-approval bookkeeping entry (mirrors the legacy processor). */
@@ -520,6 +531,7 @@ export function createChatStreamReducer(
520
531
  let streamingPhase: StreamingPhase = 'idle'
521
532
  let dialogTokenUsage: DialogTokenUsage | null = null
522
533
  let liveModel: ChatReducerState['liveModel'] = null
534
+ let guideConversationId: string | null = null
523
535
  let approvalStatuses: Record<string, ChatApprovalStatus> = {
524
536
  ...(options.approvalStatuses ?? {}),
525
537
  }
@@ -682,6 +694,7 @@ export function createChatStreamReducer(
682
694
  dialogTokenUsage,
683
695
  liveModel,
684
696
  approvalStatuses,
697
+ guideConversationId,
685
698
  }
686
699
  }
687
700
  return stateCache
@@ -989,6 +1002,17 @@ export function createChatStreamReducer(
989
1002
  }
990
1003
 
991
1004
  case 'metadata': {
1005
+ // A guide metadata event carries ONLY the hub's conversation id (the
1006
+ // decoder strips the rest): record it and stop. Falling through would
1007
+ // rebuild `liveModel` from an event with no model in it and blank the
1008
+ // dialog's model badge mid-answer.
1009
+ if (isGuideOrigin(event)) {
1010
+ if (typeof event.conversationId === 'string' && event.conversationId) {
1011
+ guideConversationId = event.conversationId
1012
+ invalidate()
1013
+ }
1014
+ break
1015
+ }
992
1016
  // Legacy `parseChunkToAction` action shape, reconstructed for the
993
1017
  // callback contract.
994
1018
  emit('onMetadata', {
@@ -1101,11 +1125,17 @@ export function createChatStreamReducer(
1101
1125
  const requestId = event.requestId
1102
1126
  const approvalType = event.approvalType ?? 'USER'
1103
1127
  const toolCalls = event.toolCalls
1128
+ // Where this card renders is ONE rule, shared with the SSE kernel and
1129
+ // the history replay (`approval-display`) — a Product Guide proposal
1130
+ // that renders inline live must not move on the next page load.
1131
+ const guideOrigin = guideApprovalOrigin(event)
1132
+ const displayInline = (type: string) =>
1133
+ approvalDisplaysInline(event, type, displayApprovalTypes)
1104
1134
 
1105
1135
  if (toolCalls && toolCalls.length > 0) {
1106
1136
  // ── Batch form ──
1107
1137
  const status = (approvalStatuses[requestId] || 'pending') as ChatApprovalStatus
1108
- if (!displayApprovalTypes.includes(approvalType)) {
1138
+ if (!displayInline(approvalType)) {
1109
1139
  // Escalated: keep batch context locally for replay on result;
1110
1140
  // surface a summary command via the legacy escalation callback.
1111
1141
  const required = toolCalls.find((c) => c.requiresApproval) ?? toolCalls[0]
@@ -1127,7 +1157,15 @@ export function createChatStreamReducer(
1127
1157
  }
1128
1158
  if (batchApprovalsEnabled) {
1129
1159
  const before = accumulator.getSegments().length
1130
- const segments = accumulator.addApprovalBatch(requestId, approvalType, toolCalls, status)
1160
+ const segments = accumulator.addApprovalBatch(
1161
+ requestId,
1162
+ approvalType,
1163
+ toolCalls,
1164
+ status,
1165
+ undefined,
1166
+ undefined,
1167
+ guideOrigin,
1168
+ )
1131
1169
  applyAccumulated(before, segments)
1132
1170
  break
1133
1171
  }
@@ -1145,6 +1183,8 @@ export function createChatStreamReducer(
1145
1183
  call.toolExplanation,
1146
1184
  approvalType,
1147
1185
  status,
1186
+ undefined,
1187
+ guideOrigin,
1148
1188
  )
1149
1189
  }
1150
1190
  applyAccumulated(before, segments)
@@ -1154,7 +1194,7 @@ export function createChatStreamReducer(
1154
1194
  // ── Single form ──
1155
1195
  const command = event.command ?? ''
1156
1196
  const explanation = event.explanation
1157
- if (displayApprovalTypes.includes(approvalType)) {
1197
+ if (displayInline(approvalType)) {
1158
1198
  const status = (approvalStatuses[requestId] || 'pending') as ChatApprovalStatus
1159
1199
  const before = accumulator.getSegments().length
1160
1200
  const segments = accumulator.addApprovalRequest(
@@ -1163,6 +1203,10 @@ export function createChatStreamReducer(
1163
1203
  explanation,
1164
1204
  approvalType,
1165
1205
  status,
1206
+ // SSE-shaped cards (a re-streamed Product Guide proposal) carry
1207
+ // their body as structured rows rather than prose.
1208
+ event.fields,
1209
+ guideOrigin,
1166
1210
  )
1167
1211
  applyAccumulated(before, segments)
1168
1212
  } else {
@@ -1707,6 +1751,7 @@ export function createChatStreamReducer(
1707
1751
  streamingPhase = 'idle'
1708
1752
  dialogTokenUsage = null
1709
1753
  liveModel = null
1754
+ guideConversationId = null
1710
1755
  approvalStatuses = {}
1711
1756
  accumulator.reset()
1712
1757
  pendingEscalated.clear()
@@ -1755,6 +1800,7 @@ export function createChatStreamReducer(
1755
1800
  streamingPhase = 'idle'
1756
1801
  dialogTokenUsage = null
1757
1802
  liveModel = null
1803
+ guideConversationId = null
1758
1804
  accumulator.reset()
1759
1805
  pendingEscalated.clear()
1760
1806
  isInStream = false
@@ -113,6 +113,12 @@ export interface ApprovalRequestData {
113
113
  requestId?: string
114
114
  approvalRequestId?: string
115
115
  approvalType?: string
116
+ /** `'guide'` when the card came from a Product Guide frame (see `GuideOrigin`
117
+ * in `chat-protocol/events`). Carried onto the segment because hosts route
118
+ * and filter approvals by where they came from: mingo lifts its OWN pending
119
+ * cards into a sticky footer, and a guide card must stay inline in the turn
120
+ * the way it does in the hub's chat. */
121
+ origin?: 'guide'
116
122
  }
117
123
 
118
124
  export interface ApprovalResultData {
@@ -189,6 +195,11 @@ export interface ApprovalBatchData {
189
195
  * batch itself is approved.
190
196
  */
191
197
  executions?: Record<string, ApprovalBatchExecutionState>
198
+ /** `'guide'` when the batch came from a Product Guide `approval_batch` frame.
199
+ * Same contract and the same reasons as `ApprovalRequestData.origin` — the
200
+ * hub groups multiple proposals into one card, and that card resolves
201
+ * through the hub exactly like a single one. */
202
+ origin?: 'guide'
192
203
  }
193
204
 
194
205
  /** Approve/reject handler stamped onto approval segments. MAY resolve a
@@ -334,6 +345,10 @@ export interface ThinkingMessageData extends MessageDataBase {
334
345
  export interface GuideMessageData extends MessageDataBase {
335
346
  type: 'GUIDE'
336
347
  text?: string
348
+ /** Persisted Product Guide frame (GraphQL `GuideData.payload`, a JSON scalar)
349
+ * for rows that carry a card instead of answer text — replayed through the
350
+ * live path's `guideFrameEvent`. */
351
+ payload?: Record<string, unknown>
337
352
  }
338
353
 
339
354
  /** Persisted `ASK` row (GraphQL `AskData`). `text` is the intro sentence, which
@@ -61,6 +61,10 @@ export interface ChunkData {
61
61
  /** On APPROVAL_RESULT chunks: display name of the user who resolved the request. */
62
62
  resolvedByName?: string
63
63
  toolCalls?: any[]
64
+ /** On GUIDE chunks: a Product Guide frame the agent re-streamed verbatim from
65
+ * the hub, instead of the answer text `text` carries. Decoded by
66
+ * `decodeNatsChunk` through the shared leading-frame table. */
67
+ payload?: Record<string, unknown>
64
68
  modelName?: string
65
69
  providerName?: string
66
70
  provider?: string
@@ -3,7 +3,12 @@
3
3
  * Contains types for message parsing, accumulation, and processing
4
4
  */
5
5
 
6
- import type { MessageSegment, PendingToolCallData, ExecutingToolState } from './message.types'
6
+ import type {
7
+ ApprovalRequestField,
8
+ MessageSegment,
9
+ PendingToolCallData,
10
+ ExecutingToolState,
11
+ } from './message.types'
7
12
  import type { ChatApprovalStatus, AssistantType } from './chat.types'
8
13
  import type { ChunkData } from './network.types'
9
14
 
@@ -18,6 +23,12 @@ export interface PendingApproval {
18
23
  command: string
19
24
  explanation?: string
20
25
  approvalType: string
26
+ /** Structured label/value rows — a Product Guide card is almost entirely
27
+ * these, so a path that forgets them renders a bare title. */
28
+ fields?: ApprovalRequestField[]
29
+ /** Where the card came from; `'guide'` keeps it inline and routes its
30
+ * buttons to the hub (see `ApprovalRequestData.origin`). */
31
+ origin?: 'guide'
21
32
  }
22
33
 
23
34
  export interface AccumulatorState {