@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.
- package/dist/{chunk-OLTGB32E.js → chunk-2HMZSCJY.js} +3179 -2078
- package/dist/chunk-2HMZSCJY.js.map +1 -0
- package/dist/chunk-4XLJWX2N.js +12 -0
- package/dist/chunk-4XLJWX2N.js.map +1 -0
- package/dist/{chunk-YGOJIDL5.cjs → chunk-C5EC5AZM.cjs} +1660 -559
- package/dist/chunk-C5EC5AZM.cjs.map +1 -0
- package/dist/chunk-VFKQMAUF.cjs +12 -0
- package/dist/chunk-VFKQMAUF.cjs.map +1 -0
- package/dist/components/chat/embeddable-chat.d.ts +35 -2
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/hooks/index.d.ts +3 -0
- package/dist/components/chat/hooks/index.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-embedded-chat.d.ts +10 -169
- package/dist/components/chat/hooks/use-embedded-chat.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +85 -0
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +124 -0
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-unified-chat.d.ts +33 -0
- package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -0
- package/dist/components/chat/index.cjs +8 -2
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +11 -5
- package/dist/components/chat/types/api.types.d.ts +17 -1
- package/dist/components/chat/types/api.types.d.ts.map +1 -1
- package/dist/components/chat/types/index.d.ts +1 -0
- package/dist/components/chat/types/index.d.ts.map +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +185 -0
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -0
- package/dist/components/features/index.cjs +3 -2
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +2 -1
- package/dist/components/index.cjs +26 -2
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +27 -3
- package/dist/components/navigation/index.cjs +3 -2
- package/dist/components/navigation/index.cjs.map +1 -1
- package/dist/components/navigation/index.js +2 -1
- package/dist/components/shared/delivery/delivery-lists.d.ts +16 -0
- package/dist/components/shared/delivery/delivery-lists.d.ts.map +1 -0
- package/dist/components/shared/delivery/delivery-table.d.ts +12 -0
- package/dist/components/shared/delivery/delivery-table.d.ts.map +1 -0
- package/dist/components/shared/delivery/index.d.ts +3 -0
- package/dist/components/shared/delivery/index.d.ts.map +1 -0
- package/dist/components/shared/dev-section/dev-section-page.d.ts +31 -0
- package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -0
- package/dist/components/shared/dev-section/dev-section-view.d.ts +34 -0
- package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -0
- package/dist/components/shared/dev-section/index.d.ts +3 -0
- package/dist/components/shared/dev-section/index.d.ts.map +1 -0
- package/dist/components/shared/legal-document/index.d.ts +10 -0
- package/dist/components/shared/legal-document/index.d.ts.map +1 -0
- package/dist/components/shared/legal-document/legal-document-page.d.ts +66 -0
- package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -0
- package/dist/components/shared/legal-document/use-legal-docs.d.ts +40 -0
- package/dist/components/shared/legal-document/use-legal-docs.d.ts.map +1 -0
- package/dist/components/shared/product-release/index.d.ts +2 -1
- package/dist/components/shared/product-release/index.d.ts.map +1 -1
- package/dist/components/shared/product-release/release-detail-page.d.ts +11 -7
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/shared/roadmap/index.d.ts +18 -0
- package/dist/components/shared/roadmap/index.d.ts.map +1 -0
- package/dist/components/shared/roadmap/roadmap-grid-skeleton.d.ts +24 -0
- package/dist/components/shared/roadmap/roadmap-grid-skeleton.d.ts.map +1 -0
- package/dist/components/shared/roadmap/roadmap-grid.d.ts +18 -0
- package/dist/components/shared/roadmap/roadmap-grid.d.ts.map +1 -0
- package/dist/components/shared/roadmap/use-roadmap-voting.d.ts +25 -0
- package/dist/components/shared/roadmap/use-roadmap-voting.d.ts.map +1 -0
- package/dist/components/ui/index.cjs +8 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +11 -5
- package/dist/components/ui/release-changelog-section.d.ts +13 -2
- package/dist/components/ui/release-changelog-section.d.ts.map +1 -1
- package/dist/embed-shims/index.cjs +1 -6
- package/dist/embed-shims/index.cjs.map +1 -1
- package/dist/embed-shims/index.js +1 -6
- package/dist/embed-shims/index.js.map +1 -1
- package/dist/index.cjs +18 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +19 -3
- package/dist/types/delivery.d.ts +49 -0
- package/dist/types/delivery.d.ts.map +1 -0
- package/dist/types/index.cjs +13 -0
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +12 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/dev-sections/index.d.ts +11 -0
- package/dist/utils/dev-sections/index.d.ts.map +1 -0
- package/dist/utils/dev-sections/openframe-dev-sections.d.ts +209 -0
- package/dist/utils/dev-sections/openframe-dev-sections.d.ts.map +1 -0
- package/dist/utils/index.cjs +82 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +81 -2
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/embeddable-chat.tsx +123 -8
- package/src/components/chat/hooks/index.ts +9 -2
- package/src/components/chat/hooks/use-embedded-chat.ts +18 -1016
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +372 -0
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +53 -6
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +1058 -0
- package/src/components/chat/hooks/use-unified-chat.ts +171 -0
- package/src/components/chat/types/api.types.ts +23 -1
- package/src/components/chat/types/index.ts +1 -0
- package/src/components/chat/types/unified-chat-state.types.ts +215 -0
- package/src/components/index.ts +8 -0
- package/src/components/shared/delivery/delivery-lists.tsx +199 -0
- package/src/components/shared/delivery/delivery-table.tsx +174 -0
- package/src/components/shared/delivery/index.ts +9 -0
- package/src/components/shared/dev-section/dev-section-page.tsx +72 -0
- package/src/components/shared/dev-section/dev-section-view.tsx +129 -0
- package/src/components/shared/dev-section/index.ts +2 -0
- package/src/components/shared/legal-document/index.ts +19 -0
- package/src/components/shared/legal-document/legal-document-page.tsx +178 -0
- package/src/components/shared/legal-document/use-legal-docs.ts +123 -0
- package/src/components/shared/product-release/index.ts +14 -3
- package/src/components/shared/product-release/release-detail-page.tsx +45 -7
- package/src/components/shared/roadmap/index.ts +23 -0
- package/src/components/shared/roadmap/roadmap-grid-skeleton.tsx +74 -0
- package/src/components/shared/roadmap/roadmap-grid.tsx +106 -0
- package/src/components/shared/roadmap/use-roadmap-voting.ts +163 -0
- package/src/components/ui/release-changelog-section.tsx +113 -32
- package/src/stories/EmbeddableChat.stories.tsx +186 -0
- package/src/types/delivery.ts +54 -0
- package/src/types/index.ts +1 -0
- package/src/utils/dev-sections/index.ts +17 -0
- package/src/utils/dev-sections/openframe-dev-sections.ts +148 -0
- package/src/utils/index.ts +6 -1
- package/dist/chunk-OLTGB32E.js.map +0 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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
|
-
|
|
241
|
-
void approvalType
|
|
288
|
+
callbacks.onApprovalResolved?.(requestId, status, approvalType)
|
|
242
289
|
break
|
|
243
290
|
}
|
|
244
291
|
|