@flamingo-stack/openframe-frontend-core 0.0.532-snapshot.20260811155109 → 0.0.532

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/decode.d.ts +9 -1
  2. package/dist/chat-protocol/decode.d.ts.map +1 -1
  3. package/dist/chat-protocol/events.d.ts +0 -35
  4. package/dist/chat-protocol/events.d.ts.map +1 -1
  5. package/dist/chat-protocol/index.cjs +3 -72
  6. package/dist/chat-protocol/index.cjs.map +1 -1
  7. package/dist/chat-protocol/index.d.ts +0 -1
  8. package/dist/chat-protocol/index.d.ts.map +1 -1
  9. package/dist/chat-protocol/index.js +4 -67
  10. package/dist/chat-protocol/index.js.map +1 -1
  11. package/dist/chat-protocol/nats-decoder.d.ts +0 -41
  12. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
  13. package/dist/{chunk-OLOKQQAP.cjs → chunk-2MBS3YEY.cjs} +5 -5
  14. package/dist/{chunk-OLOKQQAP.cjs.map → chunk-2MBS3YEY.cjs.map} +1 -1
  15. package/dist/{chunk-DFQLUK75.cjs → chunk-2QP67D2L.cjs} +3 -3
  16. package/dist/{chunk-DFQLUK75.cjs.map → chunk-2QP67D2L.cjs.map} +1 -1
  17. package/dist/{chunk-6SMLYZLH.js → chunk-3G7FDE5E.js} +4 -4
  18. package/dist/{chunk-J4BE4FIR.cjs → chunk-4GL2XJ5C.cjs} +4 -4
  19. package/dist/{chunk-J4BE4FIR.cjs.map → chunk-4GL2XJ5C.cjs.map} +1 -1
  20. package/dist/{chunk-LE3BJ5H2.js → chunk-5W3RCZIK.js} +2 -2
  21. package/dist/{chunk-MVGL5NNJ.js → chunk-65NR333L.js} +2 -2
  22. package/dist/{chunk-6W3ULZ4X.js → chunk-7SGBF2I2.js} +6 -6
  23. package/dist/{chunk-6YXN5RHW.js → chunk-ALXLWF75.js} +2 -2
  24. package/dist/{chunk-5U67WTAS.js → chunk-AZBS4PZG.js} +2 -2
  25. package/dist/{chunk-4QKXNWH5.js → chunk-ECR37JHH.js} +2 -2
  26. package/dist/{chunk-ENPGPZIB.cjs → chunk-ETF2QXXU.cjs} +61 -61
  27. package/dist/{chunk-ENPGPZIB.cjs.map → chunk-ETF2QXXU.cjs.map} +1 -1
  28. package/dist/{chunk-UPHN2VRZ.cjs → chunk-GVLA2BPW.cjs} +117 -259
  29. package/dist/chunk-GVLA2BPW.cjs.map +1 -0
  30. package/dist/{chunk-4ZRHVOEB.cjs → chunk-GWFC3NE5.cjs} +31 -31
  31. package/dist/{chunk-4ZRHVOEB.cjs.map → chunk-GWFC3NE5.cjs.map} +1 -1
  32. package/dist/{chunk-GNOK5J34.cjs → chunk-HJDCDOJM.cjs} +37 -37
  33. package/dist/{chunk-GNOK5J34.cjs.map → chunk-HJDCDOJM.cjs.map} +1 -1
  34. package/dist/{chunk-JDW2IL7A.cjs → chunk-HXHA3DCI.cjs} +11 -11
  35. package/dist/{chunk-JDW2IL7A.cjs.map → chunk-HXHA3DCI.cjs.map} +1 -1
  36. package/dist/{chunk-XN4JKIJY.js → chunk-IZOGFZ7N.js} +2 -2
  37. package/dist/{chunk-Z3NOI7X7.cjs → chunk-JZ5XDZYD.cjs} +26 -26
  38. package/dist/{chunk-Z3NOI7X7.cjs.map → chunk-JZ5XDZYD.cjs.map} +1 -1
  39. package/dist/{chunk-YLB6XXO6.js → chunk-KYQZBQYV.js} +2 -2
  40. package/dist/{chunk-RKITTO5V.cjs → chunk-L6EKEYF7.cjs} +14 -14
  41. package/dist/{chunk-RKITTO5V.cjs.map → chunk-L6EKEYF7.cjs.map} +1 -1
  42. package/dist/{chunk-NAV4FRNL.cjs → chunk-LYWSHNK4.cjs} +26 -26
  43. package/dist/{chunk-NAV4FRNL.cjs.map → chunk-LYWSHNK4.cjs.map} +1 -1
  44. package/dist/{chunk-46FKXLNE.js → chunk-OAEJWP7H.js} +2 -2
  45. package/dist/{chunk-2MYDGLJS.cjs → chunk-QEMI34VA.cjs} +87 -87
  46. package/dist/{chunk-2MYDGLJS.cjs.map → chunk-QEMI34VA.cjs.map} +1 -1
  47. package/dist/{chunk-QZ2NAPLF.cjs → chunk-QYNLVIGQ.cjs} +9 -9
  48. package/dist/{chunk-QZ2NAPLF.cjs.map → chunk-QYNLVIGQ.cjs.map} +1 -1
  49. package/dist/{chunk-KMB66NYX.cjs → chunk-TMZS63G2.cjs} +7 -7
  50. package/dist/{chunk-KMB66NYX.cjs.map → chunk-TMZS63G2.cjs.map} +1 -1
  51. package/dist/{chunk-2TUOBFPK.js → chunk-TQ7EPKKO.js} +2 -2
  52. package/dist/{chunk-U555TMPC.js → chunk-UP4RA54U.js} +5 -5
  53. package/dist/{chunk-LIUARTOZ.js → chunk-WINT757E.js} +64 -206
  54. package/dist/chunk-WINT757E.js.map +1 -0
  55. package/dist/{chunk-2XGHEJZK.js → chunk-XBYEPSNZ.js} +4 -4
  56. package/dist/components/case-studies/index.cjs +8 -8
  57. package/dist/components/case-studies/index.js +2 -2
  58. package/dist/components/chat/embeddable-chat.d.ts +0 -13
  59. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  60. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  61. package/dist/components/chat/index.cjs +2 -10
  62. package/dist/components/chat/index.cjs.map +1 -1
  63. package/dist/components/chat/index.js +1 -9
  64. package/dist/components/chat/stream/chat-stream-reducer.d.ts +0 -9
  65. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
  66. package/dist/components/chat/types/message.types.d.ts +0 -15
  67. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  68. package/dist/components/chat/types/network.types.d.ts +0 -4
  69. package/dist/components/chat/types/network.types.d.ts.map +1 -1
  70. package/dist/components/chat/types/processing.types.d.ts +1 -7
  71. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  72. package/dist/components/chat/utils/index.d.ts +0 -1
  73. package/dist/components/chat/utils/index.d.ts.map +1 -1
  74. package/dist/components/chat/utils/message-segment-accumulator.d.ts +3 -25
  75. package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
  76. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  77. package/dist/components/contact/index.cjs +3 -3
  78. package/dist/components/contact/index.js +2 -2
  79. package/dist/components/docs/index.cjs +5 -5
  80. package/dist/components/docs/index.js +4 -4
  81. package/dist/components/embeds/index.cjs +3 -3
  82. package/dist/components/embeds/index.js +2 -2
  83. package/dist/components/faq/index.cjs +3 -3
  84. package/dist/components/faq/index.js +2 -2
  85. package/dist/components/features/index.cjs +2 -2
  86. package/dist/components/features/index.js +1 -1
  87. package/dist/components/help-center-pages/index.cjs +22 -22
  88. package/dist/components/help-center-pages/index.js +13 -13
  89. package/dist/components/index.cjs +132 -140
  90. package/dist/components/index.cjs.map +1 -1
  91. package/dist/components/index.js +11 -19
  92. package/dist/components/index.js.map +1 -1
  93. package/dist/components/meeting-scheduler/index.cjs +34 -34
  94. package/dist/components/meeting-scheduler/index.js +3 -3
  95. package/dist/components/navigation/index.cjs +2 -2
  96. package/dist/components/navigation/index.js +1 -1
  97. package/dist/components/onboarding-guides/index.cjs +5 -5
  98. package/dist/components/onboarding-guides/index.js +4 -4
  99. package/dist/components/related-content/index.cjs +3 -3
  100. package/dist/components/related-content/index.js +2 -2
  101. package/dist/components/tickets/index.cjs +6 -6
  102. package/dist/components/tickets/index.js +5 -5
  103. package/dist/components/ui/index.cjs +2 -10
  104. package/dist/components/ui/index.cjs.map +1 -1
  105. package/dist/components/ui/index.js +1 -9
  106. package/dist/index.cjs +2 -10
  107. package/dist/index.cjs.map +1 -1
  108. package/dist/index.js +1 -9
  109. package/dist/utils/index.cjs.map +1 -1
  110. package/dist/utils/index.js.map +1 -1
  111. package/package.json +1 -1
  112. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +0 -70
  113. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +1 -108
  114. package/src/chat-protocol/decode.ts +153 -7
  115. package/src/chat-protocol/events.ts +0 -40
  116. package/src/chat-protocol/index.ts +0 -3
  117. package/src/chat-protocol/nats-decoder.ts +3 -111
  118. package/src/components/chat/embeddable-chat.tsx +0 -18
  119. package/src/components/chat/hooks/use-sse-chat-adapter.ts +16 -7
  120. package/src/components/chat/stream/chat-stream-reducer.ts +3 -49
  121. package/src/components/chat/types/message.types.ts +0 -15
  122. package/src/components/chat/types/network.types.ts +0 -4
  123. package/src/components/chat/types/processing.types.ts +1 -12
  124. package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +0 -92
  125. package/src/components/chat/utils/index.ts +0 -9
  126. package/src/components/chat/utils/message-segment-accumulator.ts +32 -62
  127. package/src/components/chat/utils/process-historical-messages.ts +6 -40
  128. package/dist/chat-protocol/confirm-tool.d.ts +0 -50
  129. package/dist/chat-protocol/confirm-tool.d.ts.map +0 -1
  130. package/dist/chat-protocol/leading-frames.d.ts +0 -38
  131. package/dist/chat-protocol/leading-frames.d.ts.map +0 -1
  132. package/dist/chunk-LIUARTOZ.js.map +0 -1
  133. package/dist/chunk-UPHN2VRZ.cjs.map +0 -1
  134. package/dist/components/chat/utils/approval-display.d.ts +0 -49
  135. package/dist/components/chat/utils/approval-display.d.ts.map +0 -1
  136. package/src/chat-protocol/confirm-tool.ts +0 -67
  137. package/src/chat-protocol/leading-frames.ts +0 -175
  138. package/src/components/chat/utils/approval-display.ts +0 -70
  139. /package/dist/{chunk-6SMLYZLH.js.map → chunk-3G7FDE5E.js.map} +0 -0
  140. /package/dist/{chunk-LE3BJ5H2.js.map → chunk-5W3RCZIK.js.map} +0 -0
  141. /package/dist/{chunk-MVGL5NNJ.js.map → chunk-65NR333L.js.map} +0 -0
  142. /package/dist/{chunk-6W3ULZ4X.js.map → chunk-7SGBF2I2.js.map} +0 -0
  143. /package/dist/{chunk-6YXN5RHW.js.map → chunk-ALXLWF75.js.map} +0 -0
  144. /package/dist/{chunk-5U67WTAS.js.map → chunk-AZBS4PZG.js.map} +0 -0
  145. /package/dist/{chunk-4QKXNWH5.js.map → chunk-ECR37JHH.js.map} +0 -0
  146. /package/dist/{chunk-XN4JKIJY.js.map → chunk-IZOGFZ7N.js.map} +0 -0
  147. /package/dist/{chunk-YLB6XXO6.js.map → chunk-KYQZBQYV.js.map} +0 -0
  148. /package/dist/{chunk-46FKXLNE.js.map → chunk-OAEJWP7H.js.map} +0 -0
  149. /package/dist/{chunk-2TUOBFPK.js.map → chunk-TQ7EPKKO.js.map} +0 -0
  150. /package/dist/{chunk-U555TMPC.js.map → chunk-UP4RA54U.js.map} +0 -0
  151. /package/dist/{chunk-2XGHEJZK.js.map → chunk-XBYEPSNZ.js.map} +0 -0
