@flamingo-stack/openframe-frontend-core 0.0.222 → 0.0.223

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 (183) hide show
  1. package/dist/{chunk-AIKPR6BE.js → chunk-4VHWSAQR.js} +6069 -5235
  2. package/dist/chunk-4VHWSAQR.js.map +1 -0
  3. package/dist/{chunk-GMNE7P7V.cjs → chunk-4WBVMIKQ.cjs} +26 -26
  4. package/dist/{chunk-GMNE7P7V.cjs.map → chunk-4WBVMIKQ.cjs.map} +1 -1
  5. package/dist/{chunk-QYRV6MKX.js → chunk-53EA4QPZ.js} +13 -7
  6. package/dist/chunk-53EA4QPZ.js.map +1 -0
  7. package/dist/{chunk-4JOWZLEL.cjs → chunk-7TKYFZ3L.cjs} +7 -7
  8. package/dist/{chunk-4JOWZLEL.cjs.map → chunk-7TKYFZ3L.cjs.map} +1 -1
  9. package/dist/{chunk-IZA5WF44.cjs → chunk-ABBU7QOM.cjs} +12 -12
  10. package/dist/{chunk-IZA5WF44.cjs.map → chunk-ABBU7QOM.cjs.map} +1 -1
  11. package/dist/{chunk-KM4KK2A6.cjs → chunk-ENDUMC7Z.cjs} +2558 -1724
  12. package/dist/chunk-ENDUMC7Z.cjs.map +1 -0
  13. package/dist/{chunk-CCSEXVI4.js → chunk-GYYGA24C.js} +2 -2
  14. package/dist/{chunk-RMOHKAHA.js → chunk-ISWWP2BH.js} +3 -3
  15. package/dist/{chunk-QNYH3WUU.cjs → chunk-JUWPBMQM.cjs} +13 -7
  16. package/dist/chunk-JUWPBMQM.cjs.map +1 -0
  17. package/dist/{chunk-A2SFHYVF.cjs → chunk-KXLTWANQ.cjs} +10 -10
  18. package/dist/{chunk-A2SFHYVF.cjs.map → chunk-KXLTWANQ.cjs.map} +1 -1
  19. package/dist/{chunk-ND24XSVT.js → chunk-TA6Z2AKD.js} +3 -3
  20. package/dist/chunk-TA6Z2AKD.js.map +1 -0
  21. package/dist/{chunk-HRIAZQ5D.js → chunk-WOKB6RJ7.js} +3 -3
  22. package/dist/components/chat/chat-archive-page.d.ts.map +1 -1
  23. package/dist/components/chat/chat-composer.d.ts.map +1 -1
  24. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  25. package/dist/components/chat/chat-message-skeleton.d.ts +19 -4
  26. package/dist/components/chat/chat-message-skeleton.d.ts.map +1 -1
  27. package/dist/components/chat/chat-panel-header-mobile.d.ts +24 -0
  28. package/dist/components/chat/chat-panel-header-mobile.d.ts.map +1 -0
  29. package/dist/components/chat/chat-panel-header.d.ts +6 -0
  30. package/dist/components/chat/chat-panel-header.d.ts.map +1 -1
  31. package/dist/components/chat/chat-quick-action-row.d.ts +31 -0
  32. package/dist/components/chat/chat-quick-action-row.d.ts.map +1 -0
  33. package/dist/components/chat/embeddable-chat.d.ts +27 -0
  34. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  35. package/dist/components/chat/entity-cards/dispatch.d.ts +14 -0
  36. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  37. package/dist/components/chat/entity-cards/github-activity-card.d.ts +10 -1
  38. package/dist/components/chat/entity-cards/github-activity-card.d.ts.map +1 -1
  39. package/dist/components/chat/guide-welcome.d.ts.map +1 -1
  40. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +9 -0
  41. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  42. package/dist/components/chat/hooks/use-slash-commands.d.ts +26 -0
  43. package/dist/components/chat/hooks/use-slash-commands.d.ts.map +1 -1
  44. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +12 -1
  45. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  46. package/dist/components/chat/hooks/use-unified-chat.d.ts +12 -0
  47. package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -1
  48. package/dist/components/chat/index.cjs +9 -3
  49. package/dist/components/chat/index.cjs.map +1 -1
  50. package/dist/components/chat/index.js +8 -2
  51. package/dist/components/chat/mingo-chat-history.d.ts +10 -0
  52. package/dist/components/chat/mingo-chat-history.d.ts.map +1 -1
  53. package/dist/components/chat/mingo-info-card.d.ts +63 -0
  54. package/dist/components/chat/mingo-info-card.d.ts.map +1 -0
  55. package/dist/components/chat/mingo-welcome.d.ts +14 -1
  56. package/dist/components/chat/mingo-welcome.d.ts.map +1 -1
  57. package/dist/components/chat/types/chat.types.d.ts +6 -0
  58. package/dist/components/chat/types/chat.types.d.ts.map +1 -1
  59. package/dist/components/chat/types/unified-chat-state.types.d.ts +30 -0
  60. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  61. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  62. package/dist/components/contact/index.cjs +4 -4
  63. package/dist/components/contact/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/index.cjs +110 -104
  67. package/dist/components/index.cjs.map +1 -1
  68. package/dist/components/index.js +36 -30
  69. package/dist/components/index.js.map +1 -1
  70. package/dist/components/navigation/app-layout-drawer.d.ts +7 -0
  71. package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
  72. package/dist/components/navigation/index.cjs +3 -3
  73. package/dist/components/navigation/index.js +2 -2
  74. package/dist/components/onboarding-guides/index.cjs +26 -26
  75. package/dist/components/onboarding-guides/index.js +4 -4
  76. package/dist/components/tickets/index.cjs +67 -67
  77. package/dist/components/tickets/index.js +5 -5
  78. package/dist/components/ui/actions-menu.d.ts +9 -0
  79. package/dist/components/ui/actions-menu.d.ts.map +1 -1
  80. package/dist/components/ui/dropdown-button.d.ts +2 -2
  81. package/dist/components/ui/dropdown-button.d.ts.map +1 -1
  82. package/dist/components/ui/index.cjs +9 -3
  83. package/dist/components/ui/index.cjs.map +1 -1
  84. package/dist/components/ui/index.js +8 -2
  85. package/dist/components/ui/modal-v2.d.ts.map +1 -1
  86. package/dist/components/ui/more-actions-menu.d.ts +13 -0
  87. package/dist/components/ui/more-actions-menu.d.ts.map +1 -1
  88. package/dist/components/ui/square-avatar.d.ts +4 -0
  89. package/dist/components/ui/square-avatar.d.ts.map +1 -1
  90. package/dist/components/ui/textarea.d.ts.map +1 -1
  91. package/dist/hooks/index.cjs +2 -2
  92. package/dist/hooks/index.js +1 -1
  93. package/dist/hooks/ui/use-auto-limit-tags.d.ts +7 -1
  94. package/dist/hooks/ui/use-auto-limit-tags.d.ts.map +1 -1
  95. package/dist/index.cjs +9 -3
  96. package/dist/index.cjs.map +1 -1
  97. package/dist/index.js +8 -2
  98. package/package.json +1 -1
  99. package/src/components/chat/chat-archive-page.tsx +32 -7
  100. package/src/components/chat/chat-composer.tsx +6 -2
  101. package/src/components/chat/chat-message-enhanced.tsx +24 -7
  102. package/src/components/chat/chat-message-list.tsx +1 -1
  103. package/src/components/chat/chat-message-skeleton.tsx +60 -60
  104. package/src/components/chat/chat-panel-header-mobile.tsx +94 -0
  105. package/src/components/chat/chat-panel-header.tsx +78 -50
  106. package/src/components/chat/chat-quick-action-row.tsx +146 -0
  107. package/src/components/chat/embeddable-chat.tsx +140 -45
  108. package/src/components/chat/entity-cards/dispatch.tsx +770 -232
  109. package/src/components/chat/entity-cards/github-activity-card.tsx +9 -3
  110. package/src/components/chat/guide-welcome.tsx +11 -8
  111. package/src/components/chat/hooks/use-nats-chat-adapter.ts +80 -7
  112. package/src/components/chat/hooks/use-slash-commands.ts +39 -0
  113. package/src/components/chat/hooks/use-sse-chat-adapter.ts +49 -45
  114. package/src/components/chat/hooks/use-unified-chat.ts +31 -8
  115. package/src/components/chat/mingo-chat-history.tsx +118 -36
  116. package/src/components/chat/mingo-info-card.tsx +214 -0
  117. package/src/components/chat/mingo-welcome.tsx +89 -36
  118. package/src/components/chat/types/chat.types.ts +4 -0
  119. package/src/components/chat/types/unified-chat-state.types.ts +35 -0
  120. package/src/components/chat/utils/process-historical-messages.ts +8 -0
  121. package/src/components/content-loading-container.tsx +1 -1
  122. package/src/components/empty-state.tsx +3 -3
  123. package/src/components/faq-accordion.tsx +2 -2
  124. package/src/components/features/changelog-manager.tsx +2 -2
  125. package/src/components/features/highlight-config-section.tsx +2 -2
  126. package/src/components/features/og-editor-preview.tsx +3 -3
  127. package/src/components/features/release-media-manager.tsx +2 -2
  128. package/src/components/loading/card-skeleton.tsx +3 -3
  129. package/src/components/loading/device-card-skeleton.tsx +1 -1
  130. package/src/components/loading/margin-report-skeleton.tsx +2 -2
  131. package/src/components/loading/organization-card-skeleton.tsx +1 -1
  132. package/src/components/loading/organization-icon-skeleton.tsx +1 -1
  133. package/src/components/loading/page-layout-skeleton.tsx +9 -9
  134. package/src/components/loading/users-grid-skeleton.tsx +2 -2
  135. package/src/components/logs-list.tsx +1 -1
  136. package/src/components/media-carousel.tsx +1 -1
  137. package/src/components/navigation/app-layout-drawer.tsx +70 -6
  138. package/src/components/open-source-features.tsx +2 -2
  139. package/src/components/ui/actions-menu.tsx +36 -13
  140. package/src/components/ui/allowed-domains-input.tsx +3 -3
  141. package/src/components/ui/cursor-pagination.tsx +1 -1
  142. package/src/components/ui/dropdown-button.tsx +6 -6
  143. package/src/components/ui/info-row.tsx +2 -2
  144. package/src/components/ui/modal-v2.tsx +8 -1
  145. package/src/components/ui/more-actions-menu.tsx +13 -0
  146. package/src/components/ui/square-avatar.tsx +6 -1
  147. package/src/components/ui/tags-input.tsx +1 -1
  148. package/src/components/ui/textarea.tsx +18 -4
  149. package/src/components/vendor-icon.tsx +1 -1
  150. package/src/components/vendor-tag.tsx +1 -1
  151. package/src/components/why-it-matters.tsx +1 -1
  152. package/src/hooks/ui/use-auto-limit-tags.ts +22 -8
  153. package/src/stories/ActionsMenu.stories.tsx +256 -0
  154. package/src/stories/AppLayoutSidebar.stories.tsx +6 -0
  155. package/src/stories/Board.stories.tsx +1 -1
  156. package/src/stories/ChatMingoInfoCards.stories.tsx +748 -0
  157. package/src/stories/ChatTicketList.stories.tsx +1 -1
  158. package/src/stories/DataTable.stories.tsx +17 -13
  159. package/src/stories/EmbeddableChat.stories.tsx +233 -1
  160. package/src/stories/MingoInfoCard.stories.tsx +193 -0
  161. package/src/stories/MingoOnboardingCard.stories.tsx +1 -1
  162. package/src/stories/Modal.stories.tsx +1 -0
  163. package/src/stories/NotificationDrawer.stories.tsx +14 -3
  164. package/src/stories/PageActions.stories.tsx +4 -4
  165. package/src/stories/PageContainer.stories.tsx +5 -5
  166. package/src/stories/PageLayout.stories.tsx +1 -1
  167. package/src/stories/TabSelector.stories.tsx +2 -2
  168. package/src/stories/Tag.stories.tsx +10 -5
  169. package/src/stories/TicketInfoSection.stories.tsx +23 -11
  170. package/src/stories/Toaster.stories.tsx +5 -5
  171. package/src/stories/__fixtures__/chat-card-decorator.tsx +44 -0
  172. package/dist/chunk-AIKPR6BE.js.map +0 -1
  173. package/dist/chunk-KM4KK2A6.cjs.map +0 -1
  174. package/dist/chunk-ND24XSVT.js.map +0 -1
  175. package/dist/chunk-QNYH3WUU.cjs.map +0 -1
  176. package/dist/chunk-QYRV6MKX.js.map +0 -1
  177. package/src/stories/.TicketDescriptionViewer.stories.md +0 -39
  178. package/src/stories/.TicketKnowledgeBaseList.stories.md +0 -36
  179. package/src/stories/TicketDescriptionViewer.stories.tsx +0 -70
  180. package/src/stories/TicketKnowledgeBaseList.stories.tsx +0 -51
  181. /package/dist/{chunk-CCSEXVI4.js.map → chunk-GYYGA24C.js.map} +0 -0
  182. /package/dist/{chunk-RMOHKAHA.js.map → chunk-ISWWP2BH.js.map} +0 -0
  183. /package/dist/{chunk-HRIAZQ5D.js.map → chunk-WOKB6RJ7.js.map} +0 -0
