@flamingo-stack/openframe-frontend-core 0.0.444 → 0.0.445
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-DTMJWBDQ.js → chunk-22NSWMI6.js} +3 -3
- package/dist/{chunk-NDWIFCLS.js → chunk-3BHV2QAH.js} +555 -135
- package/dist/{chunk-NDWIFCLS.js.map → chunk-3BHV2QAH.js.map} +1 -1
- package/dist/{chunk-PMTR5W5G.js → chunk-3DQ5BFPM.js} +5 -5
- package/dist/{chunk-RALJEQWM.cjs → chunk-3HYSD7NR.cjs} +15 -15
- package/dist/{chunk-RALJEQWM.cjs.map → chunk-3HYSD7NR.cjs.map} +1 -1
- package/dist/{chunk-YP7VJPCJ.cjs → chunk-5DWPXK37.cjs} +40 -40
- package/dist/{chunk-YP7VJPCJ.cjs.map → chunk-5DWPXK37.cjs.map} +1 -1
- package/dist/{chunk-OXZLBIR7.cjs → chunk-5FUQTSTT.cjs} +18 -18
- package/dist/{chunk-OXZLBIR7.cjs.map → chunk-5FUQTSTT.cjs.map} +1 -1
- package/dist/{chunk-MG4AMIFX.js → chunk-5NYXGWBN.js} +2 -2
- package/dist/{chunk-LCJGOV6P.js → chunk-7R256DKK.js} +2 -2
- package/dist/{chunk-2PLNDTRG.cjs → chunk-DTLNCOSG.cjs} +32 -32
- package/dist/{chunk-2PLNDTRG.cjs.map → chunk-DTLNCOSG.cjs.map} +1 -1
- package/dist/{chunk-3SCNEGL2.js → chunk-FIAXFNAA.js} +5 -5
- package/dist/{chunk-FQASXKBK.js → chunk-G2KXNMFQ.js} +2 -2
- package/dist/{chunk-H7H3AFLF.js → chunk-H2DYCT6M.js} +2 -2
- package/dist/{chunk-CE3AITV2.js → chunk-H352AZJN.js} +6 -6
- package/dist/{chunk-DCDB3SM4.js → chunk-K4RASNQM.js} +3 -3
- package/dist/{chunk-WXU6A6CA.cjs → chunk-KISQUH33.cjs} +73 -73
- package/dist/{chunk-WXU6A6CA.cjs.map → chunk-KISQUH33.cjs.map} +1 -1
- package/dist/{chunk-BYTJNRJW.cjs → chunk-MPSA6YAM.cjs} +40 -40
- package/dist/{chunk-BYTJNRJW.cjs.map → chunk-MPSA6YAM.cjs.map} +1 -1
- package/dist/{chunk-ABIRMYG5.js → chunk-MQUS6BFR.js} +6 -6
- package/dist/{chunk-DSW6FIEF.cjs → chunk-N64VTHZ5.cjs} +7 -7
- package/dist/{chunk-DSW6FIEF.cjs.map → chunk-N64VTHZ5.cjs.map} +1 -1
- package/dist/{chunk-F6NMMPDL.cjs → chunk-P3FN4NAE.cjs} +29 -29
- package/dist/{chunk-F6NMMPDL.cjs.map → chunk-P3FN4NAE.cjs.map} +1 -1
- package/dist/{chunk-PHWQLKVE.js → chunk-P47NN7RY.js} +29 -31
- package/dist/chunk-P47NN7RY.js.map +1 -0
- package/dist/{chunk-2CJ734N2.cjs → chunk-PTT4J5GF.cjs} +3 -3
- package/dist/{chunk-2CJ734N2.cjs.map → chunk-PTT4J5GF.cjs.map} +1 -1
- package/dist/{chunk-4R3SVOKH.cjs → chunk-QL2EI56X.cjs} +797 -377
- package/dist/chunk-QL2EI56X.cjs.map +1 -0
- package/dist/{chunk-VI2QQIGR.cjs → chunk-R6WGCJBB.cjs} +73 -73
- package/dist/{chunk-VI2QQIGR.cjs.map → chunk-R6WGCJBB.cjs.map} +1 -1
- package/dist/{chunk-Q6XOEVPH.cjs → chunk-RXLMN6B5.cjs} +12 -12
- package/dist/{chunk-Q6XOEVPH.cjs.map → chunk-RXLMN6B5.cjs.map} +1 -1
- package/dist/{chunk-F2XHBAHT.js → chunk-TCEBFMLQ.js} +2 -2
- package/dist/{chunk-7CZ4MJSI.cjs → chunk-VIJARDBM.cjs} +3 -3
- package/dist/{chunk-7CZ4MJSI.cjs.map → chunk-VIJARDBM.cjs.map} +1 -1
- package/dist/{chunk-PXXS27EE.cjs → chunk-W24U7ZUV.cjs} +40 -42
- package/dist/chunk-W24U7ZUV.cjs.map +1 -0
- package/dist/{chunk-37ZZQXHK.js → chunk-XPZLIONZ.js} +4 -4
- package/dist/{chunk-FO4CI4R5.cjs → chunk-YCLSV2H6.cjs} +5 -5
- package/dist/{chunk-FO4CI4R5.cjs.map → chunk-YCLSV2H6.cjs.map} +1 -1
- package/dist/{chunk-U7U2CZBZ.js → chunk-Z4N7XLFG.js} +3 -3
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/hooks/use-chunk-catchup.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-jetstream-dialog-subscription.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +44 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-dialog-subscription.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +10 -4
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +9 -3
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/index.cjs +142 -136
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +18 -12
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/index.cjs +10 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +9 -3
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +10 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9 -3
- package/dist/nats/index.cjs +2 -2
- package/dist/nats/index.js +1 -1
- package/dist/nats/shared-connection.d.ts +6 -0
- package/dist/nats/shared-connection.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/hooks/__tests__/nats-adapter-message-updaters.test.ts +270 -0
- package/src/components/chat/hooks/use-chunk-catchup.ts +212 -57
- package/src/components/chat/hooks/use-jetstream-dialog-subscription.ts +12 -8
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +574 -9
- package/src/components/chat/hooks/use-nats-dialog-subscription.ts +12 -6
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +40 -3
- package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +62 -0
- package/src/components/chat/utils/message-segment-accumulator.ts +6 -0
- package/src/components/chat/utils/process-historical-messages.ts +25 -3
- package/src/nats/shared-connection.ts +42 -29
- package/dist/chunk-4R3SVOKH.cjs.map +0 -1
- package/dist/chunk-PHWQLKVE.js.map +0 -1
- package/dist/chunk-PXXS27EE.cjs.map +0 -1
- /package/dist/{chunk-DTMJWBDQ.js.map → chunk-22NSWMI6.js.map} +0 -0
- /package/dist/{chunk-PMTR5W5G.js.map → chunk-3DQ5BFPM.js.map} +0 -0
- /package/dist/{chunk-MG4AMIFX.js.map → chunk-5NYXGWBN.js.map} +0 -0
- /package/dist/{chunk-LCJGOV6P.js.map → chunk-7R256DKK.js.map} +0 -0
- /package/dist/{chunk-3SCNEGL2.js.map → chunk-FIAXFNAA.js.map} +0 -0
- /package/dist/{chunk-FQASXKBK.js.map → chunk-G2KXNMFQ.js.map} +0 -0
- /package/dist/{chunk-H7H3AFLF.js.map → chunk-H2DYCT6M.js.map} +0 -0
- /package/dist/{chunk-CE3AITV2.js.map → chunk-H352AZJN.js.map} +0 -0
- /package/dist/{chunk-DCDB3SM4.js.map → chunk-K4RASNQM.js.map} +0 -0
- /package/dist/{chunk-ABIRMYG5.js.map → chunk-MQUS6BFR.js.map} +0 -0
- /package/dist/{chunk-F2XHBAHT.js.map → chunk-TCEBFMLQ.js.map} +0 -0
- /package/dist/{chunk-37ZZQXHK.js.map → chunk-XPZLIONZ.js.map} +0 -0
- /package/dist/{chunk-U7U2CZBZ.js.map → chunk-Z4N7XLFG.js.map} +0 -0
|
@@ -182,7 +182,13 @@ export function useNatsDialogSubscription({
|
|
|
182
182
|
}
|
|
183
183
|
}, [enabled, wsUrl])
|
|
184
184
|
|
|
185
|
+
// Key the subscription effects on the topics CONTENT, not the array
|
|
186
|
+
// identity — hosts often pass an inline array, and an identity dep made
|
|
187
|
+
// every render tear down + recreate the subscriptions (plain NATS delivers
|
|
188
|
+
// nothing during that gap, so chunks were silently lost).
|
|
185
189
|
const topicsKey = topics.join(',')
|
|
190
|
+
const topicsRef = useRef(topics)
|
|
191
|
+
topicsRef.current = topics
|
|
186
192
|
const lastSubscribedDialogIdRef = useRef<string | null>(null)
|
|
187
193
|
const isConnectedRef = useRef(isConnected)
|
|
188
194
|
|
|
@@ -260,7 +266,7 @@ export function useNatsDialogSubscription({
|
|
|
260
266
|
}
|
|
261
267
|
}
|
|
262
268
|
|
|
263
|
-
|
|
269
|
+
topicsRef.current.forEach((topic) => {
|
|
264
270
|
const subscription = client.subscribeBytes(
|
|
265
271
|
`chat.${dialogId}.${topic}`,
|
|
266
272
|
handleMessage(topic),
|
|
@@ -268,7 +274,7 @@ export function useNatsDialogSubscription({
|
|
|
268
274
|
)
|
|
269
275
|
subscriptionRefs.current.set(topic, subscription)
|
|
270
276
|
})
|
|
271
|
-
|
|
277
|
+
|
|
272
278
|
lastSubscribedDialogIdRef.current = dialogId
|
|
273
279
|
setIsSubscribed(true)
|
|
274
280
|
onSubscribedRef.current?.()
|
|
@@ -292,7 +298,7 @@ export function useNatsDialogSubscription({
|
|
|
292
298
|
lastSubscribedDialogIdRef.current = null
|
|
293
299
|
abortControllerRef.current = null
|
|
294
300
|
}
|
|
295
|
-
}, [enabled, dialogId, topicsKey
|
|
301
|
+
}, [enabled, dialogId, topicsKey])
|
|
296
302
|
|
|
297
303
|
useEffect(() => {
|
|
298
304
|
if (!enabled || !currentDialogIdRef.current || !isConnected) {
|
|
@@ -322,7 +328,7 @@ export function useNatsDialogSubscription({
|
|
|
322
328
|
}
|
|
323
329
|
}
|
|
324
330
|
|
|
325
|
-
|
|
331
|
+
topicsRef.current.forEach((topic) => {
|
|
326
332
|
const subscription = client.subscribeBytes(
|
|
327
333
|
`chat.${dialogId}.${topic}`,
|
|
328
334
|
handleMessage(topic),
|
|
@@ -330,14 +336,14 @@ export function useNatsDialogSubscription({
|
|
|
330
336
|
)
|
|
331
337
|
subscriptionRefs.current.set(topic, subscription)
|
|
332
338
|
})
|
|
333
|
-
|
|
339
|
+
|
|
334
340
|
lastSubscribedDialogIdRef.current = dialogId
|
|
335
341
|
setIsSubscribed(true)
|
|
336
342
|
onSubscribedRef.current?.()
|
|
337
343
|
} else if (subscriptionRefs.current.size > 0) {
|
|
338
344
|
setIsSubscribed(true)
|
|
339
345
|
}
|
|
340
|
-
}, [isConnected, enabled,
|
|
346
|
+
}, [isConnected, enabled, topicsKey])
|
|
341
347
|
|
|
342
348
|
return { isConnected, isSubscribed, reconnectionCount }
|
|
343
349
|
}
|
|
@@ -193,6 +193,16 @@ export function useRealtimeChunkProcessor(
|
|
|
193
193
|
// is the source of truth. Don't fire onToolExecuted here — its
|
|
194
194
|
// cross-message scan is first-match-wins and could touch a
|
|
195
195
|
// same-execId segment in a prior bubble (agent retry case).
|
|
196
|
+
//
|
|
197
|
+
// KNOWN EDGE (accepted): a slow async batch tool whose EXECUTED
|
|
198
|
+
// lands only AFTER the continuation stream's MESSAGE_START takes
|
|
199
|
+
// this path too — the freshly reset accumulator has no batch
|
|
200
|
+
// segment to merge into, so the chunk renders as a standalone card
|
|
201
|
+
// in the new bubble while the prior batch card's executions slot
|
|
202
|
+
// stays 'executing' until the next history refetch. Routing such
|
|
203
|
+
// chunks cross-message would reintroduce the retry hazard above;
|
|
204
|
+
// revisit only if the backend confirms batch executions can
|
|
205
|
+
// overlap the continuation stream.
|
|
196
206
|
const segments = accumulator.addToolExecution(action.segment)
|
|
197
207
|
emitSegments(segments)
|
|
198
208
|
break
|
|
@@ -286,6 +296,26 @@ export function useRealtimeChunkProcessor(
|
|
|
286
296
|
approvalType: escalatedData.approvalType,
|
|
287
297
|
})
|
|
288
298
|
|
|
299
|
+
// The escalated card was never displayed inline, so this emit is
|
|
300
|
+
// what surfaces it after resolution. In-stream the cumulative
|
|
301
|
+
// array is correct; post-MESSAGE_END the accumulator was RESET,
|
|
302
|
+
// so a cumulative (replace-mode) emit would wipe the trailing
|
|
303
|
+
// bubble down to the lone card — emit only the resolved card(s)
|
|
304
|
+
// as an append instead. Store hosts upsert batches by request id,
|
|
305
|
+
// so a replayed append stays idempotent.
|
|
306
|
+
const emitResolved = (segments: MessageSegment[]) => {
|
|
307
|
+
if (isInStreamRef.current) {
|
|
308
|
+
emitSegments(segments)
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
const delta = segments.filter(
|
|
312
|
+
(s) =>
|
|
313
|
+
(s.type === 'approval_request' && s.data.requestId === requestId) ||
|
|
314
|
+
(s.type === 'approval_batch' && s.data.approvalRequestId === requestId),
|
|
315
|
+
)
|
|
316
|
+
emitSegments(delta, { append: true })
|
|
317
|
+
}
|
|
318
|
+
|
|
289
319
|
if (escalatedData.toolCalls && escalatedData.toolCalls.length > 0) {
|
|
290
320
|
if (batchApprovalsEnabled) {
|
|
291
321
|
const segments = accumulator.addApprovalBatch(
|
|
@@ -296,7 +326,7 @@ export function useRealtimeChunkProcessor(
|
|
|
296
326
|
undefined,
|
|
297
327
|
resolvedByName,
|
|
298
328
|
)
|
|
299
|
-
|
|
329
|
+
emitResolved(segments)
|
|
300
330
|
} else {
|
|
301
331
|
let segments = accumulator.getSegments()
|
|
302
332
|
for (const call of escalatedData.toolCalls) {
|
|
@@ -309,7 +339,7 @@ export function useRealtimeChunkProcessor(
|
|
|
309
339
|
status,
|
|
310
340
|
)
|
|
311
341
|
}
|
|
312
|
-
|
|
342
|
+
emitResolved(segments)
|
|
313
343
|
}
|
|
314
344
|
} else {
|
|
315
345
|
const segments = accumulator.addApprovalRequest(
|
|
@@ -319,7 +349,7 @@ export function useRealtimeChunkProcessor(
|
|
|
319
349
|
escalatedData.approvalType,
|
|
320
350
|
status,
|
|
321
351
|
)
|
|
322
|
-
|
|
352
|
+
emitResolved(segments)
|
|
323
353
|
}
|
|
324
354
|
} else {
|
|
325
355
|
// Always keep the in-memory accumulator in sync so a following
|
|
@@ -419,6 +449,13 @@ export function useRealtimeChunkProcessor(
|
|
|
419
449
|
hasInitializedWithData.current = false
|
|
420
450
|
sawDirectMessageRef.current = false
|
|
421
451
|
directTeardownFiredRef.current = false
|
|
452
|
+
// Stream-state flags are per-dialog too. Leaking them across a reset
|
|
453
|
+
// made the next dialog's cold-start chunks take the post-stream append
|
|
454
|
+
// path (hasEverStreamed stuck true) — which consumers may silently drop
|
|
455
|
+
// when no assistant bubble exists yet — and left tool/compaction routing
|
|
456
|
+
// thinking a stream was still open (isInStream stuck true).
|
|
457
|
+
isInStreamRef.current = false
|
|
458
|
+
hasEverStreamedRef.current = false
|
|
422
459
|
}, [])
|
|
423
460
|
|
|
424
461
|
const updateApprovalStatus = useCallback(
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression guard for `displayApprovalTypes` semantics in the history
|
|
3
|
+
* processors. An OMITTED option must render EVERY approval type inline —
|
|
4
|
+
* consumers (tickets/mingo admin surfaces) that pass a wider list to their
|
|
5
|
+
* realtime processor but omit it on the history path relied on this, and a
|
|
6
|
+
* silent `['CLIENT']` default made their pending ADMIN approval cards vanish
|
|
7
|
+
* on every reload/reconnect refetch. Parity with realtime is opt-in via an
|
|
8
|
+
* explicit list.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest'
|
|
12
|
+
import { processHistoricalMessagesWithErrors } from '../process-historical-messages'
|
|
13
|
+
import type { HistoricalMessage, MessageSegment } from '../../types'
|
|
14
|
+
|
|
15
|
+
const adminApprovalMessage = (id: string): HistoricalMessage => ({
|
|
16
|
+
id,
|
|
17
|
+
createdAt: '2026-07-17T12:00:00Z',
|
|
18
|
+
owner: { type: 'ASSISTANT' },
|
|
19
|
+
messageData: [
|
|
20
|
+
{
|
|
21
|
+
type: 'APPROVAL_REQUEST',
|
|
22
|
+
approvalRequestId: `req-${id}`,
|
|
23
|
+
approvalType: 'ADMIN',
|
|
24
|
+
command: 'systemctl restart nats',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const approvalSegments = (segments: MessageSegment[] | string | undefined) =>
|
|
30
|
+
Array.isArray(segments)
|
|
31
|
+
? segments.filter((s) => s.type === 'approval_request' || s.type === 'approval_batch')
|
|
32
|
+
: []
|
|
33
|
+
|
|
34
|
+
describe('processHistoricalMessagesWithErrors — displayApprovalTypes', () => {
|
|
35
|
+
it('renders non-CLIENT approvals inline when the option is OMITTED', () => {
|
|
36
|
+
const { messages, escalatedApprovals } = processHistoricalMessagesWithErrors([
|
|
37
|
+
adminApprovalMessage('m1'),
|
|
38
|
+
])
|
|
39
|
+
const rendered = messages.flatMap((m) => approvalSegments(m.content))
|
|
40
|
+
expect(rendered).toHaveLength(1)
|
|
41
|
+
expect(escalatedApprovals.size).toBe(0)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('escalates (does not render) approval types excluded by an explicit list', () => {
|
|
45
|
+
const { messages, escalatedApprovals } = processHistoricalMessagesWithErrors(
|
|
46
|
+
[adminApprovalMessage('m1')],
|
|
47
|
+
{ displayApprovalTypes: ['CLIENT'] },
|
|
48
|
+
)
|
|
49
|
+
const rendered = messages.flatMap((m) => approvalSegments(m.content))
|
|
50
|
+
expect(rendered).toHaveLength(0)
|
|
51
|
+
expect(escalatedApprovals.has('req-m1')).toBe(true)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('renders approval types included by an explicit list', () => {
|
|
55
|
+
const { messages } = processHistoricalMessagesWithErrors(
|
|
56
|
+
[adminApprovalMessage('m1')],
|
|
57
|
+
{ displayApprovalTypes: ['CLIENT', 'ADMIN'] },
|
|
58
|
+
)
|
|
59
|
+
const rendered = messages.flatMap((m) => approvalSegments(m.content))
|
|
60
|
+
expect(rendered).toHaveLength(1)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -225,6 +225,12 @@ export class MessageSegmentAccumulator {
|
|
|
225
225
|
if (!hasCall) return seg
|
|
226
226
|
|
|
227
227
|
const prev: ApprovalBatchExecutionState | undefined = seg.data.executions?.[execId]
|
|
228
|
+
// Never downgrade a done slot back to executing (redelivered EXECUTING
|
|
229
|
+
// after its EXECUTED already landed) — matched, but unchanged.
|
|
230
|
+
if (toolData.type === 'EXECUTING_TOOL' && prev?.status === 'done') {
|
|
231
|
+
matched = true
|
|
232
|
+
return seg
|
|
233
|
+
}
|
|
228
234
|
const next: ApprovalBatchExecutionState =
|
|
229
235
|
toolData.type === 'EXECUTED_TOOL'
|
|
230
236
|
? { status: 'done', result: toolData.result, success: toolData.success }
|
|
@@ -78,7 +78,15 @@ export function processHistoricalMessages(
|
|
|
78
78
|
onReject,
|
|
79
79
|
chatTypeFilter,
|
|
80
80
|
approvalStatuses = {},
|
|
81
|
+
// An omitted option means "display every approval type" — the original
|
|
82
|
+
// history semantics. Deliberately NOT defaulted to the realtime
|
|
83
|
+
// processor's ['CLIENT']: consumers that pass a wider list to their
|
|
84
|
+
// realtime processor but omit it on the history path would silently lose
|
|
85
|
+
// pending non-CLIENT approval cards on every reload/reconnect refetch
|
|
86
|
+
// (they also ignore `escalatedApprovals`). Realtime/history parity is
|
|
87
|
+
// opt-in: pass the same explicit list to both.
|
|
81
88
|
displayApprovalTypes,
|
|
89
|
+
batchApprovalsEnabled,
|
|
82
90
|
} = options
|
|
83
91
|
|
|
84
92
|
const processedMessages: ProcessedMessage[] = []
|
|
@@ -174,7 +182,13 @@ export function processHistoricalMessages(
|
|
|
174
182
|
lastAssistantId = msg.id
|
|
175
183
|
|
|
176
184
|
messageDataArray.forEach((data) => {
|
|
177
|
-
processMessageData(
|
|
185
|
+
processMessageData(
|
|
186
|
+
data,
|
|
187
|
+
accumulator,
|
|
188
|
+
approvalStatuses,
|
|
189
|
+
{ displayApprovalTypes, batchApprovalsEnabled },
|
|
190
|
+
escalatedApprovals,
|
|
191
|
+
)
|
|
178
192
|
})
|
|
179
193
|
|
|
180
194
|
// Check if we should flush (next message is from user or last message)
|
|
@@ -473,7 +487,9 @@ export function processHistoricalMessagesWithErrors(
|
|
|
473
487
|
messages: HistoricalMessage[],
|
|
474
488
|
options: MessageProcessingOptions = {}
|
|
475
489
|
): ProcessHistoricalMessagesResult {
|
|
476
|
-
|
|
490
|
+
// displayApprovalTypes omitted = display every approval type (original
|
|
491
|
+
// history semantics — see the matching note in processHistoricalMessages).
|
|
492
|
+
const { chatTypeFilter, assistantName = 'Fae', assistantType = 'fae', assistantAvatar, onApprove, onReject, approvalStatuses = {}, displayApprovalTypes, batchApprovalsEnabled } = options
|
|
477
493
|
|
|
478
494
|
const processedMessages: ProcessedMessage[] = []
|
|
479
495
|
const accumulator = createMessageSegmentAccumulator({ onApprove, onReject })
|
|
@@ -563,7 +579,13 @@ export function processHistoricalMessagesWithErrors(
|
|
|
563
579
|
lastAssistantId = msg.id
|
|
564
580
|
|
|
565
581
|
messageDataArray.forEach((data) => {
|
|
566
|
-
processMessageData(
|
|
582
|
+
processMessageData(
|
|
583
|
+
data,
|
|
584
|
+
accumulator,
|
|
585
|
+
approvalStatuses,
|
|
586
|
+
{ displayApprovalTypes, batchApprovalsEnabled },
|
|
587
|
+
escalatedApprovals,
|
|
588
|
+
)
|
|
567
589
|
})
|
|
568
590
|
|
|
569
591
|
const nextMsg = messages[index + 1]
|
|
@@ -52,21 +52,30 @@ export interface ReleaseClientOptions {
|
|
|
52
52
|
delayMs?: number
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
// One shared connection PER URL. The previous single-slot implementation
|
|
56
|
+
// force-closed whatever was connected the moment any consumer acquired a
|
|
57
|
+
// DIFFERENT URL — even with live refs on it. With two chat surfaces on
|
|
58
|
+
// distinct endpoints (`/ws/nats` client chat vs `/ws/nats-api` dashboard)
|
|
59
|
+
// mounted at once, each acquire killed the other's socket, and the loser's
|
|
60
|
+
// retry loop self-cancelled (connection identity mismatch) leaving a dead
|
|
61
|
+
// subscription that silently received nothing.
|
|
62
|
+
const connections = new Map<string, SharedConnection>()
|
|
63
|
+
|
|
64
|
+
/** Legacy accessor from the single-slot era: returns the first live shared
|
|
65
|
+
* connection, or null. With MULTIPLE URLs connected (e.g. `/ws/nats` client
|
|
66
|
+
* chat + `/ws/nats-api` dashboard mounted together) "first" is whichever
|
|
67
|
+
* surface acquired first — an arbitrary, mount-order-dependent answer.
|
|
68
|
+
* Prefer `getSharedConnectionFor(url)`; this stays only for external
|
|
69
|
+
* registry-pinned consumers of the old single-connection API. */
|
|
57
70
|
export function getSharedConnection(): SharedConnection | null {
|
|
58
|
-
|
|
71
|
+
const first = connections.values().next()
|
|
72
|
+
return first.done ? null : first.value
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
export function acquireClient(url: string, opts?: AcquireClientOptions): SharedConnection {
|
|
62
|
-
|
|
63
|
-
if (shared) {
|
|
64
|
-
if (shared.closeTimer) clearTimeout(shared.closeTimer)
|
|
65
|
-
const old = shared
|
|
66
|
-
shared = null
|
|
67
|
-
void old.client.close().catch(() => {})
|
|
68
|
-
}
|
|
76
|
+
let conn = connections.get(url)
|
|
69
77
|
|
|
78
|
+
if (!conn) {
|
|
70
79
|
const {
|
|
71
80
|
name = 'openframe-frontend',
|
|
72
81
|
user = 'machine',
|
|
@@ -87,7 +96,7 @@ export function acquireClient(url: string, opts?: AcquireClientOptions): SharedC
|
|
|
87
96
|
maxPingOut,
|
|
88
97
|
})
|
|
89
98
|
|
|
90
|
-
|
|
99
|
+
conn = {
|
|
91
100
|
wsUrl: url,
|
|
92
101
|
client,
|
|
93
102
|
connectPromise: null,
|
|
@@ -96,39 +105,43 @@ export function acquireClient(url: string, opts?: AcquireClientOptions): SharedC
|
|
|
96
105
|
retryTimer: null,
|
|
97
106
|
retryOwner: null,
|
|
98
107
|
}
|
|
108
|
+
connections.set(url, conn)
|
|
99
109
|
}
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
clearTimeout(
|
|
104
|
-
|
|
111
|
+
conn.refCount += 1
|
|
112
|
+
if (conn.closeTimer) {
|
|
113
|
+
clearTimeout(conn.closeTimer)
|
|
114
|
+
conn.closeTimer = null
|
|
105
115
|
}
|
|
106
|
-
return
|
|
116
|
+
return conn
|
|
107
117
|
}
|
|
108
118
|
|
|
109
119
|
export function releaseClient(url: string, opts?: ReleaseClientOptions): void {
|
|
110
|
-
|
|
120
|
+
const conn = connections.get(url)
|
|
121
|
+
if (!conn) return
|
|
111
122
|
|
|
112
|
-
|
|
113
|
-
if (
|
|
123
|
+
conn.refCount = Math.max(0, conn.refCount - 1)
|
|
124
|
+
if (conn.refCount > 0) return
|
|
114
125
|
|
|
115
126
|
const delay = opts?.delayMs ?? NATS_DEFAULTS.SHARED_CLOSE_DELAY_MS
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
conn.closeTimer = setTimeout(() => {
|
|
128
|
+
conn.closeTimer = null
|
|
129
|
+
// A new acquire may have raced in during the grace period.
|
|
130
|
+
if (conn.refCount > 0) return
|
|
131
|
+
if (connections.get(url) === conn) {
|
|
132
|
+
connections.delete(url)
|
|
133
|
+
}
|
|
134
|
+
if (conn.retryTimer) {
|
|
135
|
+
clearTimeout(conn.retryTimer)
|
|
136
|
+
conn.retryTimer = null
|
|
125
137
|
}
|
|
138
|
+
void conn.client.close().catch(() => {})
|
|
126
139
|
}, delay)
|
|
127
140
|
}
|
|
128
141
|
|
|
129
142
|
export function getSharedConnectionFor(url: string | null | undefined): SharedConnection | null {
|
|
130
143
|
if (!url) return null
|
|
131
|
-
return
|
|
144
|
+
return connections.get(url) ?? null
|
|
132
145
|
}
|
|
133
146
|
|
|
134
147
|
// ---------------------------------------------------------------------------
|