@flamingo-stack/openframe-frontend-core 0.0.206 → 0.0.207-snapshot.20260526154403

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 (137) hide show
  1. package/dist/{chunk-OLTGB32E.js → chunk-2HMZSCJY.js} +3179 -2078
  2. package/dist/chunk-2HMZSCJY.js.map +1 -0
  3. package/dist/chunk-4XLJWX2N.js +12 -0
  4. package/dist/chunk-4XLJWX2N.js.map +1 -0
  5. package/dist/{chunk-YGOJIDL5.cjs → chunk-C5EC5AZM.cjs} +1660 -559
  6. package/dist/chunk-C5EC5AZM.cjs.map +1 -0
  7. package/dist/chunk-VFKQMAUF.cjs +12 -0
  8. package/dist/chunk-VFKQMAUF.cjs.map +1 -0
  9. package/dist/components/chat/embeddable-chat.d.ts +35 -2
  10. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  11. package/dist/components/chat/hooks/index.d.ts +3 -0
  12. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  13. package/dist/components/chat/hooks/use-embedded-chat.d.ts +10 -169
  14. package/dist/components/chat/hooks/use-embedded-chat.d.ts.map +1 -1
  15. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +85 -0
  16. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -0
  17. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
  18. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +124 -0
  19. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -0
  20. package/dist/components/chat/hooks/use-unified-chat.d.ts +33 -0
  21. package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -0
  22. package/dist/components/chat/index.cjs +8 -2
  23. package/dist/components/chat/index.cjs.map +1 -1
  24. package/dist/components/chat/index.js +11 -5
  25. package/dist/components/chat/types/api.types.d.ts +17 -1
  26. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  27. package/dist/components/chat/types/index.d.ts +1 -0
  28. package/dist/components/chat/types/index.d.ts.map +1 -1
  29. package/dist/components/chat/types/unified-chat-state.types.d.ts +185 -0
  30. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -0
  31. package/dist/components/features/index.cjs +3 -2
  32. package/dist/components/features/index.cjs.map +1 -1
  33. package/dist/components/features/index.js +2 -1
  34. package/dist/components/index.cjs +26 -2
  35. package/dist/components/index.cjs.map +1 -1
  36. package/dist/components/index.d.ts +4 -0
  37. package/dist/components/index.d.ts.map +1 -1
  38. package/dist/components/index.js +27 -3
  39. package/dist/components/navigation/index.cjs +3 -2
  40. package/dist/components/navigation/index.cjs.map +1 -1
  41. package/dist/components/navigation/index.js +2 -1
  42. package/dist/components/shared/delivery/delivery-lists.d.ts +16 -0
  43. package/dist/components/shared/delivery/delivery-lists.d.ts.map +1 -0
  44. package/dist/components/shared/delivery/delivery-table.d.ts +12 -0
  45. package/dist/components/shared/delivery/delivery-table.d.ts.map +1 -0
  46. package/dist/components/shared/delivery/index.d.ts +3 -0
  47. package/dist/components/shared/delivery/index.d.ts.map +1 -0
  48. package/dist/components/shared/dev-section/dev-section-page.d.ts +31 -0
  49. package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -0
  50. package/dist/components/shared/dev-section/dev-section-view.d.ts +34 -0
  51. package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -0
  52. package/dist/components/shared/dev-section/index.d.ts +3 -0
  53. package/dist/components/shared/dev-section/index.d.ts.map +1 -0
  54. package/dist/components/shared/legal-document/index.d.ts +10 -0
  55. package/dist/components/shared/legal-document/index.d.ts.map +1 -0
  56. package/dist/components/shared/legal-document/legal-document-page.d.ts +66 -0
  57. package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -0
  58. package/dist/components/shared/legal-document/use-legal-docs.d.ts +40 -0
  59. package/dist/components/shared/legal-document/use-legal-docs.d.ts.map +1 -0
  60. package/dist/components/shared/product-release/index.d.ts +2 -1
  61. package/dist/components/shared/product-release/index.d.ts.map +1 -1
  62. package/dist/components/shared/product-release/release-detail-page.d.ts +11 -7
  63. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  64. package/dist/components/shared/roadmap/index.d.ts +18 -0
  65. package/dist/components/shared/roadmap/index.d.ts.map +1 -0
  66. package/dist/components/shared/roadmap/roadmap-grid-skeleton.d.ts +24 -0
  67. package/dist/components/shared/roadmap/roadmap-grid-skeleton.d.ts.map +1 -0
  68. package/dist/components/shared/roadmap/roadmap-grid.d.ts +18 -0
  69. package/dist/components/shared/roadmap/roadmap-grid.d.ts.map +1 -0
  70. package/dist/components/shared/roadmap/use-roadmap-voting.d.ts +25 -0
  71. package/dist/components/shared/roadmap/use-roadmap-voting.d.ts.map +1 -0
  72. package/dist/components/ui/index.cjs +8 -2
  73. package/dist/components/ui/index.cjs.map +1 -1
  74. package/dist/components/ui/index.js +11 -5
  75. package/dist/components/ui/release-changelog-section.d.ts +13 -2
  76. package/dist/components/ui/release-changelog-section.d.ts.map +1 -1
  77. package/dist/embed-shims/index.cjs +1 -6
  78. package/dist/embed-shims/index.cjs.map +1 -1
  79. package/dist/embed-shims/index.js +1 -6
  80. package/dist/embed-shims/index.js.map +1 -1
  81. package/dist/index.cjs +18 -2
  82. package/dist/index.cjs.map +1 -1
  83. package/dist/index.js +19 -3
  84. package/dist/types/delivery.d.ts +49 -0
  85. package/dist/types/delivery.d.ts.map +1 -0
  86. package/dist/types/index.cjs +13 -0
  87. package/dist/types/index.cjs.map +1 -1
  88. package/dist/types/index.d.ts +1 -0
  89. package/dist/types/index.d.ts.map +1 -1
  90. package/dist/types/index.js +12 -1
  91. package/dist/types/index.js.map +1 -1
  92. package/dist/utils/dev-sections/index.d.ts +11 -0
  93. package/dist/utils/dev-sections/index.d.ts.map +1 -0
  94. package/dist/utils/dev-sections/openframe-dev-sections.d.ts +209 -0
  95. package/dist/utils/dev-sections/openframe-dev-sections.d.ts.map +1 -0
  96. package/dist/utils/index.cjs +82 -0
  97. package/dist/utils/index.cjs.map +1 -1
  98. package/dist/utils/index.d.ts +1 -0
  99. package/dist/utils/index.d.ts.map +1 -1
  100. package/dist/utils/index.js +81 -2
  101. package/dist/utils/index.js.map +1 -1
  102. package/package.json +1 -1
  103. package/src/components/chat/embeddable-chat.tsx +123 -8
  104. package/src/components/chat/hooks/index.ts +9 -2
  105. package/src/components/chat/hooks/use-embedded-chat.ts +18 -1016
  106. package/src/components/chat/hooks/use-nats-chat-adapter.ts +372 -0
  107. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +53 -6
  108. package/src/components/chat/hooks/use-sse-chat-adapter.ts +1058 -0
  109. package/src/components/chat/hooks/use-unified-chat.ts +171 -0
  110. package/src/components/chat/types/api.types.ts +23 -1
  111. package/src/components/chat/types/index.ts +1 -0
  112. package/src/components/chat/types/unified-chat-state.types.ts +215 -0
  113. package/src/components/index.ts +8 -0
  114. package/src/components/shared/delivery/delivery-lists.tsx +199 -0
  115. package/src/components/shared/delivery/delivery-table.tsx +174 -0
  116. package/src/components/shared/delivery/index.ts +9 -0
  117. package/src/components/shared/dev-section/dev-section-page.tsx +72 -0
  118. package/src/components/shared/dev-section/dev-section-view.tsx +129 -0
  119. package/src/components/shared/dev-section/index.ts +2 -0
  120. package/src/components/shared/legal-document/index.ts +19 -0
  121. package/src/components/shared/legal-document/legal-document-page.tsx +178 -0
  122. package/src/components/shared/legal-document/use-legal-docs.ts +123 -0
  123. package/src/components/shared/product-release/index.ts +14 -3
  124. package/src/components/shared/product-release/release-detail-page.tsx +45 -7
  125. package/src/components/shared/roadmap/index.ts +23 -0
  126. package/src/components/shared/roadmap/roadmap-grid-skeleton.tsx +74 -0
  127. package/src/components/shared/roadmap/roadmap-grid.tsx +106 -0
  128. package/src/components/shared/roadmap/use-roadmap-voting.ts +163 -0
  129. package/src/components/ui/release-changelog-section.tsx +113 -32
  130. package/src/stories/EmbeddableChat.stories.tsx +186 -0
  131. package/src/types/delivery.ts +54 -0
  132. package/src/types/index.ts +1 -0
  133. package/src/utils/dev-sections/index.ts +17 -0
  134. package/src/utils/dev-sections/openframe-dev-sections.ts +148 -0
  135. package/src/utils/index.ts +6 -1
  136. package/dist/chunk-OLTGB32E.js.map +0 -1
  137. package/dist/chunk-YGOJIDL5.cjs.map +0 -1
