@flamingo-stack/openframe-frontend-core 0.0.195 → 0.0.197

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 (71) hide show
  1. package/dist/chunk-4CWSZPXH.cjs.map +1 -1
  2. package/dist/{chunk-PJ5KFD2V.js → chunk-4ML3NA2L.js} +75 -1
  3. package/dist/{chunk-PJ5KFD2V.js.map → chunk-4ML3NA2L.js.map} +1 -1
  4. package/dist/{chunk-U6AJSRJP.js → chunk-GIQZAYY5.js} +731 -365
  5. package/dist/chunk-GIQZAYY5.js.map +1 -0
  6. package/dist/{chunk-IQM3G2I6.cjs → chunk-IMDXOVYD.cjs} +777 -411
  7. package/dist/chunk-IMDXOVYD.cjs.map +1 -0
  8. package/dist/{chunk-CVMSC7M4.cjs → chunk-OII2IERE.cjs} +77 -3
  9. package/dist/chunk-OII2IERE.cjs.map +1 -0
  10. package/dist/chunk-UC43NICZ.cjs.map +1 -1
  11. package/dist/chunk-V2FNIPZJ.cjs.map +1 -1
  12. package/dist/chunk-VJTFBYVG.cjs.map +1 -1
  13. package/dist/chunk-VRHGVLSL.cjs.map +1 -1
  14. package/dist/chunk-WZW7C7TF.cjs.map +1 -1
  15. package/dist/chunk-XQFFGR6U.cjs.map +1 -1
  16. package/dist/components/chart.d.ts +7 -14
  17. package/dist/components/chart.d.ts.map +1 -1
  18. package/dist/components/chat/hooks/index.d.ts +1 -0
  19. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  20. package/dist/components/chat/hooks/use-jetstream-dialog-subscription.d.ts +15 -0
  21. package/dist/components/chat/hooks/use-jetstream-dialog-subscription.d.ts.map +1 -0
  22. package/dist/components/chat/types/api.types.d.ts +43 -0
  23. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  24. package/dist/components/chat/types/network.types.d.ts +4 -0
  25. package/dist/components/chat/types/network.types.d.ts.map +1 -1
  26. package/dist/components/features/board/ticket-card-skeleton.d.ts.map +1 -1
  27. package/dist/components/features/board/ticket-card.d.ts.map +1 -1
  28. package/dist/components/features/index.cjs +3 -3
  29. package/dist/components/features/index.cjs.map +1 -1
  30. package/dist/components/features/index.js +2 -2
  31. package/dist/components/icons/index.cjs.map +1 -1
  32. package/dist/components/icons-v2-generated/index.cjs.map +1 -1
  33. package/dist/components/index.cjs +5 -3
  34. package/dist/components/index.cjs.map +1 -1
  35. package/dist/components/index.js +4 -2
  36. package/dist/components/navigation/index.cjs +3 -3
  37. package/dist/components/navigation/index.cjs.map +1 -1
  38. package/dist/components/navigation/index.js +2 -2
  39. package/dist/components/resizable.d.ts +1 -1
  40. package/dist/components/toast/index.cjs.map +1 -1
  41. package/dist/components/ui/file-manager/index.cjs.map +1 -1
  42. package/dist/components/ui/index.cjs +5 -3
  43. package/dist/components/ui/index.cjs.map +1 -1
  44. package/dist/components/ui/index.js +4 -2
  45. package/dist/hooks/index.cjs +2 -2
  46. package/dist/hooks/index.cjs.map +1 -1
  47. package/dist/hooks/index.js +1 -1
  48. package/dist/index.cjs +5 -3
  49. package/dist/index.cjs.map +1 -1
  50. package/dist/index.js +4 -2
  51. package/dist/nats/index.cjs +73 -0
  52. package/dist/nats/index.cjs.map +1 -1
  53. package/dist/nats/index.js +73 -0
  54. package/dist/nats/index.js.map +1 -1
  55. package/dist/nats/nats.d.ts +22 -1
  56. package/dist/nats/nats.d.ts.map +1 -1
  57. package/dist/tailwind.config.cjs +7 -7
  58. package/dist/tailwind.config.cjs.map +1 -1
  59. package/dist/tailwind.config.js +7 -7
  60. package/dist/tailwind.config.js.map +1 -1
  61. package/package.json +1 -1
  62. package/src/components/chat/hooks/index.ts +1 -0
  63. package/src/components/chat/hooks/use-jetstream-dialog-subscription.ts +474 -0
  64. package/src/components/chat/types/api.types.ts +45 -0
  65. package/src/components/chat/types/network.types.ts +4 -0
  66. package/src/components/features/board/ticket-card-skeleton.tsx +16 -4
  67. package/src/components/features/board/ticket-card.tsx +6 -4
  68. package/src/nats/nats.ts +117 -0
  69. package/dist/chunk-CVMSC7M4.cjs.map +0 -1
  70. package/dist/chunk-IQM3G2I6.cjs.map +0 -1
  71. package/dist/chunk-U6AJSRJP.js.map +0 -1
