@flamingo-stack/openframe-frontend-core 0.0.579 → 0.0.580-1958.5358.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 (176) hide show
  1. package/dist/chat-protocol/decode.d.ts.map +1 -1
  2. package/dist/chat-protocol/events.d.ts +1 -47
  3. package/dist/chat-protocol/events.d.ts.map +1 -1
  4. package/dist/chat-protocol/index.cjs +0 -51
  5. package/dist/chat-protocol/index.cjs.map +1 -1
  6. package/dist/chat-protocol/index.js +1 -48
  7. package/dist/chat-protocol/index.js.map +1 -1
  8. package/dist/chat-protocol/leading-frames.d.ts +3 -9
  9. package/dist/chat-protocol/leading-frames.d.ts.map +1 -1
  10. package/dist/chat-protocol/nats-decoder.d.ts +0 -41
  11. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
  12. package/dist/{chunk-OGZAFM6M.cjs → chunk-26NL6WZU.cjs} +5 -5
  13. package/dist/{chunk-OGZAFM6M.cjs.map → chunk-26NL6WZU.cjs.map} +1 -1
  14. package/dist/{chunk-QCP2KV7P.js → chunk-4SYELUXP.js} +3 -3
  15. package/dist/{chunk-4HGRXFQP.cjs → chunk-6PLACYFA.cjs} +3 -3
  16. package/dist/{chunk-4HGRXFQP.cjs.map → chunk-6PLACYFA.cjs.map} +1 -1
  17. package/dist/{chunk-FEVLXRTC.cjs → chunk-7LGVUXWE.cjs} +344 -282
  18. package/dist/chunk-7LGVUXWE.cjs.map +1 -0
  19. package/dist/{chunk-AKPLEWMH.js → chunk-A4BZ2CP4.js} +2 -2
  20. package/dist/{chunk-HHJVIHVR.cjs → chunk-COSV5FC2.cjs} +69 -69
  21. package/dist/{chunk-HHJVIHVR.cjs.map → chunk-COSV5FC2.cjs.map} +1 -1
  22. package/dist/{chunk-4WSKSNXL.js → chunk-CX62MVCV.js} +6 -6
  23. package/dist/{chunk-O2FTVZOD.js → chunk-DZAPMQ6U.js} +3 -3
  24. package/dist/{chunk-YW4WBNYV.cjs → chunk-EXUKW6GT.cjs} +24 -24
  25. package/dist/{chunk-YW4WBNYV.cjs.map → chunk-EXUKW6GT.cjs.map} +1 -1
  26. package/dist/{chunk-PWOYEUIQ.js → chunk-FN4PUTHZ.js} +5 -5
  27. package/dist/{chunk-L2CF2QZW.js → chunk-JDP5BGVO.js} +5 -5
  28. package/dist/{chunk-5Y6TPFW7.js → chunk-JFVT6XRN.js} +5 -5
  29. package/dist/{chunk-BCJQ7CJE.cjs → chunk-JHLZTU7R.cjs} +9 -9
  30. package/dist/{chunk-BCJQ7CJE.cjs.map → chunk-JHLZTU7R.cjs.map} +1 -1
  31. package/dist/{chunk-D44S2QLI.js → chunk-OSIIXSYG.js} +3 -3
  32. package/dist/{chunk-DB3TV4CW.js → chunk-PGWXVNYS.js} +3 -3
  33. package/dist/{chunk-H54EPP3P.cjs → chunk-PP3LCLWI.cjs} +4 -4
  34. package/dist/{chunk-H54EPP3P.cjs.map → chunk-PP3LCLWI.cjs.map} +1 -1
  35. package/dist/{chunk-M5GWCMYE.cjs → chunk-QIDD3LTS.cjs} +32 -32
  36. package/dist/{chunk-M5GWCMYE.cjs.map → chunk-QIDD3LTS.cjs.map} +1 -1
  37. package/dist/{chunk-3AX3ZEY3.js → chunk-QZSNH7XU.js} +1317 -1480
  38. package/dist/chunk-QZSNH7XU.js.map +1 -0
  39. package/dist/{chunk-37XJYOAY.cjs → chunk-RF65N4QA.cjs} +10 -10
  40. package/dist/{chunk-37XJYOAY.cjs.map → chunk-RF65N4QA.cjs.map} +1 -1
  41. package/dist/{chunk-36TJYCF3.cjs → chunk-S5BS2CZA.cjs} +21 -21
  42. package/dist/{chunk-36TJYCF3.cjs.map → chunk-S5BS2CZA.cjs.map} +1 -1
  43. package/dist/{chunk-QGJYWLF4.js → chunk-SN6SL4HR.js} +2 -2
  44. package/dist/{chunk-4FJSN2XW.cjs → chunk-TDM5FQZG.cjs} +10 -10
  45. package/dist/{chunk-4FJSN2XW.cjs.map → chunk-TDM5FQZG.cjs.map} +1 -1
  46. package/dist/{chunk-HGA74X35.js → chunk-TXNAZKWW.js} +3 -3
  47. package/dist/{chunk-IFYQ7DHV.cjs → chunk-UNVW4BVF.cjs} +43 -43
  48. package/dist/{chunk-IFYQ7DHV.cjs.map → chunk-UNVW4BVF.cjs.map} +1 -1
  49. package/dist/{chunk-CP5EYMNL.js → chunk-V5RWTIRS.js} +202 -140
  50. package/dist/{chunk-CP5EYMNL.js.map → chunk-V5RWTIRS.js.map} +1 -1
  51. package/dist/{chunk-RKGOVTXZ.cjs → chunk-VYQNM75R.cjs} +167 -330
  52. package/dist/chunk-VYQNM75R.cjs.map +1 -0
  53. package/dist/{chunk-PMENIZXI.js → chunk-WEUWTEQL.js} +4 -4
  54. package/dist/{chunk-H4BPHGU5.js → chunk-WW435OOF.js} +2 -2
  55. package/dist/{chunk-4LZ7B5H5.cjs → chunk-YB75SLEH.cjs} +3 -3
  56. package/dist/{chunk-4LZ7B5H5.cjs.map → chunk-YB75SLEH.cjs.map} +1 -1
  57. package/dist/{chunk-A6C5HOD7.cjs → chunk-YLYU6DDT.cjs} +15 -15
  58. package/dist/{chunk-A6C5HOD7.cjs.map → chunk-YLYU6DDT.cjs.map} +1 -1
  59. package/dist/{chunk-BW4WDUSI.js → chunk-ZNCYSM6R.js} +2 -2
  60. package/dist/{chunk-NPVDKOGD.cjs → chunk-ZQ2RBAXO.cjs} +22 -22
  61. package/dist/{chunk-NPVDKOGD.cjs.map → chunk-ZQ2RBAXO.cjs.map} +1 -1
  62. package/dist/components/case-studies/index.cjs +9 -9
  63. package/dist/components/case-studies/index.js +3 -3
  64. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  65. package/dist/components/chat/index.cjs +2 -10
  66. package/dist/components/chat/index.cjs.map +1 -1
  67. package/dist/components/chat/index.d.ts +0 -1
  68. package/dist/components/chat/index.d.ts.map +1 -1
  69. package/dist/components/chat/index.js +1 -9
  70. package/dist/components/chat/stream/chat-stream-reducer.d.ts +0 -9
  71. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
  72. package/dist/components/chat/stream/delta-batcher.d.ts +3 -3
  73. package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -1
  74. package/dist/components/chat/stream/message-mutations.d.ts.map +1 -1
  75. package/dist/components/chat/types/component.types.d.ts +0 -6
  76. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  77. package/dist/components/chat/types/message.types.d.ts +2 -30
  78. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  79. package/dist/components/chat/types/network.types.d.ts +1 -3
  80. package/dist/components/chat/types/network.types.d.ts.map +1 -1
  81. package/dist/components/chat/types/processing.types.d.ts +1 -4
  82. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  83. package/dist/components/chat/utils/approval-display.d.ts +8 -36
  84. package/dist/components/chat/utils/approval-display.d.ts.map +1 -1
  85. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  86. package/dist/components/chat/utils/index.d.ts +1 -1
  87. package/dist/components/chat/utils/index.d.ts.map +1 -1
  88. package/dist/components/chat/utils/message-segment-accumulator.d.ts +4 -16
  89. package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
  90. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  91. package/dist/components/chat/utils/resolve-fetched-card-href.d.ts +1 -1
  92. package/dist/components/contact/index.cjs +4 -4
  93. package/dist/components/contact/index.js +3 -3
  94. package/dist/components/docs/index.cjs +7 -7
  95. package/dist/components/docs/index.js +6 -6
  96. package/dist/components/embeds/index.cjs +4 -4
  97. package/dist/components/embeds/index.js +3 -3
  98. package/dist/components/faq/index.cjs +4 -4
  99. package/dist/components/faq/index.js +3 -3
  100. package/dist/components/features/index.cjs +3 -3
  101. package/dist/components/features/index.js +2 -2
  102. package/dist/components/features/push-button-selector.d.ts +29 -1
  103. package/dist/components/features/push-button-selector.d.ts.map +1 -1
  104. package/dist/components/help-center-pages/index.cjs +23 -23
  105. package/dist/components/help-center-pages/index.js +14 -14
  106. package/dist/components/index.cjs +121 -129
  107. package/dist/components/index.cjs.map +1 -1
  108. package/dist/components/index.js +13 -21
  109. package/dist/components/index.js.map +1 -1
  110. package/dist/components/meeting-scheduler/index.cjs +21 -21
  111. package/dist/components/meeting-scheduler/index.js +2 -2
  112. package/dist/components/navigation/index.cjs +6 -6
  113. package/dist/components/navigation/index.js +5 -5
  114. package/dist/components/onboarding-guides/index.cjs +6 -6
  115. package/dist/components/onboarding-guides/index.js +5 -5
  116. package/dist/components/tickets/index.cjs +7 -7
  117. package/dist/components/tickets/index.js +6 -6
  118. package/dist/components/ui/index.cjs +3 -11
  119. package/dist/components/ui/index.cjs.map +1 -1
  120. package/dist/components/ui/index.js +2 -10
  121. package/dist/index.cjs +7 -15
  122. package/dist/index.cjs.map +1 -1
  123. package/dist/index.js +6 -14
  124. package/dist/index.js.map +1 -1
  125. package/dist/utils/index.cjs.map +1 -1
  126. package/dist/utils/index.js.map +1 -1
  127. package/package.json +1 -1
  128. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +0 -80
  129. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +1 -118
  130. package/src/chat-protocol/decode.ts +3 -4
  131. package/src/chat-protocol/events.ts +0 -51
  132. package/src/chat-protocol/leading-frames.ts +3 -9
  133. package/src/chat-protocol/nats-decoder.ts +1 -110
  134. package/src/components/chat/__tests__/chat-pending-turn.test.tsx +0 -19
  135. package/src/components/chat/chat-message-enhanced.tsx +1 -13
  136. package/src/components/chat/index.ts +0 -1
  137. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +0 -90
  138. package/src/components/chat/stream/chat-dialog-store.ts +1 -1
  139. package/src/components/chat/stream/chat-stream-reducer.ts +11 -49
  140. package/src/components/chat/stream/delta-batcher.ts +4 -9
  141. package/src/components/chat/stream/message-mutations.ts +0 -2
  142. package/src/components/chat/types/component.types.ts +0 -9
  143. package/src/components/chat/types/message.types.ts +0 -32
  144. package/src/components/chat/types/network.types.ts +1 -3
  145. package/src/components/chat/types/processing.types.ts +1 -4
  146. package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +0 -93
  147. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +1 -78
  148. package/src/components/chat/utils/approval-display.ts +8 -53
  149. package/src/components/chat/utils/extract-incomplete-message-state.ts +0 -1
  150. package/src/components/chat/utils/index.ts +1 -6
  151. package/src/components/chat/utils/message-segment-accumulator.ts +3 -37
  152. package/src/components/chat/utils/process-historical-messages.ts +7 -44
  153. package/src/components/chat/utils/resolve-fetched-card-href.ts +1 -1
  154. package/src/components/features/.push-button-selector.md +1 -1
  155. package/src/components/features/push-button-selector.tsx +130 -6
  156. package/src/stories/ChatMessageEnhanced.stories.tsx +3 -7
  157. package/dist/chunk-3AX3ZEY3.js.map +0 -1
  158. package/dist/chunk-FEVLXRTC.cjs.map +0 -1
  159. package/dist/chunk-RKGOVTXZ.cjs.map +0 -1
  160. package/dist/components/chat/guide-display.d.ts +0 -14
  161. package/dist/components/chat/guide-display.d.ts.map +0 -1
  162. package/src/components/chat/guide-display.tsx +0 -42
  163. /package/dist/{chunk-QCP2KV7P.js.map → chunk-4SYELUXP.js.map} +0 -0
  164. /package/dist/{chunk-AKPLEWMH.js.map → chunk-A4BZ2CP4.js.map} +0 -0
  165. /package/dist/{chunk-4WSKSNXL.js.map → chunk-CX62MVCV.js.map} +0 -0
  166. /package/dist/{chunk-O2FTVZOD.js.map → chunk-DZAPMQ6U.js.map} +0 -0
  167. /package/dist/{chunk-PWOYEUIQ.js.map → chunk-FN4PUTHZ.js.map} +0 -0
  168. /package/dist/{chunk-L2CF2QZW.js.map → chunk-JDP5BGVO.js.map} +0 -0
  169. /package/dist/{chunk-5Y6TPFW7.js.map → chunk-JFVT6XRN.js.map} +0 -0
  170. /package/dist/{chunk-D44S2QLI.js.map → chunk-OSIIXSYG.js.map} +0 -0
  171. /package/dist/{chunk-DB3TV4CW.js.map → chunk-PGWXVNYS.js.map} +0 -0
  172. /package/dist/{chunk-QGJYWLF4.js.map → chunk-SN6SL4HR.js.map} +0 -0
  173. /package/dist/{chunk-HGA74X35.js.map → chunk-TXNAZKWW.js.map} +0 -0
  174. /package/dist/{chunk-PMENIZXI.js.map → chunk-WEUWTEQL.js.map} +0 -0
  175. /package/dist/{chunk-H4BPHGU5.js.map → chunk-WW435OOF.js.map} +0 -0
  176. /package/dist/{chunk-BW4WDUSI.js.map → chunk-ZNCYSM6R.js.map} +0 -0
