@flamingo-stack/openframe-frontend-core 0.0.589 → 0.0.590-2020.5606.1

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 (101) hide show
  1. package/dist/{chunk-EGUU3QCU.cjs → chunk-2564HPMD.cjs} +3 -3
  2. package/dist/{chunk-EGUU3QCU.cjs.map → chunk-2564HPMD.cjs.map} +1 -1
  3. package/dist/{chunk-JDINUWND.cjs → chunk-2MY6BT6P.cjs} +69 -69
  4. package/dist/{chunk-JDINUWND.cjs.map → chunk-2MY6BT6P.cjs.map} +1 -1
  5. package/dist/{chunk-G45RGWHF.cjs → chunk-353GRCTE.cjs} +37 -10
  6. package/dist/chunk-353GRCTE.cjs.map +1 -0
  7. package/dist/{chunk-5CHW7AMP.cjs → chunk-3BQESV5G.cjs} +10 -10
  8. package/dist/{chunk-5CHW7AMP.cjs.map → chunk-3BQESV5G.cjs.map} +1 -1
  9. package/dist/{chunk-KK3XU6YJ.cjs → chunk-3WZQ5RXS.cjs} +4 -4
  10. package/dist/{chunk-KK3XU6YJ.cjs.map → chunk-3WZQ5RXS.cjs.map} +1 -1
  11. package/dist/{chunk-A6X7EXP6.cjs → chunk-5KFIUNKN.cjs} +10 -10
  12. package/dist/{chunk-A6X7EXP6.cjs.map → chunk-5KFIUNKN.cjs.map} +1 -1
  13. package/dist/{chunk-CJOWLRUU.js → chunk-6BPIDRBI.js} +2 -2
  14. package/dist/{chunk-FVRRYJ6P.cjs → chunk-7H4RUNIL.cjs} +22 -22
  15. package/dist/{chunk-FVRRYJ6P.cjs.map → chunk-7H4RUNIL.cjs.map} +1 -1
  16. package/dist/{chunk-CN36E4PK.js → chunk-7JPKCUF7.js} +2 -2
  17. package/dist/{chunk-IYDHV2CS.cjs → chunk-7O7CAZYG.cjs} +43 -43
  18. package/dist/{chunk-IYDHV2CS.cjs.map → chunk-7O7CAZYG.cjs.map} +1 -1
  19. package/dist/{chunk-LGBNHWDN.js → chunk-ACUXSU6J.js} +3 -3
  20. package/dist/{chunk-KVNGYFFP.js → chunk-AV4UXMOH.js} +3 -3
  21. package/dist/{chunk-4ZKAMPBX.js → chunk-BKOCXNAP.js} +2 -2
  22. package/dist/{chunk-MENQLD57.cjs → chunk-CQHFRJSG.cjs} +5 -5
  23. package/dist/{chunk-MENQLD57.cjs.map → chunk-CQHFRJSG.cjs.map} +1 -1
  24. package/dist/{chunk-3OCYNMZ5.js → chunk-CT6I3HOP.js} +3 -3
  25. package/dist/{chunk-C6RJRSIM.cjs → chunk-DAUDWRE4.cjs} +3 -3
  26. package/dist/{chunk-C6RJRSIM.cjs.map → chunk-DAUDWRE4.cjs.map} +1 -1
  27. package/dist/{chunk-NXZC6UTN.js → chunk-DFUO7J3W.js} +2 -2
  28. package/dist/{chunk-TOYHZHTK.js → chunk-E5NSDLYJ.js} +2 -2
  29. package/dist/{chunk-77Y7R243.js → chunk-GAXC43J4.js} +5 -5
  30. package/dist/{chunk-EGYX55HT.cjs → chunk-I3HBFKKM.cjs} +159 -159
  31. package/dist/{chunk-EGYX55HT.cjs.map → chunk-I3HBFKKM.cjs.map} +1 -1
  32. package/dist/{chunk-ADANP3ZA.cjs → chunk-JIIA5BIJ.cjs} +33 -33
  33. package/dist/{chunk-ADANP3ZA.cjs.map → chunk-JIIA5BIJ.cjs.map} +1 -1
  34. package/dist/{chunk-PAUJQV3C.cjs → chunk-KIMRNY7A.cjs} +15 -15
  35. package/dist/{chunk-PAUJQV3C.cjs.map → chunk-KIMRNY7A.cjs.map} +1 -1
  36. package/dist/{chunk-FQBR6SCA.js → chunk-KLNHD4TC.js} +3 -3
  37. package/dist/{chunk-CNKVEVUN.js → chunk-LDNRL62P.js} +5 -5
  38. package/dist/{chunk-I5CJJFEJ.js → chunk-LKMRJ2CP.js} +3 -3
  39. package/dist/{chunk-KWTV3KGY.cjs → chunk-QEV5HPDQ.cjs} +9 -9
  40. package/dist/{chunk-KWTV3KGY.cjs.map → chunk-QEV5HPDQ.cjs.map} +1 -1
  41. package/dist/{chunk-WUCYI3BC.cjs → chunk-RLABO5PY.cjs} +24 -24
  42. package/dist/{chunk-WUCYI3BC.cjs.map → chunk-RLABO5PY.cjs.map} +1 -1
  43. package/dist/{chunk-5FNWOF46.js → chunk-RLC5IISG.js} +6 -6
  44. package/dist/{chunk-GKPY7VLK.js → chunk-RM3PTDSI.js} +4 -4
  45. package/dist/{chunk-NIDPX52C.js → chunk-T3WAHAMH.js} +5 -5
  46. package/dist/{chunk-D7YISHXC.js → chunk-V7RB3EUW.js} +37 -10
  47. package/dist/{chunk-D7YISHXC.js.map → chunk-V7RB3EUW.js.map} +1 -1
  48. package/dist/{chunk-BONXSRHZ.cjs → chunk-WIAEFA2K.cjs} +21 -21
  49. package/dist/{chunk-BONXSRHZ.cjs.map → chunk-WIAEFA2K.cjs.map} +1 -1
  50. package/dist/components/case-studies/index.cjs +9 -9
  51. package/dist/components/case-studies/index.js +3 -3
  52. package/dist/components/chat/index.cjs +2 -2
  53. package/dist/components/chat/index.js +1 -1
  54. package/dist/components/chat/stream/message-mutations.d.ts.map +1 -1
  55. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  56. package/dist/components/contact/index.cjs +4 -4
  57. package/dist/components/contact/index.js +3 -3
  58. package/dist/components/docs/index.cjs +7 -7
  59. package/dist/components/docs/index.js +6 -6
  60. package/dist/components/embeds/index.cjs +4 -4
  61. package/dist/components/embeds/index.js +3 -3
  62. package/dist/components/faq/index.cjs +4 -4
  63. package/dist/components/faq/index.js +3 -3
  64. package/dist/components/features/index.cjs +3 -3
  65. package/dist/components/features/index.js +2 -2
  66. package/dist/components/help-center-pages/index.cjs +23 -23
  67. package/dist/components/help-center-pages/index.js +14 -14
  68. package/dist/components/index.cjs +36 -36
  69. package/dist/components/index.js +13 -13
  70. package/dist/components/meeting-scheduler/index.cjs +21 -21
  71. package/dist/components/meeting-scheduler/index.js +2 -2
  72. package/dist/components/navigation/index.cjs +6 -6
  73. package/dist/components/navigation/index.js +5 -5
  74. package/dist/components/onboarding-guides/index.cjs +6 -6
  75. package/dist/components/onboarding-guides/index.js +5 -5
  76. package/dist/components/tickets/index.cjs +7 -7
  77. package/dist/components/tickets/index.js +6 -6
  78. package/dist/components/ui/index.cjs +3 -3
  79. package/dist/components/ui/index.js +2 -2
  80. package/dist/index.cjs +7 -7
  81. package/dist/index.js +6 -6
  82. package/package.json +1 -1
  83. package/src/components/chat/__tests__/ticket-event.test.ts +107 -0
  84. package/src/components/chat/stream/message-mutations.ts +47 -4
  85. package/src/components/chat/utils/process-historical-messages.ts +18 -6
  86. package/dist/chunk-G45RGWHF.cjs.map +0 -1
  87. /package/dist/{chunk-CJOWLRUU.js.map → chunk-6BPIDRBI.js.map} +0 -0
  88. /package/dist/{chunk-CN36E4PK.js.map → chunk-7JPKCUF7.js.map} +0 -0
  89. /package/dist/{chunk-LGBNHWDN.js.map → chunk-ACUXSU6J.js.map} +0 -0
  90. /package/dist/{chunk-KVNGYFFP.js.map → chunk-AV4UXMOH.js.map} +0 -0
  91. /package/dist/{chunk-4ZKAMPBX.js.map → chunk-BKOCXNAP.js.map} +0 -0
  92. /package/dist/{chunk-3OCYNMZ5.js.map → chunk-CT6I3HOP.js.map} +0 -0
  93. /package/dist/{chunk-NXZC6UTN.js.map → chunk-DFUO7J3W.js.map} +0 -0
  94. /package/dist/{chunk-TOYHZHTK.js.map → chunk-E5NSDLYJ.js.map} +0 -0
  95. /package/dist/{chunk-77Y7R243.js.map → chunk-GAXC43J4.js.map} +0 -0
  96. /package/dist/{chunk-FQBR6SCA.js.map → chunk-KLNHD4TC.js.map} +0 -0
  97. /package/dist/{chunk-CNKVEVUN.js.map → chunk-LDNRL62P.js.map} +0 -0
  98. /package/dist/{chunk-I5CJJFEJ.js.map → chunk-LKMRJ2CP.js.map} +0 -0
  99. /package/dist/{chunk-5FNWOF46.js.map → chunk-RLC5IISG.js.map} +0 -0
  100. /package/dist/{chunk-GKPY7VLK.js.map → chunk-RM3PTDSI.js.map} +0 -0
  101. /package/dist/{chunk-NIDPX52C.js.map → chunk-T3WAHAMH.js.map} +0 -0