@@ -0,0 +1,474 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useRef, useState } from 'react'
4
+ import {
5
+ createNatsClient,
6
+ type JetStreamSubscriptionHandle,
7
+ type NatsClient,
8
+ } from '../../../nats'
9
+ import {
10
+ type UseJetStreamDialogSubscriptionOptions,
11
+ type UseJetStreamDialogSubscriptionReturn,
12
+ NETWORK_CONFIG,
13
+ } from '../types'
14
+
15
+ type SharedConnection = {
16
+ wsUrl: string
17
+ client: NatsClient
18
+ connectPromise: Promise<void> | null
19
+ refCount: number
20
+ closeTimer: ReturnType<typeof setTimeout> | null
21
+ retryTimer: ReturnType<typeof setTimeout> | null
22
+ }
23
+
24
+ let shared: SharedConnection | null = null
25
+
26
+ const DEFAULT_INACTIVE_THRESHOLD_MS = 5 * 60_000
27
+ const DEFAULT_STREAM_NAME = 'CHAT_CHUNKS'
28
+
29
+ /**
30
+ * Subscribe to a chat dialog stream via a JetStream **ephemeral OrderedConsumer**.
31
+ *
32
+ * - Subject: `chat.{dialogId}.{topic}`
33
+ * - When `optStartSeq` is a number, the consumer resumes at `optStartSeq + 1`
34
+ * (`DeliverPolicy.ByStartSequence`). When null/undefined, it live-tails
35
+ * (`DeliverPolicy.New`).
36
+ * - On reconnect, the consumer is recreated starting from the highest stream
37
+ * sequence we've already observed + 1, so no chunk is replayed or skipped.
38
+ * - Consumer is ephemeral with `AckPolicy.None` and a 5-minute inactivity
39
+ * threshold (overridable via `inactiveThresholdMs`).
40
+ */
41
+ export function useJetStreamDialogSubscription({
42
+ enabled,
43
+ dialogId,
44
+ streamName = DEFAULT_STREAM_NAME,
45
+ topic,
46
+ optStartSeq,
47
+ onEvent,
48
+ onConnect,
49
+ onDisconnect,
50
+ onSubscribed,
51
+ onBeforeReconnect,
52
+ getNatsWsUrl,
53
+ clientConfig = {},
54
+ reconnectionBackoff,
55
+ inactiveThresholdMs,
56
+ }: UseJetStreamDialogSubscriptionOptions): UseJetStreamDialogSubscriptionReturn {
57
+ const [isConnected, setIsConnected] = useState(false)
58
+ const [isSubscribed, setIsSubscribed] = useState(false)
59
+ const [reconnectionCount, setReconnectionCount] = useState(0)
60
+ const [currentStreamSeq, setCurrentStreamSeq] = useState<number | null>(null)
61
+
62
+ const clientRef = useRef<NatsClient | null>(null)
63
+ const subscriptionRef = useRef<JetStreamSubscriptionHandle | null>(null)
64
+ const highestStreamSeqRef = useRef<number | null>(null)
65
+
66
+ const onEventRef = useRef(onEvent)
67
+ useEffect(() => {
68
+ onEventRef.current = onEvent
69
+ }, [onEvent])
70
+
71
+ const onConnectRef = useRef(onConnect)
72
+ useEffect(() => {
73
+ onConnectRef.current = onConnect
74
+ }, [onConnect])
75
+
76
+ const onDisconnectRef = useRef(onDisconnect)
77
+ useEffect(() => {
78
+ onDisconnectRef.current = onDisconnect
79
+ }, [onDisconnect])
80
+
81
+ const onSubscribedRef = useRef(onSubscribed)
82
+ useEffect(() => {
83
+ onSubscribedRef.current = onSubscribed
84
+ }, [onSubscribed])
85
+
86
+ const onBeforeReconnectRef = useRef(onBeforeReconnect)
87
+ useEffect(() => {
88
+ onBeforeReconnectRef.current = onBeforeReconnect
89
+ }, [onBeforeReconnect])
90
+
91
+ const getNatsWsUrlRef = useRef(getNatsWsUrl)
92
+ useEffect(() => {
93
+ getNatsWsUrlRef.current = getNatsWsUrl
94
+ }, [getNatsWsUrl])
95
+
96
+ const reconnectionBackoffRef = useRef(reconnectionBackoff)
97
+ useEffect(() => {
98
+ reconnectionBackoffRef.current = reconnectionBackoff
99
+ }, [reconnectionBackoff])
100
+
101
+ const optStartSeqRef = useRef<number | null | undefined>(optStartSeq)
102
+ useEffect(() => {
103
+ optStartSeqRef.current = optStartSeq
104
+ }, [optStartSeq])
105
+
106
+ const inactiveThresholdRef = useRef<number | undefined>(inactiveThresholdMs)
107
+ useEffect(() => {
108
+ inactiveThresholdRef.current = inactiveThresholdMs
109
+ }, [inactiveThresholdMs])
110
+
111
+ const hadConnectionBeforeRef = useRef(false)
112
+
113
+ const acquireClient = useCallback(
114
+ (url: string): SharedConnection => {
115
+ if (shared?.wsUrl !== url) {
116
+ if (shared) {
117
+ if (shared.closeTimer) clearTimeout(shared.closeTimer)
118
+ const old = shared
119
+ shared = null
120
+ void old.client.close().catch(() => {})
121
+ }
122
+
123
+ const { name = 'openframe-frontend-jetstream', user = 'machine', pass = '' } = clientConfig
124
+
125
+ const client = createNatsClient({
126
+ servers: url,
127
+ name,
128
+ user,
129
+ pass,
130
+ connectTimeoutMs: NETWORK_CONFIG.CONNECT_TIMEOUT_MS,
131
+ reconnect: false,
132
+ pingIntervalMs: NETWORK_CONFIG.PING_INTERVAL_MS,
133
+ maxPingOut: NETWORK_CONFIG.MAX_PING_OUT,
134
+ })
135
+ shared = {
136
+ wsUrl: url,
137
+ client,
138
+ connectPromise: null,
139
+ refCount: 0,
140
+ closeTimer: null,
141
+ retryTimer: null,
142
+ }
143
+ }
144
+
145
+ shared.refCount += 1
146
+ if (shared.closeTimer) {
147
+ clearTimeout(shared.closeTimer)
148
+ shared.closeTimer = null
149
+ }
150
+ return shared
151
+ },
152
+ [clientConfig],
153
+ )
154
+
155
+ const releaseClient = useCallback((url: string) => {
156
+ if (!shared || shared.wsUrl !== url) return
157
+
158
+ shared.refCount = Math.max(0, shared.refCount - 1)
159
+ if (shared.refCount > 0) return
160
+
161
+ shared.closeTimer = setTimeout(() => {
162
+ const s = shared
163
+ shared = null
164
+ if (s) {
165
+ if (s.retryTimer) {
166
+ clearTimeout(s.retryTimer)
167
+ s.retryTimer = null
168
+ }
169
+ void s.client.close().catch(() => {})
170
+ }
171
+ }, NETWORK_CONFIG.SHARED_CLOSE_DELAY_MS)
172
+ }, [])
173
+
174
+ const currentWsUrlRef = useRef<string>('')
175
+
176
+ // Connection lifecycle: acquire/release the shared client based on enabled + URL.
177
+ useEffect(() => {
178
+ const wsUrl = getNatsWsUrl()
179
+ if (!enabled || !wsUrl) {
180
+ if (currentWsUrlRef.current && clientRef.current) {
181
+ releaseClient(currentWsUrlRef.current)
182
+ clientRef.current = null
183
+ currentWsUrlRef.current = ''
184
+ setIsConnected(false)
185
+ }
186
+ return
187
+ }
188
+
189
+ if (
190
+ wsUrl === currentWsUrlRef.current &&
191
+ clientRef.current &&
192
+ clientRef.current.isConnected()
193
+ ) {
194
+ return
195
+ }
196
+
197
+ if (
198
+ currentWsUrlRef.current &&
199
+ currentWsUrlRef.current !== wsUrl &&
200
+ clientRef.current
201
+ ) {
202
+ releaseClient(currentWsUrlRef.current)
203
+ clientRef.current = null
204
+ setIsConnected(false)
205
+ }
206
+
207
+ currentWsUrlRef.current = wsUrl
208
+ const sharedConn = acquireClient(wsUrl)
209
+ const client = sharedConn.client
210
+
211
+ clientRef.current = client
212
+ setIsConnected(false)
213
+
214
+ let closed = false
215
+ let retryAttempt = 0
216
+
217
+ function scheduleRetry() {
218
+ if (closed) return
219
+ if (shared !== sharedConn) return
220
+
221
+ if (sharedConn.retryTimer) {
222
+ clearTimeout(sharedConn.retryTimer)
223
+ sharedConn.retryTimer = null
224
+ }
225
+
226
+ const cfg = reconnectionBackoffRef.current ?? {}
227
+ const fastRetries = cfg.fastRetries ?? 0
228
+ const fastDelay = cfg.fastRetryDelayMs ?? NETWORK_CONFIG.RETRY_INITIAL_DELAY_MS
229
+ const baseDelay = cfg.initialDelayMs ?? NETWORK_CONFIG.RETRY_INITIAL_DELAY_MS
230
+ const maxDelay = cfg.maxDelayMs ?? NETWORK_CONFIG.RETRY_MAX_DELAY_MS
231
+ const multiplier = cfg.multiplier ?? NETWORK_CONFIG.RETRY_BACKOFF_MULTIPLIER
232
+
233
+ const delay =
234
+ retryAttempt < fastRetries
235
+ ? fastDelay
236
+ : Math.min(baseDelay * multiplier ** (retryAttempt - fastRetries), maxDelay)
237
+ const jitteredDelay = delay * (0.5 + Math.random() * 0.5)
238
+ retryAttempt++
239
+
240
+ sharedConn.retryTimer = setTimeout(async () => {
241
+ sharedConn.retryTimer = null
242
+ if (closed) return
243
+ if (shared !== sharedConn) return
244
+
245
+ try {
246
+ await onBeforeReconnectRef.current?.()
247
+ } catch {
248
+ // Token refresh failed; still try to reconnect.
249
+ }
250
+
251
+ if (closed) return
252
+ if (shared !== sharedConn) return
253
+
254
+ const freshUrl = getNatsWsUrlRef.current()
255
+ if (freshUrl !== wsUrl) return
256
+
257
+ try {
258
+ sharedConn.connectPromise = null
259
+ sharedConn.connectPromise = client.connect()
260
+ await sharedConn.connectPromise
261
+ if (!closed && shared === sharedConn) {
262
+ retryAttempt = 0
263
+ setIsConnected(true)
264
+ }
265
+ } catch {
266
+ sharedConn.connectPromise = null
267
+ if (!closed && shared === sharedConn) {
268
+ scheduleRetry()
269
+ }
270
+ }
271
+ }, jitteredDelay)
272
+ }
273
+
274
+ const unsubscribeStatus = client.onStatus((event) => {
275
+ const connected = event.status === 'connected'
276
+ // `error` is a protocol-level signal (e.g. -ERR Permissions Violation when
277
+ // CONSUMER.CREATE is denied) that does NOT close the WebSocket. Treating
278
+ // it as a disconnect causes scheduleRetry() to fire on every -ERR, which
279
+ // re-runs onBeforeReconnect (auth refresh / `/api/me`) on a loop. Real
280
+ // transport loss arrives separately as `disconnected` or `closed`.
281
+ const disconnected = event.status === 'closed' || event.status === 'disconnected'
282
+ if (connected) {
283
+ setIsConnected(true)
284
+ if (hadConnectionBeforeRef.current) {
285
+ setReconnectionCount((c) => c + 1)
286
+ }
287
+ hadConnectionBeforeRef.current = true
288
+ retryAttempt = 0
289
+ onConnectRef.current?.()
290
+ }
291
+ if (event.status === 'error') {
292
+ // Subscription-level failures (e.g. consumer.get rejected by JetStream
293
+ // ACLs) already surface to the subscribe effect via the rejected
294
+ // promise; log here for diagnostics and let the existing WS connection
295
+ // keep running.
296
+ console.warn('[JetStream] NATS protocol error:', event.data)
297
+ return
298
+ }
299
+ if (disconnected) {
300
+ setIsConnected(false)
301
+ setIsSubscribed(false)
302
+
303
+ if (subscriptionRef.current) {
304
+ try {
305
+ subscriptionRef.current.unsubscribe()
306
+ } catch {
307
+ // ignore
308
+ }
309
+ subscriptionRef.current = null
310
+ }
311
+
312
+ onDisconnectRef.current?.()
313
+ scheduleRetry()
314
+ }
315
+ })
316
+
317
+ ;(async () => {
318
+ try {
319
+ sharedConn.connectPromise ||= client.connect()
320
+ await sharedConn.connectPromise
321
+ if (!closed) {
322
+ setIsConnected(true)
323
+ hadConnectionBeforeRef.current = true
324
+ }
325
+ } catch {
326
+ sharedConn.connectPromise = null
327
+ if (!closed) {
328
+ setIsConnected(false)
329
+ onDisconnectRef.current?.()
330
+ scheduleRetry()
331
+ }
332
+ }
333
+ })()
334
+
335
+ return () => {
336
+ closed = true
337
+ setIsConnected(false)
338
+ setIsSubscribed(false)
339
+ unsubscribeStatus()
340
+
341
+ if (sharedConn.retryTimer) {
342
+ clearTimeout(sharedConn.retryTimer)
343
+ sharedConn.retryTimer = null
344
+ }
345
+
346
+ if (subscriptionRef.current) {
347
+ try {
348
+ subscriptionRef.current.unsubscribe()
349
+ } catch {
350
+ // ignore
351
+ }
352
+ subscriptionRef.current = null
353
+ }
354
+
355
+ if (clientRef.current && currentWsUrlRef.current) {
356
+ releaseClient(currentWsUrlRef.current)
357
+ clientRef.current = null
358
+ currentWsUrlRef.current = ''
359
+ }
360
+ }
361
+ }, [enabled, getNatsWsUrl, acquireClient, releaseClient])
362
+
363
+ // Subscription lifecycle: (re)create the ephemeral JetStream consumer whenever
364
+ // we transition into a connected state for a dialog, and whenever the dialog
365
+ // changes. On reconnect we resume from highestStreamSeq + 1.
366
+ useEffect(() => {
367
+ if (!enabled || !dialogId || !isConnected) {
368
+ if (subscriptionRef.current) {
369
+ try {
370
+ subscriptionRef.current.unsubscribe()
371
+ } catch {
372
+ // ignore
373
+ }
374
+ subscriptionRef.current = null
375
+ }
376
+ setIsSubscribed(false)
377
+ return
378
+ }
379
+
380
+ const client = clientRef.current
381
+ if (!client) return
382
+
383
+ const abortController = new AbortController()
384
+ const decoder = new TextDecoder()
385
+ const filterSubject = `chat.${dialogId}.${topic}`
386
+
387
+ const resumeSeq = highestStreamSeqRef.current
388
+ const initialOptStart = optStartSeqRef.current
389
+ const startSeq =
390
+ resumeSeq != null ? resumeSeq + 1 : initialOptStart != null ? initialOptStart + 1 : undefined
391
+
392
+ let cancelled = false
393
+
394
+ void (async () => {
395
+ try {
396
+ const handle = await client.subscribeJetStreamOrdered(
397
+ (msg) => {
398
+ if (cancelled) return
399
+ const streamSeq = msg.info.streamSequence
400
+ if (typeof streamSeq === 'number') {
401
+ if (
402
+ highestStreamSeqRef.current == null ||
403
+ streamSeq > highestStreamSeqRef.current
404
+ ) {
405
+ highestStreamSeqRef.current = streamSeq
406
+ setCurrentStreamSeq(streamSeq)
407
+ }
408
+ }
409
+ const cb = onEventRef.current
410
+ if (!cb) return
411
+ try {
412
+ const parsed = JSON.parse(decoder.decode(msg.data)) as Record<string, unknown>
413
+ if (typeof streamSeq === 'number') {
414
+ ;(parsed as { streamSeq?: number }).streamSeq = streamSeq
415
+ }
416
+ cb(parsed, topic)
417
+ } catch {
418
+ // Ignore malformed payloads.
419
+ }
420
+ },
421
+ {
422
+ streamName,
423
+ filterSubject,
424
+ deliverPolicy: startSeq != null ? 'byStartSequence' : 'new',
425
+ optStartSeq: startSeq,
426
+ inactiveThresholdMs: inactiveThresholdRef.current ?? DEFAULT_INACTIVE_THRESHOLD_MS,
427
+ signal: abortController.signal,
428
+ },
429
+ )
430
+
431
+ if (cancelled) {
432
+ try {
433
+ handle.unsubscribe()
434
+ } catch {
435
+ // ignore
436
+ }
437
+ return
438
+ }
439
+
440
+ subscriptionRef.current = handle
441
+ setIsSubscribed(true)
442
+ onSubscribedRef.current?.()
443
+ } catch {
444
+ if (!cancelled) {
445
+ setIsSubscribed(false)
446
+ }
447
+ }
448
+ })()
449
+
450
+ return () => {
451
+ cancelled = true
452
+ abortController.abort()
453
+ if (subscriptionRef.current) {
454
+ try {
455
+ subscriptionRef.current.unsubscribe()
456
+ } catch {
457
+ // ignore
458
+ }
459
+ subscriptionRef.current = null
460
+ }
461
+ setIsSubscribed(false)
462
+ }
463
+ }, [enabled, dialogId, isConnected, streamName, topic, reconnectionCount])
464
+
465
+ // Reset the highest-seen sequence whenever the dialog changes so a new dialog
466
+ // starts from optStartSeq (or DeliverPolicy.New) rather than the previous
467
+ // dialog's offset.
468
+ useEffect(() => {
469
+ highestStreamSeqRef.current = null
470
+ setCurrentStreamSeq(null)
471
+ }, [dialogId])
472
+
473
+ return { isConnected, isSubscribed, reconnectionCount, currentStreamSeq }
474
+ }
@@ -90,6 +90,51 @@ export interface UseNatsDialogSubscriptionReturn {
90
90
  reconnectionCount: number
91
91
  }