@@ -0,0 +1,372 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * useNatsChatAdapter — the NATS/Mingo-mode transport adapter for the
5
+ * unified chat surface. Companion of `useSseChatAdapter` (Guide mode);
6
+ * both implement the same `UnifiedChatState` contract so the public
7
+ * `useChat({ mode })` can dispatch between them with zero shell-side
8
+ * branching.
9
+ *
10
+ * Composition (no new logic — all the pieces already exist in the lib):
11
+ *
12
+ * ┌──────────────────────────────────────────────────────────────┐
13
+ * │ useNatsDialogSubscription live tail of agent events │
14
+ * │ ↓ │
15
+ * │ onEvent → processChunk │
16
+ * │ ↓ │
17
+ * │ useRealtimeChunkProcessor chunk → MessageSegment[] │
18
+ * │ ↓ │
19
+ * │ onSegmentsUpdate → updates assistant message in state │
20
+ * │ │
21
+ * │ useChunkCatchup back-fill missed chunks after │
22
+ * │ activation / reconnect │
23
+ * │ │
24
+ * │ config.publishUserMessage consumer-owned send (HTTP/NATS) │
25
+ * └──────────────────────────────────────────────────────────────┘
26
+ *
27
+ * Why publish is consumer-owned: the NATS module exposes a low-level
28
+ * `publishBytes/String/Json` but the actual user-message endpoint varies
29
+ * by deployment (REST POST in some, NATS subject in others). The adapter
30
+ * stays agnostic — caller wires up "user typed something, do X" via
31
+ * the `publishUserMessage` callback.
32
+ *
33
+ * The `active` option gates the live subscription so the unified chat
34
+ * shell can keep both adapters mounted while only paying network cost
35
+ * for the currently-displayed mode.
36
+ */
37
+
38
+ import {
39
+ useCallback,
40
+ useEffect,
41
+ useMemo,
42
+ useRef,
43
+ useState,
44
+ type MutableRefObject,
45
+ } from 'react'
46
+ import { useNatsDialogSubscription } from './use-nats-dialog-subscription'
47
+ import { useRealtimeChunkProcessor } from './use-realtime-chunk-processor'
48
+ import { useChunkCatchup } from './use-chunk-catchup'
49
+ import type {
50
+ ChunkData,
51
+ FetchChunksFunction,
52
+ MessageSegment,
53
+ NatsMessageType,
54
+ StreamingPhase,
55
+ } from '../types'
56
+ import type {
57
+ UnifiedChatState,
58
+ UnifiedChatMessage,
59
+ UnifiedSendMessageOptions,
60
+ } from '../types/unified-chat-state.types'
61
+ import type { ChatRef } from '../chat-ref.types'
62
+
63
+ // =============================================================================
64
+ // Config + options
65
+ // =============================================================================
66
+
67
+ /**
68
+ * Consumer-supplied configuration for the NATS chat adapter.
69
+ *
70
+ * Every field is consumer-owned — the lib does not assume a particular
71
+ * backend protocol or auth scheme. Hosts wire these up against their
72
+ * own OpenFrame deployment.
73
+ */
74
+ export interface UseNatsChatAdapterConfig {
75
+ /**
76
+ * Active conversation/dialog id. When `null` the adapter stays
77
+ * subscription-idle (no NATS connection, no catchup fetch). Set this
78
+ * once the consumer's "open new conversation" flow has allocated an
79
+ * id from the backend.
80
+ */
81
+ dialogId: string | null
82
+
83
+ /**
84
+ * Build the NATS WebSocket URL. Returning `null` short-circuits the
85
+ * subscription — same contract as `useNatsDialogSubscription`.
86
+ */
87
+ getNatsWsUrl: () => string | null
88
+
89
+ /**
90
+ * Optional NATS client auth.
91
+ */
92
+ clientConfig?: {
93
+ name?: string
94
+ user?: string
95
+ pass?: string
96
+ }
97
+
98
+ /**
99
+ * Send a user message upstream. Consumer-owned: typically an
100
+ * authenticated HTTP POST to the OpenFrame chat endpoint, or a
101
+ * direct NATS publish to a dedicated subject.
102
+ *
103
+ * The adapter does NOT couple to the wire format — it only:
104
+ * 1. appends the user message to local state for immediate render
105
+ * 2. flips streamingPhase to 'thinking' so the input UI shows status
106
+ * 3. calls this callback
107
+ *
108
+ * Reply arrives asynchronously as NATS chunks via the live tail and
109
+ * is accumulated into the trailing assistant message.
110
+ */
111
+ publishUserMessage: (
112
+ text: string,
113
+ options: { hidden?: boolean; dialogId: string | null },
114
+ ) => Promise<void> | void
115
+
116
+ /**
117
+ * Historical-chunk fetcher used by `useChunkCatchup` to back-fill
118
+ * events that happened while the user was in another mode or before
119
+ * the websocket came online. Consumer-owned: typically a REST GET
120
+ * against the OpenFrame chat-history endpoint.
121
+ *
122
+ * When omitted, `useChunkCatchup` falls back to its own default
123
+ * fetch implementation — see hook docs for the contract.
124
+ */
125
+ fetchChunks?: FetchChunksFunction
126
+
127
+ /**
128
+ * Whether THINKING chunks are surfaced as segments. Default `false`
129
+ * (parity with the existing `useRealtimeChunkProcessor` default).
130
+ */
131
+ enableThinking?: boolean
132
+
133
+ /**
134
+ * Mirrors `UseRealtimeChunkProcessorOptions.batchApprovalsEnabled`.
135
+ * Default `true` — single batch card per APPROVAL_REQUEST with
136
+ * `toolCalls[]`. Set `false` to fall back to legacy per-tool cards.
137
+ */
138
+ batchApprovalsEnabled?: boolean
139
+ }
140
+
141
+ /**
142
+ * Per-call options for `useNatsChatAdapter`. Carries only the
143
+ * activation gate — config travels through the config object so it
144
+ * survives mode swaps without re-mounting.
145
+ */
146
+ export interface UseNatsChatAdapterOptions {
147
+ /**
148
+ * When `false` the adapter goes idle: no NATS subscription, no
149
+ * catchup fetch, no publish. Local message state is preserved so
150
+ * the user sees their history when the mode flips back to active.
151
+ * Default `true`.
152
+ */
153
+ active?: boolean
154
+ }
155
+
156
+ // =============================================================================
157
+ // Internal helpers
158
+ // =============================================================================
159
+
160
+ function nextId(role: 'user' | 'assistant'): string {
161
+ // Date.now() + counter sliver keeps ids monotonic even when two
162
+ // messages are produced inside the same ms tick (user + assistant
163
+ // placeholder fire back-to-back from a single sendMessage call).
164
+ return `${role}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
165
+ }
166
+
167
+ /**
168
+ * Replace (or append) the trailing assistant message with the latest
169
+ * accumulated segments. Mirrors the use-chat.ts pattern so render
170
+ * behaviour matches the SSE adapter exactly.
171
+ */
172
+ function updateTrailingAssistant(
173
+ prev: UnifiedChatMessage[],
174
+ segments: MessageSegment[],
175
+ ): UnifiedChatMessage[] {
176
+ const last = prev[prev.length - 1]
177
+ if (!last || last.role !== 'assistant') {
178
+ // No placeholder exists — append a fresh assistant message.
179
+ return [
180
+ ...prev,
181
+ {
182
+ id: nextId('assistant'),
183
+ role: 'assistant',
184
+ content: '',
185
+ segments,
186
+ },
187
+ ]
188
+ }
189
+ return [
190
+ ...prev.slice(0, -1),
191
+ { ...last, segments },
192
+ ]
193
+ }
194
+
195
+ // =============================================================================
196
+ // Hook
197
+ // =============================================================================
198
+
199
+ export function useNatsChatAdapter(
200
+ config: UseNatsChatAdapterConfig,
201
+ options: UseNatsChatAdapterOptions = {},
202
+ ): UnifiedChatState {
203
+ const { active = true } = options
204
+ const {
205
+ dialogId,
206
+ getNatsWsUrl,
207
+ clientConfig,
208
+ publishUserMessage,
209
+ fetchChunks,
210
+ enableThinking,
211
+ batchApprovalsEnabled,
212
+ } = config
213
+
214
+ const [messages, setMessages] = useState<UnifiedChatMessage[]>([])
215
+ const [streamingPhase, setStreamingPhase] = useState<StreamingPhase>('idle')
216
+
217
+ // Stable callback ref so `useRealtimeChunkProcessor`'s options object
218
+ // doesn't churn every render and tear down the accumulator state.
219
+ const callbacksRef: MutableRefObject<{
220
+ onSegmentsUpdate: (segments: MessageSegment[]) => void
221
+ onStreamStart: () => void
222
+ onStreamEnd: () => void
223
+ }> = useRef({
224
+ onSegmentsUpdate: (segments: MessageSegment[]) => {
225
+ setMessages((prev) => updateTrailingAssistant(prev, segments))
226
+ },
227
+ onStreamStart: () => setStreamingPhase('streaming'),
228
+ onStreamEnd: () => setStreamingPhase('idle'),
229
+ })
230
+
231
+ // Real-time chunk → segment processor.
232
+ const { processChunk, reset: resetAccumulator } = useRealtimeChunkProcessor({
233
+ callbacks: {
234
+ onSegmentsUpdate: (segments) => callbacksRef.current.onSegmentsUpdate(segments),
235
+ onStreamStart: () => callbacksRef.current.onStreamStart(),
236
+ onStreamEnd: () => callbacksRef.current.onStreamEnd(),
237
+ },
238
+ enableThinking,
239
+ batchApprovalsEnabled,
240
+ })
241
+
242
+ // History catchup — back-fills chunks emitted while the adapter was
243
+ // inactive or before the WS came online.
244
+ const {
245
+ processChunk: catchupProcessChunk,
246
+ catchUpChunks,
247
+ startInitialBuffering,
248
+ resetChunkTracking,
249
+ } = useChunkCatchup({
250
+ dialogId: active ? dialogId : null,
251
+ onChunkReceived: (chunk: ChunkData) => processChunk(chunk),
252
+ fetchChunks,
253
+ })
254
+
255
+ // Trigger initial backfill whenever a fresh dialog activates. Mirrors the
256
+ // pattern in `.use-chunk-catchup.md`: enable buffering first so realtime
257
+ // chunks queue behind the historical fetch, then flush in order.
258
+ useEffect(() => {
259
+ if (!active || !dialogId) return
260
+ resetChunkTracking()
261
+ startInitialBuffering()
262
+ catchUpChunks().catch((err) => {
263
+ console.error('[useNatsChatAdapter] initial catchup failed:', err)
264
+ })
265
+ }, [active, dialogId, resetChunkTracking, startInitialBuffering, catchUpChunks])
266
+
267
+ // Live tail subscription. `enabled` is gated on both `active` and a
268
+ // non-null dialogId so the consumer doesn't pay socket cost before
269
+ // a conversation exists.
270
+ useNatsDialogSubscription({
271
+ enabled: active && dialogId != null,
272
+ dialogId,
273
+ getNatsWsUrl,
274
+ clientConfig,
275
+ onEvent: (payload: unknown, messageType: NatsMessageType) => {
276
+ // Route via catchup so the buffer/dedupe logic stays consistent
277
+ // with historical playback. `useChunkCatchup` itself forwards to
278
+ // `processChunk` once dedupe checks pass.
279
+ catchupProcessChunk(payload as ChunkData, messageType)
280
+ },
281
+ })
282
+
283
+ // ─── Public API ───────────────────────────────────────────────────────────
284
+
285
+ const sendMessage = useCallback(
286
+ async (
287
+ text: string,
288
+ sendOptions?: UnifiedSendMessageOptions,
289
+ ): Promise<void> => {
290
+ const hidden = sendOptions?.hidden ?? false
291
+
292
+ // Optimistically append the user bubble + an empty assistant
293
+ // placeholder. The assistant body fills in as NATS chunks land.
294
+ setMessages((prev) => [
295
+ ...prev,
296
+ {
297
+ id: nextId('user'),
298
+ role: 'user',
299
+ content: text,
300
+ ...(hidden ? { hidden: true } : {}),
301
+ },
302
+ {
303
+ id: nextId('assistant'),
304
+ role: 'assistant',
305
+ content: '',
306
+ segments: [],
307
+ },
308
+ ])
309
+ setStreamingPhase('thinking')
310
+
311
+ await publishUserMessage(text, { hidden, dialogId })
312
+ },
313
+ [publishUserMessage, dialogId],
314
+ )
315
+
316
+ const stopMessage = useCallback(() => {
317
+ // NATS streams are driven server-side; the client can't really
318
+ // "cancel" an in-flight agent task without backend cooperation.
319
+ // For now we just drop the UI status — incoming chunks will still
320
+ // be accepted and rendered if the agent completes anyway.
321
+ setStreamingPhase('idle')
322
+ }, [])
323
+
324
+ const clearMessages = useCallback(() => {
325
+ setMessages([])
326
+ resetAccumulator()
327
+ setStreamingPhase('idle')
328
+ }, [resetAccumulator])
329
+
330
+ // No-op refs — Mingo agent has no RAG entity-card affordances.
331
+ const discussRef = useCallback((_ref: ChatRef) => {
332
+ /* no-op in Mingo mode */
333
+ }, [])
334
+ const displayRef = useCallback((_ref: ChatRef) => {
335
+ /* no-op in Mingo mode */
336
+ }, [])
337
+
338
+ // ─── Return shape ─────────────────────────────────────────────────────────
339
+
340
+ const isLoading = streamingPhase !== 'idle'
341
+
342
+ return useMemo<UnifiedChatState>(
343
+ () => ({
344
+ messages,
345
+ isLoading,
346
+ streamingPhase,
347
+ sendMessage,
348
+ stopMessage,
349
+ clearMessages,
350
+ discussRef,
351
+ displayRef,
352
+ // SSE-only telemetry — null in NATS mode.
353
+ currentProvider: null,
354
+ currentModelLabel: null,
355
+ currentContextWindowMaxTokens: null,
356
+ currentInputTokens: null,
357
+ currentOutputTokens: null,
358
+ currentCacheHitRatePct: null,
359
+ currentUsageBreakdown: null,
360
+ }),
361
+ [
362
+ messages,
363
+ isLoading,
364
+ streamingPhase,
365
+ sendMessage,
366
+ stopMessage,
367
+ clearMessages,
368
+ discussRef,
369
+ displayRef,
370
+ ],
371
+ )
372
+ }
@@ -55,11 +55,22 @@ export function useRealtimeChunkProcessor(
55
55
  })
56
56
  }
57
57
 
58
+ // Resumed dialog: a MESSAGE_START already fired server-side. Treat
59
+ // subsequent continuation chunks (after the next MESSAGE_END) as
60
+ // post-stream so they append into the existing bubble instead of
61
+ // replacing its content via the cold-start cumulative path.
62
+ hasEverStreamedRef.current = true
58
63
  hasInitializedWithData.current = true
59
64
  }
60
65
  }, [initialState, callbacks])
61
66
 
62
67
  const isInStreamRef = useRef(false)
68
+ // Distinguishes post-MESSAGE_END continuation (append into prior bubble)
69
+ // from cold-start before any MESSAGE_START (cumulative; otherwise
70
+ // appendSegmentsToLastAssistant silently drops the chunk when no
71
+ // assistant bubble exists yet). Flipped true on MESSAGE_START and on
72
+ // resumed-dialog initializeWithState.
73
+ const hasEverStreamedRef = useRef(false)
63
74
 
64
75
  // Track pending escalated approvals (single or batch)
65
76
  const pendingEscalatedRef = useRef<
@@ -85,6 +96,7 @@ export function useRealtimeChunkProcessor(
85
96
  switch (action.action) {
86
97
  case 'message_start':
87
98
  isInStreamRef.current = true
99
+ hasEverStreamedRef.current = true
88
100
  callbacks.onStreamStart?.()
89
101
  accumulator.resetSegments()
90
102
  break
@@ -101,17 +113,43 @@ export function useRealtimeChunkProcessor(
101
113
 
102
114
  case 'text': {
103
115
  const segments = accumulator.appendText(action.text)
104
- callbacks.onSegmentsUpdate?.(segments)
116
+ // Append-mode only for *true* post-stream continuation (after a
117
+ // MESSAGE_END we actually saw). Cold-start chunks (no prior
118
+ // MESSAGE_START) emit cumulative segments so the consumer can
119
+ // spawn the first assistant bubble — otherwise appendSegmentsToLastAssistant
120
+ // silently drops the chunk when no last assistant exists.
121
+ if (isInStreamRef.current || !hasEverStreamedRef.current) {
122
+ callbacks.onSegmentsUpdate?.(segments)
123
+ } else {
124
+ callbacks.onSegmentsUpdate?.([{ type: 'text', text: action.text }], { append: true })
125
+ }
105
126
  break
106
127
  }
107
128
 
108
129
  case 'thinking': {
109
130
  const segments = accumulator.appendThinking(action.text)
110
- callbacks.onSegmentsUpdate?.(segments)
131
+ if (isInStreamRef.current || !hasEverStreamedRef.current) {
132
+ callbacks.onSegmentsUpdate?.(segments)
133
+ } else {
134
+ callbacks.onSegmentsUpdate?.([{ type: 'thinking', text: action.text }], { append: true })
135
+ }
111
136
  break
112
137
  }
113
138
 
114
139
  case 'tool_execution': {
140
+ // Post-MESSAGE_END tool chunks (cancellations / async batch
141
+ // results for a batch in a prior bubble) flow only through the
142
+ // cross-message updater. Skipping the accumulator avoids
143
+ // pushing a standalone segment that the next text chunk would
144
+ // replay into a new bubble.
145
+ if (!isInStreamRef.current && callbacks.onToolExecuted) {
146
+ callbacks.onToolExecuted(action.segment)
147
+ break
148
+ }
149
+ // In-stream: accumulator-driven update of the streaming bubble
150
+ // is the source of truth. Don't fire onToolExecuted here — its
151
+ // cross-message scan is first-match-wins and could touch a
152
+ // same-execId segment in a prior bubble (agent retry case).
115
153
  const segments = accumulator.addToolExecution(action.segment)
116
154
  callbacks.onSegmentsUpdate?.(segments)
117
155
  break
@@ -234,11 +272,20 @@ export function useRealtimeChunkProcessor(
234
272
  callbacks.onSegmentsUpdate?.(segments)
235
273
  }
236
274
  } else {
237
- const segments = accumulator.updateApprovalStatus(requestId, status)
238
- callbacks.onSegmentsUpdate?.(segments)
275
+ // Always keep the in-memory accumulator in sync so a following
276
+ // text/tool chunk replays the resolved status into the message.
277
+ accumulator.updateApprovalStatus(requestId, status)
278
+ // When the consumer wires cross-message resolution via
279
+ // `onApprovalResolved`, skip `onSegmentsUpdate` here: this path
280
+ // routes through `ensureAssistantMessage` + `updateStreamingMessageSegments`,
281
+ // which adopts/creates an assistant bubble and replays the
282
+ // accumulator's segments into it — turning a status flip into a
283
+ // bubble overwrite that wipes the original card.
284
+ if (!callbacks.onApprovalResolved) {
285
+ callbacks.onSegmentsUpdate?.(accumulator.getSegments())
286
+ }
239
287
  }
240
- // approvalType from the result is informational; not consumed downstream yet.
241
- void approvalType
288
+ callbacks.onApprovalResolved?.(requestId, status, approvalType)
242
289
  break
243
290
  }
244
291