@@ -59,7 +59,6 @@ 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'
63
62
  import { createSseFrameDecoder } from '../../../chat-protocol/decode'
64
63
  import type { ChatStreamEvent } from '../../../chat-protocol/events'
65
64
  import {
@@ -438,11 +437,13 @@ export function useSseChatAdapter(
438
437
  ? endpointsRef.current.approvalToolUrl
439
438
  : endpointsRef.current.chatStreamUrl
440
439
  const requestBody = approvalAction
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 })
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
+ }
446
447
  : {
447
448
  messages: [{ role: 'user', content: text }],
448
449
  ...(commandOverride ? { commandOverride } : {}),
@@ -475,7 +476,15 @@ export function useSseChatAdapter(
475
476
  // `{error, code}`) — a bare "Chat request failed: 409" told
476
477
  // the user nothing when e.g. a batch approval expired; the
477
478
  // server ships real copy ("This approval expired — ...").
478
- const serverMessage = await readServerErrorMessage(response)
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
+ }
479
488
  throw new Error(serverMessage ?? `Chat request failed: ${response.status}`)
480
489
  }
481
490
  const reader = response.body?.getReader()
@@ -58,8 +58,6 @@ 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'
63
61
  import { buildChatRefKey } from '../types/chat.types'
64
62
  import type {
65
63
  ApprovalBatchSegment,
@@ -164,15 +162,6 @@ export interface ChatReducerState {
164
162
  contextWindowMaxTokens: number | null
165
163
  } | null
166
164
  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
176
165
  }