92
92
 
93
+ export interface UseJetStreamDialogSubscriptionOptions {
94
+ enabled: boolean
95
+ dialogId: string | null
96
+ /** JetStream stream name. Default: 'CHAT_CHUNKS'. */
97
+ streamName?: string
98
+ /** Single topic to subscribe to. */
99
+ topic: NatsMessageType
100
+ /**
101
+ * Resume from this JetStream sequence + 1. When null/undefined, the consumer starts with
102
+ * DeliverPolicy.New (live tail only).
103
+ */
104
+ optStartSeq?: number | null
105
+ onEvent?: (payload: unknown, messageType: NatsMessageType) => void
106
+ onConnect?: () => void
107
+ onDisconnect?: () => void
108
+ onSubscribed?: () => void
109
+ /** Called on disconnect, before reconnect attempt. Use to refresh auth. */
110
+ onBeforeReconnect?: () => Promise<void> | void
111
+ /** Build the NATS WebSocket URL (or null when not yet available). */
112
+ getNatsWsUrl: () => string | null
113
+ clientConfig?: {
114
+ name?: string
115
+ user?: string
116
+ pass?: string
117
+ }
118
+ reconnectionBackoff?: {
119
+ fastRetries?: number
120
+ fastRetryDelayMs?: number
121
+ initialDelayMs?: number
122
+ maxDelayMs?: number
123
+ multiplier?: number
124
+ }
125
+ /** Consumer inactivity threshold in ms before NATS auto-cleans it. Default: 5 minutes. */
126
+ inactiveThresholdMs?: number
127
+ }
128
+
129
+ export interface UseJetStreamDialogSubscriptionReturn {
130
+ isConnected: boolean
131
+ isSubscribed: boolean
132
+ /** Incremented each time the underlying NATS connection reconnects. Starts at 0. */
133
+ reconnectionCount: number
134
+ /** Highest JetStream stream sequence observed so far (null before first delivery). */
135
+ currentStreamSeq: number | null
136
+ }
137
+
93
138
  export interface SegmentsUpdateMetadata {
94
139
  /** Segments should be appended to the last assistant message */
95
140
  append?: boolean
@@ -34,6 +34,10 @@ export type NetworkConfig = typeof NETWORK_CONFIG
34
34
 
35
35
  export interface ChunkData {
36
36
  sequenceId?: number
37
+ /** JetStream stream sequence, populated from JsMsg.info.streamSequence when delivered via JetStream. */
38
+ streamSeq?: number
39
+ /** Authoritative dialog streaming state at the time this chunk was emitted; consumers should mirror it into their dialog cache. */
40
+ streamState?: 'IDLE' | 'STREAMING'
37
41
  type: string
38
42
  text?: string
39
43
  integratedToolType?: string
@@ -17,11 +17,23 @@ export const TicketCardSkeleton = React.forwardRef<HTMLDivElement, TicketCardSke
17
17
  {...props}
18
18
  >
19
19
  <div className="flex items-start gap-[var(--spacing-system-sf)]">
20
- <div className="flex flex-1 flex-col gap-[var(--spacing-system-xsf)]">
21
- <Skeleton className="h-5 w-3/4" />
22
- <Skeleton className="h-4 w-1/2" />
20
+ <div className="flex min-w-0 flex-1 flex-col gap-[var(--spacing-system-xxs)]">
21
+ <div className="text-h3 flex items-center">
22
+ <Skeleton className="h-4 w-3/4" />
23
+ </div>
24
+ <div className="text-h6 flex items-center gap-[var(--spacing-system-xxs)]">
25
+ <Skeleton className="size-4 shrink-0 rounded-sm" />
26
+ <Skeleton className="h-3 w-1/2" />
27
+ </div>
23
28
  </div>
24
- <Skeleton className="size-8 rounded-full" />
29
+ <div className="flex shrink-0 items-center gap-[var(--spacing-system-xsf)]">
30
+ <Skeleton className="size-4 rounded-sm" />
31
+ <Skeleton className="size-8 rounded-full" />
32
+ </div>
33
+ </div>
34
+ <div className="flex h-8 items-center gap-[var(--spacing-system-xxs)]">
35
+ <Skeleton className="h-8 w-16 rounded-md" />
36
+ <Skeleton className="h-8 w-12 rounded-md" />
25
37
  </div>
26
38
  </div>
27
39
  ),
@@ -99,7 +99,7 @@ export function TicketCard({
99
99
  const body = (
100
100
  <>
101
101
  <div className="flex items-start gap-[var(--spacing-system-sf)]">
102
- <div className="flex min-w-0 flex-1 flex-col gap-[var(--spacing-system-xxs)]">
102
+ <div className="flex min-w-0 flex-1 flex-col gap-[var(--spacing-system-zero)]">
103
103
  <p className="text-h3 truncate text-ods-text-primary">{ticket.title}</p>
104
104
  {showDeviceRow && (
105
105
  <div className="flex min-w-0 items-center gap-[var(--spacing-system-xxs)] text-h6 text-ods-text-secondary">
@@ -129,10 +129,12 @@ export function TicketCard({
129
129
  ...(isOverlay ? {} : sortable.listeners),
130
130
  }
131
131
 
132
+ const innerWrapperClass = 'relative z-10 flex flex-col gap-[var(--spacing-system-sf)]'
133
+
132
134
  if (isOverlay) {
133
135
  return (
134
136
  <div {...outerProps}>
135
- <div className="relative z-10">{body}</div>
137
+ <div className={innerWrapperClass}>{body}</div>
136
138
  </div>
137
139
  )
138
140
  }
@@ -148,7 +150,7 @@ export function TicketCard({
148
150
  aria-label={ticket.title}
149
151
  className="absolute inset-0 z-0 rounded-md focus-visible:outline-none"
150
152
  />
151
- <div className="pointer-events-none relative z-10">{body}</div>
153
+ <div className={cn('pointer-events-none', innerWrapperClass)}>{body}</div>
152
154
  </div>
153
155
  )
154
156
  }
@@ -161,7 +163,7 @@ export function TicketCard({
161
163
  aria-label={ticket.title}
162
164
  className="absolute inset-0 z-0 cursor-pointer rounded-md focus-visible:outline-none"
163
165
  />
164
- <div className="pointer-events-none relative z-10">{body}</div>
166
+ <div className={cn('pointer-events-none', innerWrapperClass)}>{body}</div>
165
167
  </div>
166
168
  )
167
169
  }