@@ -155,22 +155,6 @@ export class MessageSegmentAccumulator {
155
155
  return this.getSegments();
156
156
  }
157
157
 
158
- /**
159
- * Append guide text to the current message.
160
- * If the last segment is guide, append to it; otherwise start a new guide segment.
161
- */
162
- appendGuide(text: string): MessageSegment[] {
163
- const lastSegment = this.segments[this.segments.length - 1];
164
-
165
- if (lastSegment && lastSegment.type === 'guide') {
166
- this.segments[this.segments.length - 1] = { type: 'guide', text: lastSegment.text + text };
167
- } else {
168
- this.segments.push({ type: 'guide', text });
169
- }
170
-
171
- return this.getSegments();
172
- }
173
-
174
158
  /**
175
159
  * Add a clarification (ask) card. Unlike the three delta streams an ask
176
160
  * arrives whole in one chunk, so it is always a NEW segment — never merged
@@ -325,15 +309,11 @@ export class MessageSegmentAccumulator {
325
309
  status: ChatApprovalStatus = 'pending',
326
310
  /** Structured label/value rows. The card prefers them over `explanation`
327
311
  * (see `ApprovalRequestData.fields`). Optional because the agent's own
328
- * approvals carry prose; a Product Guide card is almost entirely fields —
329
- * dropping them here left it as a bare title. */
312
+ * approvals carry prose. */
330
313
  fields?: ApprovalRequestField[],