177
166
 
178
167
  /** Escalated-approval bookkeeping entry (mirrors the legacy processor). */
@@ -531,7 +520,6 @@ export function createChatStreamReducer(
531
520
  let streamingPhase: StreamingPhase = 'idle'
532
521
  let dialogTokenUsage: DialogTokenUsage | null = null
533
522
  let liveModel: ChatReducerState['liveModel'] = null
534
- let guideConversationId: string | null = null
535
523
  let approvalStatuses: Record<string, ChatApprovalStatus> = {
536
524
  ...(options.approvalStatuses ?? {}),
537
525
  }
@@ -694,7 +682,6 @@ export function createChatStreamReducer(
694
682
  dialogTokenUsage,
695
683
  liveModel,
696
684
  approvalStatuses,
697
- guideConversationId,
698
685
  }
699
686
  }
700
687
  return stateCache
@@ -1002,17 +989,6 @@ export function createChatStreamReducer(
1002
989
  }
1003
990
 
1004
991
  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
- }
1016
992
  // Legacy `parseChunkToAction` action shape, reconstructed for the
1017
993
  // callback contract.
1018
994
  emit('onMetadata', {
@@ -1125,17 +1101,11 @@ export function createChatStreamReducer(
1125
1101
  const requestId = event.requestId
1126
1102
  const approvalType = event.approvalType ?? 'USER'
1127
1103
  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)
1134
1104
 
1135
1105
  if (toolCalls && toolCalls.length > 0) {
1136
1106
  // ── Batch form ──
1137
1107
  const status = (approvalStatuses[requestId] || 'pending') as ChatApprovalStatus
1138
- if (!displayInline(approvalType)) {
1108
+ if (!displayApprovalTypes.includes(approvalType)) {
1139
1109
  // Escalated: keep batch context locally for replay on result;
1140
1110
  // surface a summary command via the legacy escalation callback.
1141
1111
  const required = toolCalls.find((c) => c.requiresApproval) ?? toolCalls[0]
@@ -1157,15 +1127,7 @@ export function createChatStreamReducer(
1157
1127
  }
1158
1128
  if (batchApprovalsEnabled) {
1159
1129
  const before = accumulator.getSegments().length
1160
- const segments = accumulator.addApprovalBatch(
1161
- requestId,
1162
- approvalType,
1163
- toolCalls,
1164
- status,
1165
- undefined,
1166
- undefined,
1167
- guideOrigin,
1168
- )
1130
+ const segments = accumulator.addApprovalBatch(requestId, approvalType, toolCalls, status)
1169
1131
  applyAccumulated(before, segments)
1170
1132
  break
1171
1133
  }
@@ -1183,8 +1145,6 @@ export function createChatStreamReducer(
1183
1145
  call.toolExplanation,
1184
1146
  approvalType,
1185
1147
  status,
1186
- undefined,
1187
- guideOrigin,
1188
1148
  )
1189
1149
  }
1190
1150
  applyAccumulated(before, segments)
@@ -1194,7 +1154,7 @@ export function createChatStreamReducer(
1194
1154
  // ── Single form ──
1195
1155
  const command = event.command ?? ''
1196
1156
  const explanation = event.explanation
1197
- if (displayInline(approvalType)) {
1157
+ if (displayApprovalTypes.includes(approvalType)) {
1198
1158
  const status = (approvalStatuses[requestId] || 'pending') as ChatApprovalStatus
1199
1159
  const before = accumulator.getSegments().length
1200
1160
  const segments = accumulator.addApprovalRequest(
@@ -1203,10 +1163,6 @@ export function createChatStreamReducer(
1203
1163
  explanation,
1204
1164
  approvalType,
1205
1165
  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,
1210
1166
  )
1211
1167
  applyAccumulated(before, segments)
1212
1168
  } else {
@@ -1751,7 +1707,6 @@ export function createChatStreamReducer(
1751
1707
  streamingPhase = 'idle'
1752
1708
  dialogTokenUsage = null
1753
1709
  liveModel = null
1754
- guideConversationId = null
1755
1710
  approvalStatuses = {}
1756
1711
  accumulator.reset()
1757
1712
  pendingEscalated.clear()
@@ -1800,7 +1755,6 @@ export function createChatStreamReducer(
1800
1755
  streamingPhase = 'idle'
1801
1756
  dialogTokenUsage = null
1802
1757
  liveModel = null
1803
- guideConversationId = null
1804
1758
  accumulator.reset()
1805
1759
  pendingEscalated.clear()
1806
1760
  isInStream = false
@@ -113,12 +113,6 @@ 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'
122
116
  }
123
117
 
124
118
  export interface ApprovalResultData {
@@ -195,11 +189,6 @@ export interface ApprovalBatchData {
195
189
  * batch itself is approved.
196
190
  */
197
191
  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'
203
192
  }
204
193
 
205
194
  /** Approve/reject handler stamped onto approval segments. MAY resolve a
@@ -345,10 +334,6 @@ export interface ThinkingMessageData extends MessageDataBase {
345
334
  export interface GuideMessageData extends MessageDataBase {
346
335
  type: 'GUIDE'
347
336
  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>
352
337
  }
353
338
 
354
339
  /** Persisted `ASK` row (GraphQL `AskData`). `text` is the intro sentence, which
@@ -61,10 +61,6 @@ 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>
68
64
  modelName?: string
69
65
  providerName?: string
70
66
  provider?: string
@@ -3,12 +3,7 @@
3
3
  * Contains types for message parsing, accumulation, and processing
4
4
  */
5
5
 
6
- import type {
7
- ApprovalRequestField,
8
- MessageSegment,
9
- PendingToolCallData,
10
- ExecutingToolState,
11
- } from './message.types'
6
+ import type { MessageSegment, PendingToolCallData, ExecutingToolState } from './message.types'
12
7
  import type { ChatApprovalStatus, AssistantType } from './chat.types'
13
8
  import type { ChunkData } from './network.types'
14
9
 
@@ -23,12 +18,6 @@ export interface PendingApproval {
23
18
  command: string
24
19
  explanation?: string
25
20
  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'
32
21
  }
33
22
 
34
23
  export interface AccumulatorState {
@@ -10,7 +10,6 @@
10
10
 
11
11
  import { describe, it, expect } from 'vitest'
12
12
  import { processHistoricalMessagesWithErrors } from '../process-historical-messages'
13
- import { decodeNatsChunk } from '../../../../chat-protocol/nats-decoder'
14
13
  import { createChatStreamReducer } from '../../stream/chat-stream-reducer'
15
14
  import type { HistoricalMessage, MessageSegment } from '../../types'
16
15
 
@@ -157,94 +156,3 @@ describe('displayApprovalTypes — history vs realtime default divergence', () =
157
156
  expect(live.escalated).toHaveLength(0)
158
157
  })
159
158
  })
160
-
161
- /**
162
- * A Product Guide card must survive a reload UNCHANGED.
163
- *
164
- * It arrives as a hub frame the agent re-streams into the dialog, so it is
165
- * typed by TOOL and resolves through the hub's confirm route — the tier gate
166
- * these tests are about simply does not apply to it. When the rule lived only
167
- * in the realtime kernel, the same card rendered inline while streaming and
168
- * then, on the next page load, was tier-gated: lifted out of its turn into the
169
- * consumer's sticky pending-approvals envelope, stripped of the `fields` that
170
- * are its entire body, and stripped of the `origin` that routes its buttons —
171
- * so clicking Approve hit the agent's endpoint and 404'd.
172
- */
173
- describe('Product Guide approvals — history and realtime agree', () => {
174
- const guideProposalMessage = (id: string): HistoricalMessage => ({
175
- id,
176
- createdAt: '2026-07-17T12:00:00Z',
177
- owner: { type: 'ASSISTANT' },
178
- messageData: [
179
- {
180
- type: 'GUIDE',
181
- payload: {
182
- kind: 'approval_request',
183
- proposalId: 'prop-1',
184
- toolName: 'create_ticket',
185
- title: 'Open a support ticket',
186
- fields: [{ label: 'Subject', value: 'Agent will not enroll' }],
187
- },
188
- } as any,
189
- ],
190
- })
191
-
192
- // The narrowest possible tier list: it excludes the guide's tool name, so a
193
- // card that still renders proves the gate was bypassed, not merely satisfied.
194
- const NARROW_TIERS = ['CLIENT']
195
-
196
- it('renders inline on reload despite a tier list that excludes it', () => {
197
- const { messages, escalatedApprovals } = processHistoricalMessagesWithErrors(
198
- [guideProposalMessage('m1')],
199
- { displayApprovalTypes: NARROW_TIERS },
200
- )
201
- const rendered = messages.flatMap((m) => approvalSegments(m.content))
202
- expect(rendered).toHaveLength(1)
203
- expect(escalatedApprovals.size).toBe(0)
204
- })
205
-
206
- it('keeps the card in its own turn, not in the sticky pending-approvals envelope', () => {
207
- const { messages } = processHistoricalMessagesWithErrors([guideProposalMessage('m1')], {
208
- displayApprovalTypes: NARROW_TIERS,
209
- })
210
- expect(messages.some((m) => String(m.id).startsWith('pending-approvals-'))).toBe(false)
211
- const turn = messages.find((m) => m.id === 'm1')
212
- expect(approvalSegments(turn?.content)).toHaveLength(1)
213
- })
214
-
215
- it('carries the same fields and origin as the live stream', () => {
216
- /** The single card's `data`, narrowed off the segment union. */
217
- const cardData = (segments: MessageSegment[]) => {
218
- const card = segments.find((s) => s.type === 'approval_request')
219
- return card?.type === 'approval_request' ? card.data : undefined
220
- }
221
-
222
- const { messages } = processHistoricalMessagesWithErrors([guideProposalMessage('m1')], {
223
- displayApprovalTypes: NARROW_TIERS,
224
- })
225
- const replayed = cardData(messages.flatMap((m) => approvalSegments(m.content)))
226
-
227
- const live = createChatStreamReducer({ transport: 'nats', displayApprovalTypes: NARROW_TIERS })
228
- live.apply({ type: 'turn-start' })
229
- live.apply(
230
- decodeNatsChunk({
231
- type: 'GUIDE',
232
- payload: {
233
- kind: 'approval_request',
234
- proposalId: 'prop-1',
235
- toolName: 'create_ticket',
236
- title: 'Open a support ticket',
237
- fields: [{ label: 'Subject', value: 'Agent will not enroll' }],
238
- },
239
- })!,
240
- )
241
- const streamed = cardData(live.state.messages.flatMap((m) => approvalSegments(m.segments)))
242
-
243
- // Same card either way — down to the rows that ARE its body, and the marker
244
- // that decides which backend its buttons talk to.
245
- expect(replayed).toEqual(streamed)
246
- expect(replayed?.origin).toBe('guide')
247
- expect(replayed?.approvalType).toBe('create_ticket')
248
- expect(replayed?.fields).toEqual([{ label: 'Subject', value: 'Agent will not enroll' }])
249
- })
250
- })
@@ -14,15 +14,6 @@ export {
14
14
  type AccumulatorCallbacks,
15
15
  } from './message-segment-accumulator'
16
16
 
17
- // Where an approval card renders — one rule for both live kernels, the history
18
- // replay, and hosts that sort cards between their own footer and the flow.
19
- export {
20
- isGuideApproval,
21
- guideApprovalOrigin,
22
- approvalDisplaysInline,
23
- isGuideApprovalSegment,
24
- } from './approval-display'
25
-
26
17
  // Historical message processing
27
18
  export {
28
19
  processHistoricalMessages,
@@ -12,7 +12,6 @@ import type {
12
12
  AskOptionData,
13
13
  MessageSegment,
14
14
  ToolExecutionSegment,
15
- ApprovalRequestField,
16
15
  ApprovalRequestSegment,
17
16
  ApprovalBatchSegment,
18
17
  ApprovalBatchExecutionState,
@@ -325,55 +324,23 @@ export class MessageSegmentAccumulator {
325
324
  command: string,
326
325
  explanation: string | undefined,
327
326
  approvalType: string,
328
- status: ChatApprovalStatus = 'pending',
329
- /** Structured label/value rows. The card prefers them over `explanation`
330
- * (see `ApprovalRequestData.fields`). Optional because the agent's own
331
- * approvals carry prose; a Product Guide card is almost entirely fields —
332
- * dropping them here left it as a bare title. */
333
- fields?: ApprovalRequestField[],
334
- /** Where the card came from; `'guide'` keeps it inline (see
335
- * `ApprovalRequestData.origin`). */
336
- origin?: 'guide'
327
+ status: ChatApprovalStatus = 'pending'
337
328
  ): MessageSegment[] {
338
- this.segments.push(
339
- this.buildApprovalRequestSegment(
340
- requestId,
341
- { command, explanation, approvalType, fields, origin },
342
- status,
343
- ),
344
- )
345
- return this.getSegments()
346
- }
347
-
348
- /**
349
- * Build one approval-request segment — THE constructor for this segment type.
350
- *
351
- * Three paths produce these: `addApprovalRequest` (live stream and replay),
352
- * `flushPendingApprovals` (tracked-but-unresolved after a history replay) and
353
- * `processApprovalResult` (a result arriving for a tracked request). They used
354
- * to hand-write the object each, which is how `fields` and `origin` reached
355
- * the card down one path and not the others — the same proposal rendering as a
356
- * bare title, or losing the marker that routes its buttons to the hub.
357
- */
358
- private buildApprovalRequestSegment(
359
- requestId: string,
360
- approval: PendingApproval,
361
- status: ChatApprovalStatus,
362
- ): ApprovalRequestSegment {
363
- return {
329
+ const segment: ApprovalRequestSegment = {
364
330
  type: 'approval_request',
365
331
  data: {
366
- command: approval.command,
367
- explanation: approval.explanation,
332
+ command,
333
+ explanation,
368
334
  requestId,
369
- approvalType: approval.approvalType,
370
- ...(approval.fields && approval.fields.length > 0 ? { fields: approval.fields } : {}),
371
- ...(approval.origin ? { origin: approval.origin } : {}),
335
+ approvalType,
372
336
  },
373
337
  status,
374
338
  onApprove: this.callbacks.onApprove,
375
339
  onReject: this.callbacks.onReject,
376
340
  }
341
+
342
+ this.segments.push(segment)
343
+ return this.getSegments()
377
344
  }
378
345
 
379
346
  /**
@@ -396,9 +363,6 @@ export class MessageSegmentAccumulator {
396
363
  status: ChatApprovalStatus = 'pending',
397
364
  executions?: Record<string, ApprovalBatchExecutionState>,
398
365
  resolvedByName?: string | null,
399
- /** Where the batch came from; `'guide'` keeps it inline and routes it to
400
- * the hub, exactly as for a single card (see `ApprovalBatchData.origin`). */
401
- origin?: 'guide',
402
366
  ): MessageSegment[] {
403
367
  const existingIndex = this.segments.findIndex(
404
368
  (s): s is ApprovalBatchSegment =>
@@ -408,9 +372,6 @@ export class MessageSegmentAccumulator {
408
372
  if (existingIndex !== -1) {
409
373
  const existing = this.segments[existingIndex] as ApprovalBatchSegment
410
374
  const mergedExecutions = executions ?? existing.data.executions
411
- // An upsert must not strip the marker: the flip to `approved` comes from
412
- // a later event that carries no origin of its own.
413
- const mergedOrigin = origin ?? existing.data.origin
414
375
  this.segments[existingIndex] = {
415
376
  ...existing,
416
377
  data: {
@@ -418,7 +379,6 @@ export class MessageSegmentAccumulator {
418
379
  approvalType,
419
380
  toolCalls,
420
381
  ...(mergedExecutions ? { executions: mergedExecutions } : {}),
421
- ...(mergedOrigin ? { origin: mergedOrigin } : {}),
422
382
  },
423
383
  status,
424
384
  resolvedByName: resolvedByName ?? existing.resolvedByName,
@@ -435,7 +395,6 @@ export class MessageSegmentAccumulator {
435
395
  approvalType,
436
396
  toolCalls,
437
397
  ...(executions ? { executions } : {}),
438
- ...(origin ? { origin } : {}),
439
398
  },
440
399
  status,
441
400
  resolvedByName,
@@ -458,17 +417,20 @@ export class MessageSegmentAccumulator {
458
417
  ): { segment: ApprovalRequestSegment; pendingData: PendingApproval | null } | null {
459
418
  const pendingApproval = this.pendingApprovals.get(requestId)
460
419
  const status: ChatApprovalStatus = approved ? 'approved' : 'rejected'
461
-
462
- const segment = this.buildApprovalRequestSegment(
463
- requestId,
464
- {
465
- ...pendingApproval,
420
+
421
+ const segment: ApprovalRequestSegment = {
422
+ type: 'approval_request',
423
+ data: {
466
424
  command: pendingApproval?.command || '',
425
+ explanation: pendingApproval?.explanation,
426
+ requestId,
467
427
  approvalType: pendingApproval?.approvalType || approvalType,
468
428
  },
469
429
  status,
470
- )
471
-
430
+ onApprove: this.callbacks.onApprove,
431
+ onReject: this.callbacks.onReject,
432
+ }
433
+
472
434
  this.segments.push(segment)
473
435
 
474
436
  if (pendingApproval) {
@@ -576,11 +538,22 @@ export class MessageSegmentAccumulator {
576
538
  */
577
539
  flushPendingApprovals(): ApprovalRequestSegment[] {
578
540
  const segments: ApprovalRequestSegment[] = []
579
-
541
+
580
542
  this.pendingApprovals.forEach((approval, requestId) => {
581
- segments.push(this.buildApprovalRequestSegment(requestId, approval, 'pending'))
543
+ segments.push({
544
+ type: 'approval_request',
545
+ data: {
546
+ command: approval.command,
547
+ explanation: approval.explanation,
548
+ requestId,
549
+ approvalType: approval.approvalType,
550
+ },
551
+ status: 'pending',
552
+ onApprove: this.callbacks.onApprove,
553
+ onReject: this.callbacks.onReject,
554
+ })
582
555
  })
583
-
556
+
584
557
  return segments
585
558
  }
586
559
 
@@ -654,8 +627,6 @@ export class MessageSegmentAccumulator {
654
627
  data.explanation,
655
628
  data.approvalType || '',
656
629
  status,
657
- data.fields,
658
- data.origin,
659
630
  )
660
631
  break
661
632
  }
@@ -668,7 +639,6 @@ export class MessageSegmentAccumulator {
668
639
  status,
669
640
  data.executions,
670
641
  resolvedByName,
671
- data.origin,
672
642
  )
673
643
  break
674
644
  }
@@ -40,8 +40,7 @@ import {
40
40
  import { ESCALATION_STATE, escalationResolvedStatus, type ChatStreamEvent } from '../../../chat-protocol/events'
41
41
  // One normalizer for ask rows, shared with the live decoder — history and the
42
42
  // stream must agree on which options are usable.
43
- import { guideFrameEvent, normalizeAskOptions } from '../../../chat-protocol/nats-decoder'
44
- import { approvalDisplaysInline, guideApprovalOrigin } from './approval-display'
43
+ import { normalizeAskOptions } from '../../../chat-protocol/nats-decoder'
45
44
  import { MessageSegmentAccumulator, createMessageSegmentAccumulator } from './message-segment-accumulator'
46
45
  import { applyApprovalStatusToSegment } from '../stream/message-mutations'
47
46
  import { getCommandText } from './tool-call-helpers'
@@ -116,21 +115,11 @@ export function decodeHistoricalMessageData(data: MessageData): ChatStreamEvent
116
115
  }
117
116
  return null
118
117
 
119
- // Two persisted shapes, mirroring the live `GUIDE` chunk: the answer body
120
- // (`text`) and a Product Guide frame the agent re-streamed (`payload`,
121
- // persisted so a card survives a reload). Decoded through the SAME
122
- // `guideFrameEvent` the live path uses — a second mapping here would let
123
- // history and realtime disagree about the same bytes.
124
- case MESSAGE_TYPE.GUIDE: {
118
+ case MESSAGE_TYPE.GUIDE:
125
119
  if ('text' in data && data.text) {
126
120
  return { type: 'guide-delta', text: data.text }
127
121
  }
128
- const payload = 'payload' in data ? data.payload : undefined
129
- if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
130
- return guideFrameEvent(payload as Record<string, unknown>)
131
- }
132
122
  return null
133
- }
134
123
 
135
124
  // Same completeness gate as the live decoder (`decodeNatsChunk`): a
136
125
  // persisted row without a question or without options is not a card the
@@ -372,24 +361,12 @@ function applyHistoryEvent(
372
361
  const approvalType = event.approvalType || 'CLIENT'
373
362
  const toolCalls = event.toolCalls as PendingToolCallData[] | undefined
374
363
  const isBatch = !!toolCalls && toolCalls.length > 0
375
- // Same rule the live kernels use — a card must not change where it
376
- // renders (or which backend its buttons hit) just because the page was
377
- // reloaded and it came back through history instead of the stream.
378
- const guideOrigin = guideApprovalOrigin(event)
379
364
 
380
- if (approvalDisplaysInline(event, approvalType, displayApprovalTypes)) {
365
+ if (!displayApprovalTypes || displayApprovalTypes.includes(approvalType)) {
381
366
  if (isBatch) {
382
367
  const status = (approvalStatuses[event.requestId] as ChatApprovalStatus) || 'pending'
383
368
  if (batchApprovalsEnabled) {
384
- accumulator.addApprovalBatch(
385
- event.requestId,
386
- approvalType,
387
- toolCalls!,
388
- status,
389
- undefined,
390
- undefined,
391
- guideOrigin,
392
- )
369
+ accumulator.addApprovalBatch(event.requestId, approvalType, toolCalls!, status)
393
370
  } else {
394
371
  // Flag OFF — unfold batch into N legacy approval cards (same id).
395
372
  for (const call of toolCalls!) {
@@ -400,8 +377,6 @@ function applyHistoryEvent(
400
377
  call.toolExplanation,
401
378
  approvalType,
402
379
  status,
403
- undefined,
404
- guideOrigin,
405
380
  )
406
381
  }
407
382
  }
@@ -414,28 +389,19 @@ function applyHistoryEvent(
414
389
  // sticky card on every history re-process. Mirror the batch path
415
390
  // and honor `approvalStatuses`.
416
391
  const resolvedStatus = approvalStatuses[event.requestId] as ChatApprovalStatus | undefined
417
- const isResolved = resolvedStatus === 'approved' || resolvedStatus === 'rejected'
418
- // A guide card is added inline even while pending. The tracked path
419
- // below ends in `flushPendingApprovals`, whose segments the consumer
420
- // lifts into a sticky footer — that is the treatment for the
421
- // consumer's OWN approvals, and it is not how the hub's chat renders
422
- // a proposal (nor where its preamble expects it).
423
- if (guideOrigin || isResolved) {
392
+ if (resolvedStatus === 'approved' || resolvedStatus === 'rejected') {
424
393
  accumulator.addApprovalRequest(
425
394
  event.requestId,
426
395
  event.command || '',
427
396
  event.explanation,
428
397
  approvalType,
429
- resolvedStatus ?? 'pending',
430
- event.fields,
431
- guideOrigin,
398
+ resolvedStatus,
432
399
  )
433
400
  } else {
434
401
  accumulator.trackApprovalRequest(event.requestId, {
435
402
  command: event.command || '',
436
403
  explanation: event.explanation,
437
404
  approvalType,
438
- fields: event.fields,
439
405
  })
440
406
  }
441
407
  }