@@ -18,6 +18,7 @@ import { describe, expect, it } from 'vitest';
18
18
  import { decodeNatsChunk } from '../../../chat-protocol/nats-decoder';
19
19
  import { createChatStreamReducer } from '../stream/chat-stream-reducer';
20
20
  import type { HistoricalMessage, MessageSegment, TicketEventSegment } from '../types';
21
+ import type { UnifiedChatMessage } from '../types/unified-chat-state.types';
21
22
  import { MessageSegmentAccumulator } from '../utils/message-segment-accumulator';
22
23
  import { processHistoricalMessages } from '../utils/process-historical-messages';
23
24
 
@@ -312,6 +313,112 @@ describe('processHistoricalMessages — ticket event', () => {
312
313
  });
313
314
  });
314
315
 
316
+ describe('processHistoricalMessages — row stream sequence', () => {
317
+ it('stamps the hydrated card with the row seq (standalone row)', () => {
318
+ // TICKET_EVENT chunks are standalone, one per row, so the row's
319
+ // `lastChunkStreamSeq` IS the event's sequence. Without the stamp the
320
+ // hydrated card never joins a stale-consumer/catch-up replay of the same
321
+ // chunk in the store, and the replayed copy rendered as a second
322
+ // identical card (the resolve/reopen duplication).
323
+ const { messages } = processHistoricalMessages([
324
+ {
325
+ id: 'm1',
326
+ createdAt: '2026-08-31T00:54:00Z',
327
+ owner: { type: 'ASSISTANT' },
328
+ lastChunkStreamSeq: 412,
329
+ messageData: [{ type: 'TICKET_EVENT', kind: 'RESOLVED', actorType: 'AI' } as never],
330
+ },
331
+ ]);
332
+ expect(events(messages[0].content)[0].streamSeq).toBe(412);
333
+ });
334
+
335
+ it('does NOT stamp a bundled row - its seq belongs to the LAST chunk', () => {
336
+ const { messages } = processHistoricalMessages([
337
+ {
338
+ id: 'm1',
339
+ createdAt: '2026-08-31T00:54:00Z',
340
+ owner: { type: 'ASSISTANT' },
341
+ lastChunkStreamSeq: 413,
342
+ messageData: [
343
+ { type: 'TICKET_EVENT', kind: 'RESOLVED', actorType: 'AI' } as never,
344
+ { type: 'TEXT', text: 'trailing text' } as never,
345
+ ],
346
+ },
347
+ ]);
348
+ expect(events(messages[0].content)[0].streamSeq).toBeUndefined();
349
+ });
350
+ });
351
+
352
+ describe('reducer over a hydrated thread — the resolve/reopen duplication', () => {
353
+ // The reported bug (admin PSA chat, ticket 86ak8a74d; same lib hole as the
354
+ // desktop dup in 86ak56ehp): reopening a dialog hydrates the persisted
355
+ // TICKET_EVENT row into the thread, then a consumer created with a stale
356
+ // `optStartSeq` replays the same chunk. The twins meet ONLY in the store
357
+ // (a complete hydrated tail seeds no accumulator), so the join must happen
358
+ // in `appendToTrailingAssistant` - it used to push the replay beside the
359
+ // hydrated card as a second identical block.
360
+ const hydratedThread = (card: TicketEventSegment): UnifiedChatMessage[] => [
361
+ {
362
+ id: 'assistant-m1',
363
+ role: 'assistant',
364
+ content: '',
365
+ segments: [{ type: 'text', text: 'Excellent! Feel free to reach out.' }, card],
366
+ },
367
+ ];
368
+
369
+ const replay = (reducer: ReturnType<typeof createChatStreamReducer>, chunk: unknown) => {
370
+ const event = decodeNatsChunk(chunk);
371
+ if (event) reducer.apply(event);
372
+ return reducer.state.messages[reducer.state.messages.length - 1]?.segments ?? [];
373
+ };
374
+
375
+ it('joins the replay with a seq-stamped hydrated card (one card, row time kept)', () => {
376
+ const reducer = createChatStreamReducer({ transport: 'nats' });
377
+ reducer.initializeWithState(
378
+ hydratedThread({
379
+ type: 'ticket_event',
380
+ data: { kind: 'RESOLVED', actorType: 'AI' },
381
+ streamSeq: 412,
382
+ occurredAt: new Date('2026-08-31T00:54:00Z'),
383
+ }),
384
+ );
385
+ const segments = replay(reducer, { type: 'TICKET_EVENT', kind: 'RESOLVED', actorType: 'AI', streamSeq: 412 });
386
+ const cards = events(segments);
387
+ expect(cards).toHaveLength(1);
388
+ // First-known time wins: the replay only knows its late arrival time.
389
+ expect(cards[0].occurredAt).toEqual(new Date('2026-08-31T00:54:00Z'));
390
+ // The reply the user is reading stays intact.
391
+ expect(segments.find(s => s.type === 'text')).toMatchObject({ text: 'Excellent! Feel free to reach out.' });
392
+ });
393
+
394
+ it('joins the replay with a seq-LESS hydrated twin (rows persisted before the seq stamp)', () => {
395
+ const reducer = createChatStreamReducer({ transport: 'nats' });
396
+ reducer.initializeWithState(
397
+ hydratedThread({
398
+ type: 'ticket_event',
399
+ data: { kind: 'RESOLVED', actorType: 'AI' },
400
+ occurredAt: new Date('2026-08-31T00:54:00Z'),
401
+ }),
402
+ );
403
+ const cards = events(replay(reducer, { type: 'TICKET_EVENT', kind: 'RESOLVED', actorType: 'AI', streamSeq: 412 }));
404
+ expect(cards).toHaveLength(1);
405
+ expect(cards[0].streamSeq).toBe(412);
406
+ expect(cards[0].occurredAt).toEqual(new Date('2026-08-31T00:54:00Z'));
407
+ });
408
+
409
+ it('keeps a genuinely repeated cycle distinct across hydrate + live', () => {
410
+ // resolve (hydrated, seq-less legacy row) -> live reopen -> live resolve
411
+ // payload-identical to the first: only the LAST ticket event may
412
+ // payload-match, so the final resolve renders as its own card.
413
+ const reducer = createChatStreamReducer({ transport: 'nats' });
414
+ reducer.initializeWithState(hydratedThread({ type: 'ticket_event', data: { kind: 'RESOLVED', actorType: 'AI' } }));
415
+ replay(reducer, { type: 'TICKET_EVENT', kind: 'REOPENED', actorType: 'CLIENT', streamSeq: 413 });
416
+ const cards = events(replay(reducer, { type: 'TICKET_EVENT', kind: 'RESOLVED', actorType: 'AI', streamSeq: 414 }));
417
+ expect(cards).toHaveLength(3);
418
+ expect(cards.map(c => c.data.kind)).toEqual(['RESOLVED', 'REOPENED', 'RESOLVED']);
419
+ });
420
+ });
421
+
315
422
  describe('accumulator — occurredAt across the hydrate/catch-up overlap', () => {
316
423
  it('keeps the hydrated row time when the seq-stamped redelivery carries none', () => {
317
424
  const accumulator = new MessageSegmentAccumulator();
@@ -95,14 +95,56 @@ function upsertKey(seg: MessageSegment): string | null {
95
95
  return `escalated:${seg.data.ticketId}`;
96
96
  case 'ticket_event':
97
97
  // The stream sequence is the event's only stable id; a seq-less segment
98
- // (hydrated history) is pushed raw — `addTicketEvent`'s payload-equality
99
- // upsert owns that case.
98
+ // (a row hydrated without `lastChunkStreamSeq`) is pushed raw and joined
99
+ // by `seqlessTicketEventTwinIndex` when its replayed copy arrives.
100
100
  return seg.streamSeq !== undefined ? `ticket-event:${seg.streamSeq}` : null;
101
101
  default:
102
102
  return null;
103
103
  }
104
104
  }
105
105
 
106
+ /**
107
+ * A seq'd ticket event meeting a HYDRATED twin of itself: history stamps the
108
+ * row's `lastChunkStreamSeq` onto the segment only for standalone rows, and
109
+ * rows persisted before that field existed hydrate seq-less - so the
110
+ * `ticket-event:<seq>` key alone cannot join them, and the replayed copy used
111
+ * to render as a second identical card. Mirror of `addTicketEvent`'s payload
112
+ * fallback: consider only the LAST ticket_event (older ones may be a genuinely
113
+ * repeated resolve/reopen cycle) and never collapse two distinct known seqs.
114
+ */
115
+ function seqlessTicketEventTwinIndex(segments: MessageSegment[], seg: MessageSegment): number {
116
+ if (seg.type !== 'ticket_event') return -1;
117
+ for (let i = segments.length - 1; i >= 0; i--) {
118
+ const s = segments[i];
119
+ if (s.type !== 'ticket_event') continue;
120
+ if (
121
+ s.streamSeq === undefined &&
122
+ s.data.kind === seg.data.kind &&
123
+ s.data.actorId === seg.data.actorId &&
124
+ s.data.actorName === seg.data.actorName &&
125
+ s.data.actorType === seg.data.actorType &&
126
+ s.data.reason === seg.data.reason &&
127
+ s.data.targetStatusKind === seg.data.targetStatusKind
128
+ ) {
129
+ return i;
130
+ }
131
+ break;
132
+ }
133
+ return -1;
134
+ }
135
+
136
+ /**
137
+ * First-known time wins, exactly like `addTicketEvent`: the hydrated twin
138
+ * carries the row's real `createdAt`, while a replay only knows its (late)
139
+ * arrival time - replacing wholesale would regress the card to arrival time.
140
+ * Non-ticket-event upserts keep their replace-wholesale semantics.
141
+ */
142
+ function withFirstKnownOccurredAt(existing: MessageSegment, incoming: MessageSegment): MessageSegment {
143
+ if (existing.type !== 'ticket_event' || incoming.type !== 'ticket_event') return incoming;
144
+ const occurredAt = existing.occurredAt ?? incoming.occurredAt;
145
+ return occurredAt !== undefined ? { ...incoming, occurredAt } : incoming;
146
+ }
147
+
106
148
  /**
107
149
  * Append-mode counterpart of `updateTrailingAssistant` for post-MESSAGE_END
108
150
  * continuation fragments (`SegmentsUpdateMetadata.append`). Coalesces
@@ -130,8 +172,9 @@ export function appendToTrailingAssistant(
130
172
  // Block deltas must be IDEMPOTENT: an emit can be seen twice (live plus
131
173
  // the JetStream catch-up replay over hydrated history), so upsert on the
132
174
  // block's identity instead of raw-appending a duplicate card.
133
- const idx = merged.findIndex(m => upsertKey(m) === key);
134
- if (idx !== -1) merged[idx] = seg;
175
+ let idx = merged.findIndex(m => upsertKey(m) === key);
176
+ if (idx === -1) idx = seqlessTicketEventTwinIndex(merged, seg);
177
+ if (idx !== -1) merged[idx] = withFirstKnownOccurredAt(merged[idx], seg);
135
178
  else merged.push(seg);
136
179
  } else {
137
180
  // Other non-text segments are pushed RAW on purpose: EXECUTING↔EXECUTED
@@ -315,6 +315,7 @@ function applyHistoryEvent(
315
315
  escalatedApprovals?: EscalatedApprovals,
316
316
  offerResolutions?: OfferResolutions,
317
317
  rowCreatedAt?: Date,
318
+ rowStreamSeq?: number,
318
319
  ): void {
319
320
  // batchApprovalsEnabled is owned by the consumer (oss-tenant chat client /
320
321
  // openframe-frontend tickets). Defaults to ON so consumers that haven't
@@ -342,12 +343,16 @@ function applyHistoryEvent(
342
343
  });
343
344
  break;
344
345
 
345
- // Seq-less on purpose: the persisted row's sequence lives on the message
346
- // (`lastChunkStreamSeq`), not in `messageData`. A live redelivery of the
347
- // same event matches this segment by payload equality (see
348
- // `addTicketEvent`).
346
+ // `rowStreamSeq` re-stamps the event's JetStream sequence (the persisted
347
+ // row's `lastChunkStreamSeq` - `messageData` itself does not carry it).
348
+ // The store's `ticket-event:<seq>` upsert key is the ONLY join between
349
+ // this hydrated segment and a catch-up/stale-consumer replay of the same
350
+ // chunk; a seq-less segment never joins, so the replayed copy rendered as
351
+ // a second identical card (the resolve/reopen duplication). The payload
352
+ // fallback in `addTicketEvent` cannot own that case: a COMPLETE hydrated
353
+ // tail seeds no accumulator, so the twins only ever meet in the store.
349
354
  case 'ticket-event':
350
- // `rowCreatedAt` is the persisted row's own time — without it the card
355
+ // `rowCreatedAt` is the persisted row's own time - without it the card
351
356
  // renders the enclosing assistant bubble's timestamp, i.e. the FIRST
352
357
  // row of the turn, and every lifecycle card reads the same stale time.
353
358
  accumulator.addTicketEvent(
@@ -359,7 +364,7 @@ function applyHistoryEvent(
359
364
  reason: event.reason,
360
365
  targetStatusKind: event.targetStatusKind,
361
366
  },
362
- undefined,
367
+ rowStreamSeq,
363
368
  rowCreatedAt,
364
369
  );
365
370
  break;
@@ -697,6 +702,13 @@ export function processHistoricalMessages(
697
702
  escalatedApprovals,
698
703
  offerResolutions,
699
704
  new Date(msg.createdAt),
705
+ // TICKET_EVENT chunks are standalone, one per row, so the row seq IS
706
+ // the event seq - but vouch for that only when the row holds exactly
707
+ // one entry: a bundled row's `lastChunkStreamSeq` belongs to its LAST
708
+ // chunk and could stamp the wrong event.
709
+ messageDataArray.length === 1 && typeof msg.lastChunkStreamSeq === 'number'
710
+ ? msg.lastChunkStreamSeq
711
+ : undefined,
700
712
  );
701
713
  });
702
714