331
- /** Where the card came from; `'guide'` keeps it inline (see
332
- * `ApprovalRequestData.origin`). */
333
- origin?: 'guide',
334
314
  ): MessageSegment[] {
335
315
  this.segments.push(
336
- this.buildApprovalRequestSegment(requestId, { command, explanation, approvalType, fields, origin }, status),
316
+ this.buildApprovalRequestSegment(requestId, { command, explanation, approvalType, fields }, status),
337
317
  );
338
318
  return this.getSegments();
339
319
  }
@@ -344,7 +324,7 @@ export class MessageSegmentAccumulator {
344
324
  * Three paths produce these: `addApprovalRequest` (live stream and replay),
345
325
  * `flushPendingApprovals` (tracked-but-unresolved after a history replay) and
346
326
  * `processApprovalResult` (a result arriving for a tracked request). They used
347
- * to hand-write the object each, which is how `fields` and `origin` reached
327
+ * to hand-write the object each, which is how `fields` reached
348
328
  * the card down one path and not the others — the same proposal rendering as a
349
329
  * bare title, or losing the marker that routes its buttons to the hub.
350
330
  */
@@ -361,7 +341,6 @@ export class MessageSegmentAccumulator {
361
341
  requestId,
362
342
  approvalType: approval.approvalType,
363
343
  ...(approval.fields && approval.fields.length > 0 ? { fields: approval.fields } : {}),
364
- ...(approval.origin ? { origin: approval.origin } : {}),
365
344
  },
366
345
  status,
367
346
  onApprove: this.callbacks.onApprove,
@@ -389,9 +368,6 @@ export class MessageSegmentAccumulator {
389
368
  status: ChatApprovalStatus = 'pending',
390
369
  executions?: Record<string, ApprovalBatchExecutionState>,
391
370
  resolvedByName?: string | null,
392
- /** Where the batch came from; `'guide'` keeps it inline and routes it to
393
- * the hub, exactly as for a single card (see `ApprovalBatchData.origin`). */
394
- origin?: 'guide',
395
371
  ): MessageSegment[] {
396
372
  const existingIndex = this.segments.findIndex(
397
373
  (s): s is ApprovalBatchSegment => s.type === 'approval_batch' && s.data.approvalRequestId === approvalRequestId,
@@ -400,9 +376,6 @@ export class MessageSegmentAccumulator {
400
376
  if (existingIndex !== -1) {
401
377
  const existing = this.segments[existingIndex] as ApprovalBatchSegment;
402
378
  const mergedExecutions = executions ?? existing.data.executions;
403
- // An upsert must not strip the marker: the flip to `approved` comes from
404
- // a later event that carries no origin of its own.
405
- const mergedOrigin = origin ?? existing.data.origin;
406
379
  this.segments[existingIndex] = {
407
380
  ...existing,
408
381
  data: {
@@ -410,7 +383,6 @@ export class MessageSegmentAccumulator {
410
383
  approvalType,
411
384
  toolCalls,
412
385
  ...(mergedExecutions ? { executions: mergedExecutions } : {}),
413
- ...(mergedOrigin ? { origin: mergedOrigin } : {}),
414
386
  },
415
387
  status,
416
388
  resolvedByName: resolvedByName ?? existing.resolvedByName,
@@ -427,7 +399,6 @@ export class MessageSegmentAccumulator {
427
399
  approvalType,
428
400
  toolCalls,
429
401
  ...(executions ? { executions } : {}),
430
- ...(origin ? { origin } : {}),
431
402
  },
432
403
  status,
433
404
  resolvedByName,
@@ -685,9 +656,6 @@ export class MessageSegmentAccumulator {
685
656
  case 'thinking':
686
657
  if (segment.text) this.appendThinking(segment.text);
687
658
  break;
688
- case 'guide':
689
- if (segment.text) this.appendGuide(segment.text);
690
- break;
691
659
  case 'ask':
692
660
  this.addAsk(segment.question, segment.options);
693
661
  break;
@@ -703,7 +671,6 @@ export class MessageSegmentAccumulator {
703
671
  data.approvalType || '',
704
672
  status,
705
673
  data.fields,
706
- data.origin,
707
674
  );
708
675
  break;
709
676
  }
@@ -716,7 +683,6 @@ export class MessageSegmentAccumulator {
716
683
  status,
717
684
  data.executions,
718
685
  resolvedByName,
719
- data.origin,
720
686
  );
721
687
  break;
722
688
  }
@@ -33,7 +33,7 @@ import {
33
33
  } from '../../../chat-protocol/events';
34
34
  // One normalizer for ask rows, shared with the live decoder — history and the
35
35
  // stream must agree on which options are usable.
36
- import { guideFrameEvent, normalizeAskOptions } from '../../../chat-protocol/nats-decoder';
36
+ import { normalizeAskOptions } from '../../../chat-protocol/nats-decoder';
37
37
  import { applyApprovalStatusToSegment } from '../stream/message-mutations';
38
38
  import {
39
39
  MESSAGE_TYPE,
@@ -47,7 +47,7 @@ import {
47
47
  type MessageData,
48
48
  type MessageOwner,
49
49
  } from '../types';
50
- import { approvalDisplaysInline, guideApprovalOrigin } from './approval-display';
50
+ import { approvalDisplaysInline } from './approval-display';
51
51
  import { type MessageSegmentAccumulator, createMessageSegmentAccumulator } from './message-segment-accumulator';
52
52
  import { getCommandText } from './tool-call-helpers';
53
53
 
@@ -121,22 +121,6 @@ export function decodeHistoricalMessageData(data: MessageData): ChatStreamEvent
121
121
  }
122
122
  return null;
123
123
 
124
- // Two persisted shapes, mirroring the live `GUIDE` chunk: the answer body
125
- // (`text`) and a Product Guide frame the agent re-streamed (`payload`,
126
- // persisted so a card survives a reload). Decoded through the SAME
127
- // `guideFrameEvent` the live path uses — a second mapping here would let
128
- // history and realtime disagree about the same bytes.
129
- case MESSAGE_TYPE.GUIDE: {
130
- if ('text' in data && data.text) {
131
- return { type: 'guide-delta', text: data.text };
132
- }
133
- const payload = 'payload' in data ? data.payload : undefined;
134
- if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
135
- return guideFrameEvent(payload);
136
- }
137
- return null;
138
- }
139
-
140
124
  // Same completeness gate as the live decoder (`decodeNatsChunk`): a
141
125
  // persisted row without a question or without options is not a card the
142
126
  // user can answer, so it replays as nothing rather than as empty chrome.
@@ -397,10 +381,6 @@ function applyHistoryEvent(
397
381
  accumulator.appendThinking(event.text);
398
382
  break;
399
383
 
400
- case 'guide-delta':
401
- accumulator.appendGuide(event.text);
402
- break;
403
-
404
384
  // Mirror of the live path: the intro sentence replays as answer text in
405
385
  // front of the card, so a reloaded thread reads exactly like the stream did.
406
386
  case 'ask':
@@ -417,23 +397,13 @@ function applyHistoryEvent(
417
397
  const toolCalls = event.toolCalls;
418
398
  const isBatch = !!toolCalls && toolCalls.length > 0;
419
399
  // Same rule the live kernels use — a card must not change where it
420
- // renders (or which backend its buttons hit) just because the page was
421
- // reloaded and it came back through history instead of the stream.
422
- const guideOrigin = guideApprovalOrigin(event);
423
-
424
- if (approvalDisplaysInline(event, approvalType, displayApprovalTypes)) {
400
+ // renders just because the page was reloaded and it came back through
401
+ // history instead of the stream.
402
+ if (approvalDisplaysInline(approvalType, displayApprovalTypes)) {
425
403
  if (isBatch) {
426
404
  const status = (approvalStatuses[event.requestId] as ChatApprovalStatus) || 'pending';
427
405
  if (batchApprovalsEnabled) {
428
- accumulator.addApprovalBatch(
429
- event.requestId,
430
- approvalType,
431
- toolCalls,
432
- status,
433
- undefined,
434
- undefined,
435
- guideOrigin,
436
- );
406
+ accumulator.addApprovalBatch(event.requestId, approvalType, toolCalls, status, undefined, undefined);
437
407
  } else {
438
408
  // Flag OFF — unfold batch into N legacy approval cards (same id).
439
409
  for (const call of toolCalls) {
@@ -445,7 +415,6 @@ function applyHistoryEvent(
445
415
  approvalType,
446
416
  status,
447
417
  undefined,
448
- guideOrigin,
449
418
  );
450
419
  }
451
420
  }
@@ -459,12 +428,7 @@ function applyHistoryEvent(
459
428
  // and honor `approvalStatuses`.
460
429
  const resolvedStatus = approvalStatuses[event.requestId] as ChatApprovalStatus | undefined;
461
430
  const isResolved = resolvedStatus === 'approved' || resolvedStatus === 'rejected';
462
- // A guide card is added inline even while pending. The tracked path
463
- // below ends in `flushPendingApprovals`, whose segments the consumer
464
- // lifts into a sticky footer — that is the treatment for the
465
- // consumer's OWN approvals, and it is not how the hub's chat renders
466
- // a proposal (nor where its preamble expects it).
467
- if (guideOrigin || isResolved) {
431
+ if (isResolved) {
468
432
  accumulator.addApprovalRequest(
469
433
  event.requestId,
470
434
  event.command || '',
@@ -472,7 +436,6 @@ function applyHistoryEvent(
472
436
  approvalType,
473
437
  resolvedStatus ?? 'pending',
474
438
  event.fields,
475
- guideOrigin,
476
439
  );
477
440
  } else {
478
441
  accumulator.trackApprovalRequest(event.requestId, {
@@ -11,7 +11,7 @@
11
11
  * (`title` / `url` / `targetPlatform` / `sourceRepo`). `resolveSourceRowCTA`
12
12
  * then routes that `externalUrl` through the host's `composeContentUrl`
13
13
  * seam and the card is clickable.
14
- * - **Mingo / NATS transport** (`messageData: [{ type: 'GUIDE' | 'TEXT', text }]`)
14
+ * - **Mingo / NATS transport** (`messageData: [{ type: 'TEXT', text }]`)
15
15
  * — the body carries bare `[card://type:id]` markers and NOTHING else.
16
16
  * `chat-message-enhanced.tsx` synthesizes a minimal `{ type, id, title: id,
17
17
  * url: null }` ref so fetch-mode cards can still self-fetch their row, but
@@ -1,4 +1,4 @@
1
- <!-- source-hash: 1b470f4e084b6cb0c7cd286c460838ca -->
1
+ <!-- source-hash: 79a669c46226384775342873e4432c2f -->
2
2
  A client-side React component that renders a list of toggleable push-button options with support for single/multi-select, section grouping, disabled states with tooltips, loading skeletons, and error handling.
3
3
 
4
4
  ## Key Components
@@ -2,7 +2,8 @@
2
2
 
3
3
  import { Check } from 'lucide-react';
4
4
  import type React from 'react';
5
- import { Badge } from '../ui';
5
+ import { useEffect, useRef } from 'react';
6
+ import { Badge, Input } from '../ui';
6
7
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
7
8
 
8
9
  export interface SelectableOption {
@@ -27,6 +28,28 @@ export interface SectionDefinition {
27
28
  description?: string; // Optional description shown under section label
28
29
  }
29
30
 
31
+ /** SERVER-SIDE search: the selector renders the input; the PARENT owns the
32
+ * query state and re-fetches `options` (never a client-side .filter of a
33
+ * pre-fetched list). While `isLoading`, the input stays mounted (typing
34
+ * must not lose focus to a skeleton swap). */
35
+ export interface PushButtonSelectorSearch {
36
+ value: string;
37
+ onChange: (value: string) => void;
38
+ placeholder?: string;
39
+ ariaLabel?: string;
40
+ }
41
+
42
+ /** Lazy paging: the option list becomes a FIXED-HEIGHT scroll viewport with
43
+ * an IntersectionObserver sentinel near the bottom — the selector calls
44
+ * `onLoadMore` as the user scrolls; the parent appends to `options`. */
45
+ export interface PushButtonSelectorLazyLoad {
46
+ hasMore: boolean;
47
+ isFetchingMore: boolean;
48
+ onLoadMore: () => void;
49
+ /** CSS height of the scroll viewport (default '336px' ≈ 6 rows). */
50
+ maxHeight?: string;
51
+ }
52
+
30
53
  interface PushButtonSelectorProps {
31
54
  options: SelectableOption[];
32
55
  selectedIds: string[];
@@ -41,6 +64,14 @@ interface PushButtonSelectorProps {
41
64
  error?: string | null;
42
65
  skeletonCount?: number;
43
66
  sections?: SectionDefinition[]; // Optional sections for grouping options
67
+ /** Built-in server-side search input (see PushButtonSelectorSearch). */
68
+ search?: PushButtonSelectorSearch;
69
+ /** Built-in fixed-height scroll + infinite paging (see PushButtonSelectorLazyLoad). */
70
+ lazyLoad?: PushButtonSelectorLazyLoad;
71
+ /** Rendered inside the list when `options` is empty and not loading. */
72
+ emptyMessage?: React.ReactNode;
73
+ /** Rendered under the list (result counts, hints). */
74
+ footer?: React.ReactNode;
44
75
  }
45
76
 
46
77
  // Skeleton component matching external pattern from announcement-form.tsx
@@ -94,21 +125,82 @@ export function PushButtonSelector({
94
125
  error = null,
95
126
  skeletonCount = 3,
96
127
  sections,
128
+ search,
129
+ lazyLoad,
130
+ emptyMessage,
131
+ footer,
97
132
  }: PushButtonSelectorProps) {
98
- // LOADING STATE
133
+ // Lazy-load sentinel: observe within the scroll viewport so onLoadMore
134
+ // fires as the user approaches the bottom. Hooks run unconditionally
135
+ // (before the early returns) per the rules of hooks. Pass a STABLE
136
+ // onLoadMore (react-query's fetchNextPage is) — an inline arrow re-arms
137
+ // the observer each render (harmless, just wasteful).
138
+ const scrollRef = useRef<HTMLDivElement | null>(null);
139
+ const sentinelRef = useRef<HTMLDivElement | null>(null);
140
+ const lazyHasMore = lazyLoad?.hasMore ?? false;
141
+ const lazyFetching = lazyLoad?.isFetchingMore ?? false;
142
+ const onLoadMore = lazyLoad?.onLoadMore;
143
+ const optionCount = options.length;
144
+ useEffect(() => {
145
+ const sentinel = sentinelRef.current;
146
+ const root = scrollRef.current;
147
+ if (!sentinel || !root || !onLoadMore || !lazyHasMore) return undefined;
148
+ const observer = new IntersectionObserver(
149
+ entries => {
150
+ if (entries.some(e => e.isIntersecting) && !lazyFetching) {
151
+ onLoadMore();
152
+ }
153
+ },
154
+ { root, rootMargin: '80px' },
155
+ );
156
+ observer.observe(sentinel);
157
+ return () => {
158
+ observer.disconnect();
159
+ };
160
+ // optionCount: re-arm when the list grows (the sentinel moves).
161
+ }, [lazyHasMore, lazyFetching, onLoadMore, optionCount]);
162
+
163
+ const searchInput = search ? (
164
+ <Input
165
+ value={search.value}
166
+ onChange={e => search.onChange(e.target.value)}
167
+ placeholder={search.placeholder}
168
+ aria-label={search.ariaLabel ?? search.placeholder ?? 'Search'}
169
+ />
170
+ ) : null;
171
+
172
+ // LOADING STATE — with a search input, the input STAYS mounted (a skeleton
173
+ // swap mid-keystroke would drop focus); only the list skeletons.
99
174
  if (isLoading) {
100
175
  return (
101
176
  <div className={className}>
102
- <PushButtonSelectorSkeleton count={skeletonCount} hasTitle={!!title} />
177
+ {search ? (
178
+ <div className="space-y-4">
179
+ {title && <h3 className="text-ods-text-primary text-h5">{title}</h3>}
180
+ {searchInput}
181
+ <PushButtonSelectorSkeleton count={skeletonCount} />
182
+ </div>
183
+ ) : (
184
+ <PushButtonSelectorSkeleton count={skeletonCount} hasTitle={!!title} />
185
+ )}
103
186
  </div>
104
187
  );
105
188
  }
106
189
 
107
- // ERROR STATE
190
+ // ERROR STATE — same rule as loading: a mounted search input survives so
191
+ // the user can adjust the query to retry.
108
192
  if (error) {
109
193
  return (
110
194
  <div className={className}>
111
- <PushButtonSelectorError message={error} title={title} />
195
+ {search ? (
196
+ <div className="space-y-4">
197
+ {title && <h3 className="text-ods-text-primary text-h5">{title}</h3>}
198
+ {searchInput}
199
+ <PushButtonSelectorError message={error} />
200
+ </div>
201
+ ) : (
202
+ <PushButtonSelectorError message={error} title={title} />
203
+ )}
112
204
  </div>
113
205
  );
114
206
  }
@@ -252,12 +344,44 @@ export function PushButtonSelector({
252
344
  );
253
345
  };
254
346
 
347
+ const listContent = (
348
+ <>
349
+ {options.length === 0 && emptyMessage !== undefined ? (
350
+ <div className="text-ods-text-secondary text-h6">{emptyMessage}</div>
351
+ ) : (
352
+ renderOptionsContent()
353
+ )}
354
+ {lazyLoad ? (
355
+ <>
356
+ <div ref={sentinelRef} />
357
+ {lazyLoad.isFetchingMore ? (
358
+ <div className="py-2 text-center text-ods-text-secondary text-h6">Loading more…</div>
359
+ ) : null}
360
+ </>
361
+ ) : null}
362
+ </>
363
+ );
364
+
255
365
  return (
256
366
  <TooltipProvider delayDuration={150}>
257
367
  <div className={`space-y-4 ${className}`}>
258
368
  {title && <h3 className="text-ods-text-primary text-h5">{title}</h3>}
259
369
 
260
- {renderOptionsContent()}
370
+ {searchInput}
371
+
372
+ {lazyLoad ? (
373
+ <div
374
+ ref={scrollRef}
375
+ className="space-y-4 overflow-y-auto rounded-lg border border-ods-border p-2"
376
+ style={{ height: lazyLoad.maxHeight ?? '336px' }}
377
+ >
378
+ {listContent}
379
+ </div>
380
+ ) : (
381
+ listContent
382
+ )}
383
+
384
+ {footer}
261
385
 
262
386
  {/* Selection Summary */}
263
387
  {selectionSummary && validSelectedIds.length > 0 && (
@@ -210,18 +210,18 @@ export const GuideWithEntityCards: Story = {
210
210
  ) : null,
211
211
  content: [
212
212
  {
213
- type: 'guide',
213
+ type: 'text',
214
214
  text: "Here's how to create a monitoring policy (check) in OpenFrame:\n\n[card://onboarding_guide:first-check]\n\nStep-by-step:\n\n1. Go to Monitoring → Policies → Add Policy\n2. Write your osquery SQL query\n3. Save the policy",
215
215
  },
216
216
  {
217
217
  type: 'text',
218
- text: 'A plain text segment renders its own marker identically:\n\n[card://onboarding_guide:add-devices]',
218
+ text: 'A second text segment renders its own marker identically:\n\n[card://onboarding_guide:add-devices]',
219
219
  },
220
220
  ] satisfies MessageSegment[],
221
221
  },
222
222
  };
223
223
 
224
- /** Every remaining segment type in one message — thinking, guide, tool execution, context compaction, error. */
224
+ /** Every remaining segment type in one message — thinking, tool execution, context compaction, error. */
225
225
  export const AllSegmentTypes: Story = {
226
226
  args: {
227
227
  role: 'assistant',
@@ -233,10 +233,6 @@ export const AllSegmentTypes: Story = {
233
233
  text: 'The user wants Slack installed. Homebrew is present on the endpoint, so `brew install --cask slack` is the cleanest path.',
234
234
  },
235
235
  { type: 'text', text: 'Installing Slack now.' },
236
- {
237
- type: 'guide',
238
- text: "Here's how to create a monitoring policy (check) in OpenFrame:\n\nStep-by-step:\n\n1. Go to Monitoring → Policies → Add Policy\n2. Give it a Name (something descriptive)\n3. Write your osquery SQL query — the query should return rows for devices that fail the check\n4. Set the interval — how often each device runs the check\n5. Save the policy",
239
- },
240
236
  {
241
237
  type: 'tool_execution',
242
238
  data: {