@@ -67,7 +67,7 @@ function kindIcon(kind: GitHubActivityKind, className = 'h-4 w-4') {
67
67
  }
68
68
  }
69
69
 
70
- function kindLabel(kind: GitHubActivityKind): string {
70
+ export function kindLabel(kind: GitHubActivityKind): string {
71
71
  switch (kind) {
72
72
  case 'pull_request': return 'PR'
73
73
  case 'pr_review': return 'Review'
@@ -76,14 +76,14 @@ function kindLabel(kind: GitHubActivityKind): string {
76
76
  }
77
77
  }
78
78
 
79
- function formatActivityId(id: string, kind: GitHubActivityKind): string {
79
+ export function formatActivityId(id: string, kind: GitHubActivityKind): string {
80
80
  if (!id) return ''
81
81
  if (kind === 'pull_request' || kind === 'pr_review') return `#${id}`
82
82
  if (/^[0-9a-f]{40}$/i.test(id)) return id.slice(0, 7)
83
83
  return id.length > 12 ? id.slice(0, 12) : id
84
84
  }
85
85
 
86
- function parseGithubTitle(title: string, kind: GitHubActivityKind): { display: string; reviewState: PrReviewState | null } {
86
+ export function parseGithubTitle(title: string, kind: GitHubActivityKind): { display: string; reviewState: PrReviewState | null } {
87
87
  if (!title) return { display: '', reviewState: null }
88
88
 
89
89
  // Split prefix-tag from rest by hand instead of one giant regex.
@@ -150,6 +150,12 @@ const REVIEW_STATE_STYLE: Record<PrReviewState, { label: string; className: stri
150
150
  },
151
151
  }
152
152
 
153
+ /** Short review-state label ("APPROVED" → "Approved") shared with the
154
+ * MingoInfoCard dispatch mapping so both surfaces read identically. */
155
+ export function reviewStateLabel(state: PrReviewState): string {
156
+ return (REVIEW_STATE_STYLE[state] ?? REVIEW_STATE_STYLE.COMMENTED).label
157
+ }
158
+
153
159
  function ReviewStateBadge({ state, className = '' }: { state: PrReviewState; className?: string }) {
154
160
  const { label, className: paint } = REVIEW_STATE_STYLE[state] ?? REVIEW_STATE_STYLE.COMMENTED
155
161
  return (
@@ -4,7 +4,7 @@ import * as React from 'react'
4
4
  import { cn } from '../../utils/cn'
5
5
  import { MingoIcon } from '../icons'
6
6
  import { Tag } from '../ui/tag'
7
- import { MoreActionsMenu } from '../ui/more-actions-menu'
7
+ import { ActionsMenuDropdown } from '../ui/actions-menu'
8
8
  import { Ellipsis01Icon } from '../icons-v2-generated'
9
9
 
10
10
  // =============================================================================
@@ -174,14 +174,17 @@ export function GuideWelcome({
174
174
  </button>
175
175
  ))}
176
176
  {overflowActions.length > 0 && (
177
- <MoreActionsMenu
178
- ariaLabel="More quick actions"
177
+ <ActionsMenuDropdown
178
+ triggerAriaLabel="More quick actions"
179
179
  onCloseAutoFocus={(e) => e.preventDefault()}
180
- items={overflowActions.map((action) => ({
181
- label: action.label,
182
- onClick: () => onQuickAction?.(action),
183
- }))}
184
- trigger={
180
+ groups={[{
181
+ items: overflowActions.map((action) => ({
182
+ id: action.id,
183
+ label: action.label,
184
+ onClick: () => onQuickAction?.(action),
185
+ })),
186
+ }]}
187
+ customTrigger={
185
188
  <button
186
189
  type="button"
187
190
  aria-label="More quick actions"
@@ -307,6 +307,16 @@ export interface UseNatsChatAdapterConfig {
307
307
 
308
308
  /** Default page size for message history pagination. Defaults to 50. */
309
309
  messagesPageSize?: number
310
+
311
+ /**
312
+ * Baseline model display for the composer's `<ModelDisplay>` — used as the
313
+ * empty-state fallback before any streaming `metadata` frame arrives (e.g.
314
+ * a brand-new chat). The host typically sources these from its AI-config
315
+ * endpoint. Live `metadata` frames refine them per-turn. NATS-only; SSE
316
+ * (guide) derives its own model from the stream.
317
+ */
318
+ modelProvider?: string | null
319
+ modelLabel?: string | null
310
320
  }
311
321
 
312
322
  /**
@@ -431,6 +441,8 @@ export function useNatsChatAdapter(
431
441
  chatTypeFilter,
432
442
  dialogsPageSize = 20,
433
443
  messagesPageSize = 50,
444
+ modelProvider = null,
445
+ modelLabel = null,
434
446
  } = config
435
447
 
436
448
  // ─── Active dialog id resolution ──────────────────────────────────────────
@@ -467,6 +479,7 @@ export function useNatsChatAdapter(
467
479
  const [dialogs, setDialogs] = useState<DialogItem[]>([])
468
480
  const [dialogsNextCursor, setDialogsNextCursor] = useState<string | null>(null)
469
481
  const [isDialogsLoading, setIsDialogsLoading] = useState<boolean>(false)
482
+ const [dialogsError, setDialogsError] = useState<boolean>(false)
470
483
  const [isCreatingDialog, setIsCreatingDialog] = useState<boolean>(false)
471
484
 
472
485
  // ─── Message-history pagination ───────────────────────────────────────────
@@ -480,6 +493,15 @@ export function useNatsChatAdapter(
480
493
  null,
481
494
  )
482
495
 
496
+ // ─── Live model metadata (from streaming `metadata` frames) ───────────────
497
+ // Refines the composer's model badge per-turn. Falls back to the config
498
+ // baseline (`modelProvider`/`modelLabel`) until the first frame lands.
499
+ const [liveModel, setLiveModel] = useState<{
500
+ provider: string | null
501
+ modelLabel: string | null
502
+ contextWindowMaxTokens: number | null
503
+ } | null>(null)
504
+
483
505
  // ─── Connection state ─────────────────────────────────────────────────────
484
506
 
485
507
  const [connectionState, setConnectionState] =
@@ -554,12 +576,29 @@ export function useNatsChatAdapter(
554
576
  onSegmentsUpdate: (segments: MessageSegment[]) => void
555
577
  onStreamStart: () => void
556
578
  onStreamEnd: () => void
579
+ onTokenUsage: (data: DialogTokenUsage) => void
580
+ onMetadata: (meta: {
581
+ modelDisplayName?: string
582
+ modelName: string
583
+ providerName: string
584
+ contextWindow: number
585
+ }) => void
557
586
  }> = useRef({
558
587
  onSegmentsUpdate: (segments: MessageSegment[]) => {
559
588
  setMessages((prev) => updateTrailingAssistant(prev, segments))
560
589
  },
561
590
  onStreamStart: () => setStreamingPhase('streaming'),
562
591
  onStreamEnd: () => setStreamingPhase('idle'),
592
+ // Live cumulative token counter for the active dialog — drives the
593
+ // composer's "X / Y tokens used" tail as the assistant streams.
594
+ onTokenUsage: (data: DialogTokenUsage) => setDialogTokenUsage(data),
595
+ // Per-turn model badge (provider/label/context window).
596
+ onMetadata: (meta) =>
597
+ setLiveModel({
598
+ provider: meta.providerName || null,
599
+ modelLabel: meta.modelDisplayName || meta.modelName || null,
600
+ contextWindowMaxTokens: meta.contextWindow || null,
601
+ }),
563
602
  })
564
603
 
565
604
  // Real-time chunk → segment processor. Approval handlers route through
@@ -570,6 +609,8 @@ export function useNatsChatAdapter(
570
609
  onSegmentsUpdate: (segments) => callbacksRef.current.onSegmentsUpdate(segments),
571
610
  onStreamStart: () => callbacksRef.current.onStreamStart(),
572
611
  onStreamEnd: () => callbacksRef.current.onStreamEnd(),
612
+ onTokenUsage: (data) => callbacksRef.current.onTokenUsage(data),
613
+ onMetadata: (meta) => callbacksRef.current.onMetadata(meta),
573
614
  onApprove: accumApprove,
574
615
  onReject: accumReject,
575
616
  },
@@ -679,6 +720,9 @@ export function useNatsChatAdapter(
679
720
  setMessages([])
680
721
  setMessagesNextCursor(null)
681
722
  setDialogTokenUsage(null)
723
+ // Clear per-dialog live model metadata too — otherwise the composer briefly
724
+ // shows the previous dialog's provider/model until the next chunk streams.
725
+ setLiveModel(null)
682
726
  setStreamingPhase('idle')
683
727
  // No load runs when there's no active dialog, so clear the flag here;
684
728
  // otherwise the superseded load's guarded `finally` leaves it stuck on.
@@ -740,10 +784,16 @@ export function useNatsChatAdapter(
740
784
 
741
785
  // ─── Dialog list management (managed-dialog mode) ─────────────────────────
742
786
 
787
+ // Forward-declared so `loadDialogsPage` can reset the initial-load guard on
788
+ // a first-page failure (enabling retry / auto-retry on re-activation).
789
+ const initialDialogsLoadedRef = useRef(false)
790
+
743
791
  const loadDialogsPage = useCallback(
744
792
  async (cursor?: string): Promise<void> => {
745
793
  if (!fetchDialogs) return
746
794
  setIsDialogsLoading(true)
795
+ // Clear a prior first-page error when (re)loading the first page.
796
+ if (cursor === undefined) setDialogsError(false)
747
797
  try {
748
798
  const result = await fetchDialogs({
749
799
  cursor,
@@ -757,6 +807,13 @@ export function useNatsChatAdapter(
757
807
  }
758
808
  } catch (err) {
759
809
  console.error('[useNatsChatAdapter] fetchDialogs failed:', err)
810
+ // Only the FIRST page failing is a "can't show the list" error — a
811
+ // pagination failure keeps the already-loaded list intact. Flag it and
812
+ // release the initial-load guard so a retry (or re-activation) re-runs.
813
+ if (cursor === undefined) {
814
+ setDialogsError(true)
815
+ initialDialogsLoadedRef.current = false
816
+ }
760
817
  } finally {
761
818
  setIsDialogsLoading(false)
762
819
  }
@@ -764,8 +821,12 @@ export function useNatsChatAdapter(
764
821
  [fetchDialogs, dialogsPageSize],
765
822
  )
766
823
 
824
+ // Retry the initial dialog-list load after a failure.
825
+ const reloadDialogs = useCallback(() => {
826
+ void loadDialogsPage()
827
+ }, [loadDialogsPage])
828
+
767
829
  // Initial dialog list load.
768
- const initialDialogsLoadedRef = useRef(false)
769
830
  useEffect(() => {
770
831
  if (!fetchDialogs) return
771
832
  if (!active) return
@@ -973,12 +1034,17 @@ export function useNatsChatAdapter(
973
1034
  clearMessages,
974
1035
  discussRef,
975
1036
  displayRef,
976
- // SSE-only telemetry null in NATS mode.
977
- currentProvider: null,
978
- currentModelLabel: null,
979
- currentContextWindowMaxTokens: null,
980
- currentInputTokens: null,
981
- currentOutputTokens: null,
1037
+ // Model badge + token usage for the composer's `<ModelDisplay>`.
1038
+ // Model: live `metadata` frame → config baseline (host AI-config).
1039
+ // Tokens: live `token_usage` frame / dialog snapshot (`dialogTokenUsage`).
1040
+ // contextWindow uses the dialog's `contextSize` (the "X / Y" denominator,
1041
+ // matching the /mingo page). cacheHitRate/breakdown are SSE-only → null.
1042
+ currentProvider: liveModel?.provider ?? modelProvider ?? null,
1043
+ currentModelLabel: liveModel?.modelLabel ?? modelLabel ?? null,
1044
+ currentContextWindowMaxTokens:
1045
+ dialogTokenUsage?.contextSize ?? liveModel?.contextWindowMaxTokens ?? null,
1046
+ currentInputTokens: dialogTokenUsage?.inputTokensSize ?? null,
1047
+ currentOutputTokens: dialogTokenUsage?.outputTokensSize ?? null,
982
1048
  currentCacheHitRatePct: null,
983
1049
  currentUsageBreakdown: null,
984
1050
  // Dialog management
@@ -990,6 +1056,8 @@ export function useNatsChatAdapter(
990
1056
  renameDialog,
991
1057
  archiveDialog,
992
1058
  isDialogsLoading: isDialogsLoading || isCreatingDialog,
1059
+ dialogsError,
1060
+ reloadDialogs,
993
1061
  isMessagesLoading,
994
1062
  hasMoreDialogs,
995
1063
  loadMoreDialogs,
@@ -1020,6 +1088,8 @@ export function useNatsChatAdapter(
1020
1088
  renameDialog,
1021
1089
  archiveDialog,
1022
1090
  isDialogsLoading,
1091
+ dialogsError,
1092
+ reloadDialogs,
1023
1093
  isCreatingDialog,
1024
1094
  isMessagesLoading,
1025
1095
  hasMoreDialogs,
@@ -1029,6 +1099,9 @@ export function useNatsChatAdapter(
1029
1099
  approveRequest,
1030
1100
  rejectRequest,
1031
1101
  dialogTokenUsage,
1102
+ liveModel,
1103
+ modelProvider,
1104
+ modelLabel,
1032
1105
  connectionState,
1033
1106
  ],
1034
1107
  )
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import { useEffect, useState } from "react";
17
+ import { useQuery } from "@tanstack/react-query";
17
18
  import { embedAuthedFetch } from "../../../utils/embed-authed-fetch";
18
19
  import type {
19
20
  SlashCommandActionId,
@@ -113,3 +114,41 @@ export function useSlashCommands(
113
114
 
114
115
  return { commands, loading };
115
116
  }
117
+
118
+ /**
119
+ * The full slash-command registry (empty-prefix fetch), shared via
120
+ * react-query so every consumer in one chat surface dedupes to a SINGLE
121
+ * network request.
122
+ *
123
+ * Keyed on `commandsUrl` ALONE — the request sends no `source` param (the
124
+ * hub resolves source server-side), so the URL fully determines the
125
+ * response. Both the onboarding-card list in `<EmbeddableChat>` and the
126
+ * SSE adapter's `displayRef` table lookup read from this one cache entry
127
+ * instead of each firing their own `fetchSlashCommands('')`.
128
+ *
129
+ * `staleTime`/`gcTime: Infinity` → static per session, so toggling modes
130
+ * or remounting the (close-on-unmount) drawer reads from cache, never
131
+ * refetches.
132
+ *
133
+ * `enabled` gates the live fetch: pass `activeMode === 'guide'` (or the
134
+ * SSE adapter's `active` flag) so opening the panel in Mingo mode does
135
+ * NOT hit the commands endpoint at all.
136
+ */
137
+ export function useSlashCommandRegistry(
138
+ commandsUrl: string,
139
+ options?: { enabled?: boolean },
140
+ ): { commands: SlashCommandSummary[]; loading: boolean; loaded: boolean } {
141
+ const query = useQuery({
142
+ queryKey: ["chat-slash-commands", commandsUrl],
143
+ queryFn: ({ signal }) => fetchSlashCommands("", signal, commandsUrl),
144
+ enabled: options?.enabled ?? true,
145
+ staleTime: Infinity,
146
+ gcTime: Infinity,
147
+ });
148
+ return {
149
+ commands: query.data ?? [],
150
+ loading: query.isLoading,
151
+ // "Loaded" = the fetch has settled, or it isn't running (disabled).
152
+ loaded: !query.isLoading,
153
+ };
154
+ }
@@ -45,7 +45,7 @@ import { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'
45
45
  import type { ChatRef } from '../chat-ref.types'
46
46
  import { buildChatRefKey } from '../types/chat.types'
47
47
  import type { MessageSegment } from '../types/message.types'
48
- import { fetchSlashCommands, type SlashCommandSummary } from './use-slash-commands'
48
+ import { useSlashCommandRegistry, type SlashCommandSummary } from './use-slash-commands'
49
49
  import { getChatProxyAuth } from '../utils/chat-proxy-auth-storage'
50
50
  import { chatAuthedFetch } from '../utils/chat-authed-fetch'
51
51
  import { parseScrollAnchor, type ScrollAnchor } from '../utils/scroll-anchor'
@@ -170,6 +170,18 @@ export interface UseSseChatAdapterOptions {
170
170
  tableIdForDocumentType?: (documentType: string) => string | null
171
171
  }
172
172
 
173
+ export interface UseSseChatAdapterRuntimeOptions {
174
+ /**
175
+ * When `false` the adapter skips its background slash-command registry
176
+ * fetch (used to hydrate the `displayRef` table lookup). Mirrors the
177
+ * NATS adapter's `active` gate: `useUnifiedChat` passes `false` while
178
+ * Guide mode is configured-but-not-active, so opening the panel in
179
+ * Mingo mode does NOT hit the commands endpoint. Default `true` so
180
+ * standalone callers keep the eager prefetch.
181
+ */
182
+ active?: boolean
183
+ }
184
+
173
185
  // =============================================================================
174
186
  // Internal helpers
175
187
  // =============================================================================
@@ -679,7 +691,9 @@ function savePersistedChat(source: DocSource, state: PersistedChatState) {
679
691
  */
680
692
  export function useSseChatAdapter(
681
693
  options?: UseSseChatAdapterOptions,
694
+ runtimeOptions: UseSseChatAdapterRuntimeOptions = {},
682
695
  ): UnifiedChatState {
696
+ const { active = true } = runtimeOptions
683
697
  // Chat-specific code REQUIRES a runtime — the lib's `<HubRuntimeProvider>`
684
698
  // (hub) / embedder's provider must wrap the tree.
685
699
  const runtime = useRequiredChatRuntime()
@@ -742,54 +756,41 @@ export function useSseChatAdapter(
742
756
  ],
743
757
  )
744
758
 
745
- // Per-source tableId → slash-command-id lookup. Hydrated from the
746
- // commands endpoint on mount. Used by `displayRef` to translate an
759
+ // Per-source tableId → slash-command-id lookup, derived from the shared
760
+ // slash-command registry. Used by `displayRef` to translate an
747
761
  // inline-card's Display click into a `/<cmd> display "<value>"`
748
762
  // invocation, picking the canonical command for the row's RAG table.
749
- const [cmdIdByTableId, setCmdIdByTableId] = useState<Map<string, string>>(
750
- () => new Map(),
751
- )
763
+ //
764
+ // Reads from the SAME react-query cache entry as `<EmbeddableChat>`'s
765
+ // onboarding-card list (keyed on `commandsUrl`), so Guide mode fetches
766
+ // `commands` ONCE. Gated on `active` so a Mingo-only panel — where this
767
+ // adapter is mounted but idle — never hits the endpoint.
752
768
  const commandsUrl = runtime.endpoints.commandsUrl
753
- useEffect(() => {
754
- let cancelled = false
755
- const ctrl = new AbortController()
756
- fetchSlashCommands('', ctrl.signal, commandsUrl)
757
- .then((commands) => {
758
- if (cancelled) return
759
- const buckets = new Map<string, SlashCommandSummary[]>()
760
- for (const cmd of commands) {
761
- if (!cmd.primarySourceId) continue
762
- const arr = buckets.get(cmd.primarySourceId) ?? []
763
- arr.push(cmd)
764
- buckets.set(cmd.primarySourceId, arr)
765
- }
766
- const map = new Map<string, string>()
767
- for (const [tableId, cmds] of buckets) {
768
- const display = cmds.find((c) => c.actions.some((a) => a.id === 'display'))
769
- const picked =
770
- display ??
771
- [...cmds].sort((a, b) => {
772
- const ao = a.displayOrder ?? Number.POSITIVE_INFINITY
773
- const bo = b.displayOrder ?? Number.POSITIVE_INFINITY
774
- return ao - bo
775
- })[0]
776
- if (picked) map.set(tableId, picked.id)
777
- }
778
- setCmdIdByTableId(map)
779
- })
780
- .catch((err) => {
781
- if (!cancelled && (err as Error)?.name !== 'AbortError') {
782
- console.warn(
783
- '[useSseChatAdapter] failed to fetch slash commands for displayRef:',
784
- err,
785
- )
786
- }
787
- })
788
- return () => {
789
- cancelled = true
790
- ctrl.abort()
769
+ const { commands: slashCommands } = useSlashCommandRegistry(commandsUrl, {
770
+ enabled: active,
771
+ })
772
+ const cmdIdByTableId = useMemo(() => {
773
+ const buckets = new Map<string, SlashCommandSummary[]>()
774
+ for (const cmd of slashCommands) {
775
+ if (!cmd.primarySourceId) continue
776
+ const arr = buckets.get(cmd.primarySourceId) ?? []
777
+ arr.push(cmd)
778
+ buckets.set(cmd.primarySourceId, arr)
779
+ }
780
+ const map = new Map<string, string>()
781
+ for (const [tableId, cmds] of buckets) {
782
+ const display = cmds.find((c) => c.actions.some((a) => a.id === 'display'))
783
+ const picked =
784
+ display ??
785
+ [...cmds].sort((a, b) => {
786
+ const ao = a.displayOrder ?? Number.POSITIVE_INFINITY
787
+ const bo = b.displayOrder ?? Number.POSITIVE_INFINITY
788
+ return ao - bo
789
+ })[0]
790
+ if (picked) map.set(tableId, picked.id)
791
791
  }
792
- }, [source, commandsUrl])
792
+ return map
793
+ }, [slashCommands])
793
794
 
794
795
  // Persist on every messages change. Sources + sendCount live in refs,
795
796
  // so we read their current values at write time.
@@ -1074,6 +1075,9 @@ export function useSseChatAdapter(
1074
1075
  renameDialog: noopRenameDialog,
1075
1076
  archiveDialog: noopArchiveDialog,
1076
1077
  isDialogsLoading: false,
1078
+ // SSE/guide has no server-side dialog list — never errors, nothing to retry.
1079
+ dialogsError: false,
1080
+ reloadDialogs: noopAsync,
1077
1081
  isMessagesLoading: false,
1078
1082
  hasMoreDialogs: false,
1079
1083
  loadMoreDialogs: noopAsync,
@@ -60,6 +60,19 @@ export interface UseUnifiedChatModes {
60
60
  export interface UseUnifiedChatOptions {
61
61
  modes: UseUnifiedChatModes
62
62
  activeMode: ChatMode
63
+
64
+ /**
65
+ * Pre-built Mingo-mode state, supplied by the host instead of letting
66
+ * the internal `useNatsChatAdapter` own it. When provided AND the active
67
+ * mode is `'mingo'`, this object is used verbatim as the active state and
68
+ * the internal NATS adapter stays idle (the host should also omit
69
+ * `modes.mingo` so no live subscription is opened here).
70
+ *
71
+ * This is the seam that lets a host keep chat data/streaming in its own
72
+ * store + cache (so it survives the panel unmounting) rather than in this
73
+ * hook's local React state. Guide mode is unaffected.
74
+ */
75
+ mingoStateOverride?: UnifiedChatState
63
76
  }
64
77
 
65
78
  // =============================================================================
@@ -88,7 +101,7 @@ function createDisabledNatsConfig(): UseNatsChatAdapterConfig {
88
101
  export function useUnifiedChat(
89
102
  options: UseUnifiedChatOptions,
90
103
  ): UnifiedChatState {
91
- const { modes, activeMode } = options
104
+ const { modes, activeMode, mingoStateOverride } = options
92
105
 
93
106
  // The mingo config object identity matters — `useNatsChatAdapter`
94
107
  // wires its `dialogId`/url/publish into deps. Stabilise the disabled
@@ -105,18 +118,21 @@ export function useUnifiedChat(
105
118
  const sseActive = activeMode === 'guide' && modes.guide !== undefined
106
119
  const natsActive = activeMode === 'mingo' && modes.mingo !== undefined
107
120
 
108
- const sseState = useSseChatAdapter(modes.guide ?? EMPTY_SSE_OPTIONS)
121
+ const sseState = useSseChatAdapter(modes.guide ?? EMPTY_SSE_OPTIONS, {
122
+ active: sseActive,
123
+ })
109
124
  const natsState = useNatsChatAdapter(
110
125
  modes.mingo ?? disabledNatsRef.current,
111
126
  { active: natsActive },
112
127
  )
113
128
 
114
- // Mark `sseActive` as used SSE adapter is request-response so it
115
- // doesn't currently consume an `active` gate. Keeping the flag here
116
- // documents intent + leaves the door open for future SSE backgrounding.
117
- void sseActive
118
-
119
- const activeState = activeMode === 'guide' ? sseState : natsState
129
+ // Host-injected Mingo state wins when present: the internal `natsState`
130
+ // still runs (rules of hooks) but is idle (no `modes.mingo` not active),
131
+ // and we hand the host's store-backed state straight through as active.
132
+ const activeState =
133
+ activeMode === 'guide'
134
+ ? sseState
135
+ : (mingoStateOverride ?? natsState)
120
136
 
121
137
  // Re-wrap so the returned identity is stable for the active state.
122
138
  // Consumers shouldn't see the inactive adapter's state at all.
@@ -169,6 +185,10 @@ export function useUnifiedChat(
169
185
  () => activeState.loadMoreDialogs(),
170
186
  [activeState],
171
187
  )
188
+ const reloadDialogs = useCallback(
189
+ () => activeState.reloadDialogs(),
190
+ [activeState],
191
+ )
172
192
  const loadMoreMessages = useCallback(
173
193
  () => activeState.loadMoreMessages(),
174
194
  [activeState],
@@ -209,6 +229,8 @@ export function useUnifiedChat(
209
229
  renameDialog,
210
230
  archiveDialog,
211
231
  isDialogsLoading: activeState.isDialogsLoading,
232
+ dialogsError: activeState.dialogsError,
233
+ reloadDialogs,
212
234
  isMessagesLoading: activeState.isMessagesLoading,
213
235
  hasMoreDialogs: activeState.hasMoreDialogs,
214
236
  loadMoreDialogs,
@@ -234,6 +256,7 @@ export function useUnifiedChat(
234
256
  renameDialog,
235
257
  archiveDialog,
236
258
  loadMoreDialogs,
259
+ reloadDialogs,
237
260
  loadMoreMessages,
238
261
  approveRequest,
239
262
  rejectRequest,