@flamingo-stack/openframe-frontend-core 0.0.222 → 0.0.223-snapshot.20260604201541

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 (188) hide show
  1. package/dist/{chunk-QYRV6MKX.js → chunk-53EA4QPZ.js} +13 -7
  2. package/dist/chunk-53EA4QPZ.js.map +1 -0
  3. package/dist/{chunk-GMNE7P7V.cjs → chunk-7JGR67H2.cjs} +26 -26
  4. package/dist/{chunk-GMNE7P7V.cjs.map → chunk-7JGR67H2.cjs.map} +1 -1
  5. package/dist/{chunk-KM4KK2A6.cjs → chunk-AENTBOSN.cjs} +2656 -1774
  6. package/dist/chunk-AENTBOSN.cjs.map +1 -0
  7. package/dist/{chunk-AIKPR6BE.js → chunk-BNYPQCKL.js} +6179 -5297
  8. package/dist/chunk-BNYPQCKL.js.map +1 -0
  9. package/dist/{chunk-QNYH3WUU.cjs → chunk-JUWPBMQM.cjs} +13 -7
  10. package/dist/chunk-JUWPBMQM.cjs.map +1 -0
  11. package/dist/{chunk-CCSEXVI4.js → chunk-LEHEHEVW.js} +2 -2
  12. package/dist/{chunk-ND24XSVT.js → chunk-OAOLVEYW.js} +3 -3
  13. package/dist/chunk-OAOLVEYW.js.map +1 -0
  14. package/dist/{chunk-4JOWZLEL.cjs → chunk-SZCTY3YF.cjs} +7 -7
  15. package/dist/{chunk-4JOWZLEL.cjs.map → chunk-SZCTY3YF.cjs.map} +1 -1
  16. package/dist/{chunk-RMOHKAHA.js → chunk-V6ME2VMU.js} +3 -3
  17. package/dist/{chunk-IZA5WF44.cjs → chunk-WIAQRCJ5.cjs} +12 -12
  18. package/dist/{chunk-IZA5WF44.cjs.map → chunk-WIAQRCJ5.cjs.map} +1 -1
  19. package/dist/{chunk-HRIAZQ5D.js → chunk-YEOM52ZR.js} +3 -3
  20. package/dist/{chunk-A2SFHYVF.cjs → chunk-ZCHTRQBO.cjs} +10 -10
  21. package/dist/{chunk-A2SFHYVF.cjs.map → chunk-ZCHTRQBO.cjs.map} +1 -1
  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-chat-card-item.d.ts.map +1 -1
  41. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +9 -0
  42. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  43. package/dist/components/chat/hooks/use-slash-commands.d.ts +26 -0
  44. package/dist/components/chat/hooks/use-slash-commands.d.ts.map +1 -1
  45. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +12 -1
  46. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  47. package/dist/components/chat/hooks/use-unified-chat.d.ts +12 -0
  48. package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -1
  49. package/dist/components/chat/index.cjs +9 -3
  50. package/dist/components/chat/index.cjs.map +1 -1
  51. package/dist/components/chat/index.js +8 -2
  52. package/dist/components/chat/mingo-chat-history.d.ts +10 -0
  53. package/dist/components/chat/mingo-chat-history.d.ts.map +1 -1
  54. package/dist/components/chat/mingo-info-card.d.ts +63 -0
  55. package/dist/components/chat/mingo-info-card.d.ts.map +1 -0
  56. package/dist/components/chat/mingo-welcome.d.ts +14 -1
  57. package/dist/components/chat/mingo-welcome.d.ts.map +1 -1
  58. package/dist/components/chat/types/chat.types.d.ts +6 -0
  59. package/dist/components/chat/types/chat.types.d.ts.map +1 -1
  60. package/dist/components/chat/types/unified-chat-state.types.d.ts +40 -0
  61. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  62. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  63. package/dist/components/contact/index.cjs +4 -4
  64. package/dist/components/contact/index.js +3 -3
  65. package/dist/components/features/index.cjs +3 -3
  66. package/dist/components/features/index.js +2 -2
  67. package/dist/components/index.cjs +110 -104
  68. package/dist/components/index.cjs.map +1 -1
  69. package/dist/components/index.js +36 -30
  70. package/dist/components/index.js.map +1 -1
  71. package/dist/components/navigation/app-layout-drawer.d.ts +7 -0
  72. package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
  73. package/dist/components/navigation/index.cjs +3 -3
  74. package/dist/components/navigation/index.js +2 -2
  75. package/dist/components/onboarding-guides/index.cjs +26 -26
  76. package/dist/components/onboarding-guides/index.js +4 -4
  77. package/dist/components/tickets/index.cjs +67 -67
  78. package/dist/components/tickets/index.js +5 -5
  79. package/dist/components/ui/actions-menu.d.ts +9 -0
  80. package/dist/components/ui/actions-menu.d.ts.map +1 -1
  81. package/dist/components/ui/dropdown-button.d.ts +2 -2
  82. package/dist/components/ui/dropdown-button.d.ts.map +1 -1
  83. package/dist/components/ui/index.cjs +9 -3
  84. package/dist/components/ui/index.cjs.map +1 -1
  85. package/dist/components/ui/index.js +8 -2
  86. package/dist/components/ui/modal-v2.d.ts.map +1 -1
  87. package/dist/components/ui/more-actions-menu.d.ts +13 -0
  88. package/dist/components/ui/more-actions-menu.d.ts.map +1 -1
  89. package/dist/components/ui/simple-markdown-renderer.d.ts +9 -1
  90. package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
  91. package/dist/components/ui/square-avatar.d.ts +4 -0
  92. package/dist/components/ui/square-avatar.d.ts.map +1 -1
  93. package/dist/components/ui/textarea.d.ts.map +1 -1
  94. package/dist/hooks/index.cjs +2 -2
  95. package/dist/hooks/index.js +1 -1
  96. package/dist/hooks/ui/use-auto-limit-tags.d.ts +7 -1
  97. package/dist/hooks/ui/use-auto-limit-tags.d.ts.map +1 -1
  98. package/dist/index.cjs +9 -3
  99. package/dist/index.cjs.map +1 -1
  100. package/dist/index.js +8 -2
  101. package/package.json +1 -1
  102. package/src/components/chat/chat-archive-page.tsx +32 -7
  103. package/src/components/chat/chat-composer.tsx +6 -2
  104. package/src/components/chat/chat-message-enhanced.tsx +91 -21
  105. package/src/components/chat/chat-message-list.tsx +1 -1
  106. package/src/components/chat/chat-message-skeleton.tsx +60 -60
  107. package/src/components/chat/chat-panel-header-mobile.tsx +94 -0
  108. package/src/components/chat/chat-panel-header.tsx +78 -50
  109. package/src/components/chat/chat-quick-action-row.tsx +146 -0
  110. package/src/components/chat/embeddable-chat.tsx +185 -52
  111. package/src/components/chat/entity-cards/dispatch.tsx +770 -232
  112. package/src/components/chat/entity-cards/github-activity-card.tsx +9 -3
  113. package/src/components/chat/guide-welcome.tsx +11 -8
  114. package/src/components/chat/hooks/use-chat-card-item.ts +17 -1
  115. package/src/components/chat/hooks/use-nats-chat-adapter.ts +80 -7
  116. package/src/components/chat/hooks/use-slash-commands.ts +39 -0
  117. package/src/components/chat/hooks/use-sse-chat-adapter.ts +49 -45
  118. package/src/components/chat/hooks/use-unified-chat.ts +69 -34
  119. package/src/components/chat/mingo-chat-history.tsx +118 -36
  120. package/src/components/chat/mingo-info-card.tsx +214 -0
  121. package/src/components/chat/mingo-welcome.tsx +89 -36
  122. package/src/components/chat/types/chat.types.ts +4 -0
  123. package/src/components/chat/types/unified-chat-state.types.ts +46 -0
  124. package/src/components/chat/utils/process-historical-messages.ts +8 -0
  125. package/src/components/content-loading-container.tsx +1 -1
  126. package/src/components/empty-state.tsx +3 -3
  127. package/src/components/faq-accordion.tsx +2 -2
  128. package/src/components/features/changelog-manager.tsx +2 -2
  129. package/src/components/features/highlight-config-section.tsx +2 -2
  130. package/src/components/features/og-editor-preview.tsx +3 -3
  131. package/src/components/features/release-media-manager.tsx +2 -2
  132. package/src/components/loading/card-skeleton.tsx +3 -3
  133. package/src/components/loading/device-card-skeleton.tsx +1 -1
  134. package/src/components/loading/margin-report-skeleton.tsx +2 -2
  135. package/src/components/loading/organization-card-skeleton.tsx +1 -1
  136. package/src/components/loading/organization-icon-skeleton.tsx +1 -1
  137. package/src/components/loading/page-layout-skeleton.tsx +9 -9
  138. package/src/components/loading/users-grid-skeleton.tsx +2 -2
  139. package/src/components/logs-list.tsx +1 -1
  140. package/src/components/media-carousel.tsx +1 -1
  141. package/src/components/navigation/app-layout-drawer.tsx +70 -6
  142. package/src/components/open-source-features.tsx +2 -2
  143. package/src/components/ui/actions-menu.tsx +36 -13
  144. package/src/components/ui/allowed-domains-input.tsx +3 -3
  145. package/src/components/ui/cursor-pagination.tsx +1 -1
  146. package/src/components/ui/dropdown-button.tsx +6 -6
  147. package/src/components/ui/info-row.tsx +2 -2
  148. package/src/components/ui/modal-v2.tsx +8 -1
  149. package/src/components/ui/more-actions-menu.tsx +13 -0
  150. package/src/components/ui/simple-markdown-renderer.tsx +12 -2
  151. package/src/components/ui/square-avatar.tsx +6 -1
  152. package/src/components/ui/tags-input.tsx +1 -1
  153. package/src/components/ui/textarea.tsx +18 -4
  154. package/src/components/vendor-icon.tsx +1 -1
  155. package/src/components/vendor-tag.tsx +1 -1
  156. package/src/components/why-it-matters.tsx +1 -1
  157. package/src/hooks/ui/use-auto-limit-tags.ts +22 -8
  158. package/src/stories/ActionsMenu.stories.tsx +256 -0
  159. package/src/stories/AppLayoutSidebar.stories.tsx +6 -0
  160. package/src/stories/Board.stories.tsx +1 -1
  161. package/src/stories/ChatMingoInfoCards.stories.tsx +748 -0
  162. package/src/stories/ChatTicketList.stories.tsx +1 -1
  163. package/src/stories/DataTable.stories.tsx +17 -13
  164. package/src/stories/EmbeddableChat.stories.tsx +233 -1
  165. package/src/stories/MingoInfoCard.stories.tsx +193 -0
  166. package/src/stories/MingoOnboardingCard.stories.tsx +1 -1
  167. package/src/stories/Modal.stories.tsx +1 -0
  168. package/src/stories/NotificationDrawer.stories.tsx +14 -3
  169. package/src/stories/PageActions.stories.tsx +4 -4
  170. package/src/stories/PageContainer.stories.tsx +5 -5
  171. package/src/stories/PageLayout.stories.tsx +1 -1
  172. package/src/stories/TabSelector.stories.tsx +2 -2
  173. package/src/stories/Tag.stories.tsx +10 -5
  174. package/src/stories/TicketInfoSection.stories.tsx +23 -11
  175. package/src/stories/Toaster.stories.tsx +5 -5
  176. package/src/stories/__fixtures__/chat-card-decorator.tsx +44 -0
  177. package/dist/chunk-AIKPR6BE.js.map +0 -1
  178. package/dist/chunk-KM4KK2A6.cjs.map +0 -1
  179. package/dist/chunk-ND24XSVT.js.map +0 -1
  180. package/dist/chunk-QNYH3WUU.cjs.map +0 -1
  181. package/dist/chunk-QYRV6MKX.js.map +0 -1
  182. package/src/stories/.TicketDescriptionViewer.stories.md +0 -39
  183. package/src/stories/.TicketKnowledgeBaseList.stories.md +0 -36
  184. package/src/stories/TicketDescriptionViewer.stories.tsx +0 -70
  185. package/src/stories/TicketKnowledgeBaseList.stories.tsx +0 -51
  186. /package/dist/{chunk-CCSEXVI4.js.map → chunk-LEHEHEVW.js.map} +0 -0
  187. /package/dist/{chunk-RMOHKAHA.js.map → chunk-V6ME2VMU.js.map} +0 -0
  188. /package/dist/{chunk-HRIAZQ5D.js.map → chunk-YEOM52ZR.js.map} +0 -0
@@ -0,0 +1,146 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '../../utils/cn'
5
+ import { Tag } from '../ui/tag'
6
+ import { ActionsMenuDropdown } from '../ui/actions-menu'
7
+ import { Ellipsis01Icon } from '../icons-v2-generated'
8
+ import { useAutoLimitTags } from '../../hooks/ui/use-auto-limit-tags'
9
+
10
+ // =============================================================================
11
+ // Types
12
+ // =============================================================================
13
+
14
+ /** A single collapsible quick-action chip. */
15
+ export interface QuickActionChip {
16
+ /** Stable React key + menu-item id. */
17
+ id: string
18
+ label: string
19
+ icon?: React.ReactNode
20
+ /** `'primary'` = accent (yellow) chip, `'outline'` = bordered chip (default). */
21
+ variant?: 'primary' | 'outline'
22
+ onSelect?: () => void
23
+ }
24
+
25
+ export interface ChatQuickActionRowProps {
26
+ /** Always-visible leading chip(s) (e.g. Mingo's "Start Guide Chat"). Never
27
+ * collapses into the overflow menu — rendered before the measured zone. */
28
+ leading?: React.ReactNode
29
+ /** Collapsible chips — as many as fit on one line render inline; the rest
30
+ * collapse under a trailing "⋯" overflow menu (small icon button). */
31
+ chips: ReadonlyArray<QuickActionChip>
32
+ className?: string
33
+ }
34
+
35
+ // =============================================================================
36
+ // Chip button
37
+ // =============================================================================
38
+
39
+ function ChipButton({ chip }: { chip: QuickActionChip }) {
40
+ return (
41
+ <button
42
+ type="button"
43
+ onClick={chip.onSelect}
44
+ className="shrink-0 rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent"
45
+ >
46
+ <Tag variant={chip.variant ?? 'outline'} icon={chip.icon} label={chip.label} />
47
+ </button>
48
+ )
49
+ }
50
+
51
+ // =============================================================================
52
+ // Component
53
+ // =============================================================================
54
+
55
+ /**
56
+ * A single-line quick-action chip row that collapses overflow into a "⋯" menu.
57
+ *
58
+ * Same dynamic, width-measured overflow used by the multi-select Autocomplete
59
+ * (`useAutoLimitTags`): off-screen copies of every chip are measured, as many
60
+ * as fit render inline, and the remainder move into an `ActionsMenuDropdown`
61
+ * behind a small "⋯" icon button. An optional `leading` slot stays pinned and
62
+ * never collapses, so the row's primary action is always reachable.
63
+ */
64
+ export function ChatQuickActionRow({
65
+ leading,
66
+ chips,
67
+ className,
68
+ }: ChatQuickActionRowProps) {
69
+ const auto = useAutoLimitTags({
70
+ count: chips.length,
71
+ reserveInputWidth: false,
72
+ })
73
+
74
+ const visibleChips = chips.slice(0, auto.visibleCount)
75
+ const hiddenChips = chips.slice(auto.visibleCount)
76
+
77
+ if (!leading && chips.length === 0) return null
78
+
79
+ return (
80
+ <div
81
+ className={cn('relative flex shrink-0 items-center gap-1', className)}
82
+ >
83
+ {leading}
84
+
85
+ {chips.length > 0 && (
86
+ // Measured zone: `flex-1 min-w-0 overflow-hidden` so it claims the width
87
+ // left by the pinned `leading` slot and clips anything past the fit.
88
+ <div
89
+ ref={auto.middleRef}
90
+ className="flex flex-1 min-w-0 items-center gap-1 overflow-hidden"
91
+ >
92
+ {visibleChips.map((chip) => (
93
+ <ChipButton key={chip.id} chip={chip} />
94
+ ))}
95
+
96
+ {hiddenChips.length > 0 && (
97
+ <ActionsMenuDropdown
98
+ triggerAriaLabel="More quick actions"
99
+ // Don't return focus (and its ring) to the "⋯" trigger on close.
100
+ onCloseAutoFocus={(e) => e.preventDefault()}
101
+ groups={[
102
+ {
103
+ items: hiddenChips.map((chip) => ({
104
+ id: chip.id,
105
+ label: chip.label,
106
+ icon: chip.icon,
107
+ onClick: chip.onSelect,
108
+ })),
109
+ },
110
+ ]}
111
+ customTrigger={
112
+ <button
113
+ ref={auto.badgeRef}
114
+ type="button"
115
+ aria-label="More quick actions"
116
+ className="shrink-0 rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent"
117
+ >
118
+ <Tag variant="outline" label={<Ellipsis01Icon size={16} />} />
119
+ </button>
120
+ }
121
+ />
122
+ )}
123
+ </div>
124
+ )}
125
+
126
+ {/* Off-screen measurement copies of every chip — the hook reads their
127
+ widths to decide how many fit. Mirror the visible chip markup (same
128
+ variant/icon/label and the same `gap-1`) so the measured widths match
129
+ what actually renders. */}
130
+ <div
131
+ ref={auto.measureRef}
132
+ aria-hidden
133
+ className="pointer-events-none invisible absolute left-0 top-0 -z-10 flex gap-1"
134
+ >
135
+ {chips.map((chip) => (
136
+ <Tag
137
+ key={`measure-${chip.id}`}
138
+ variant={chip.variant ?? 'outline'}
139
+ icon={chip.icon}
140
+ label={chip.label}
141
+ />
142
+ ))}
143
+ </div>
144
+ </div>
145
+ )
146
+ }
@@ -56,6 +56,7 @@ import { ChatComposer } from './chat-composer'
56
56
  import { useChatDialogManager } from './hooks/use-chat-dialog-manager'
57
57
  import { ChatDialogModals } from './mingo-chat-modals'
58
58
  import { ChatMessageList } from './chat-message-list'
59
+ import { ChatMessageListSkeleton } from './chat-message-skeleton'
59
60
  import { SourceActionButton } from './source-action-button'
60
61
  import { NavLinkAnchorViaRuntime } from './nav-link-anchor-via-runtime'
61
62
  import { ChatAttachmentChipStrip } from './chat-attachment-bar'
@@ -70,6 +71,7 @@ import {
70
71
  type ChatMode,
71
72
  type UseUnifiedChatModes,
72
73
  } from './hooks/use-unified-chat'
74
+ import type { UnifiedChatState } from './types/unified-chat-state.types'
73
75
  import type {
74
76
  UseNatsChatAdapterConfig,
75
77
  FetchDialogsParams,
@@ -79,7 +81,7 @@ import { useChatAttachments } from './hooks/use-chat-attachments'
79
81
  import { useChatAttachmentImageGallery } from './hooks/use-chat-attachment-image-gallery'
80
82
  import { useChatIdentity } from './hooks/use-chat-identity'
81
83
  import { useCloseOnNavigation } from './hooks/use-close-on-navigation'
82
- import { fetchSlashCommands, type SlashCommandSummary } from './hooks/use-slash-commands'
84
+ import { fetchSlashCommands, useSlashCommandRegistry, type SlashCommandSummary } from './hooks/use-slash-commands'
83
85
 
84
86
  import type { ChatRef } from './chat-ref.types'
85
87
  import type { ChatInputRef, SlashCommandActionId } from './types/component.types'
@@ -162,6 +164,35 @@ export interface EmbeddableChatProps {
162
164
  */
163
165
  modes?: UseUnifiedChatModes
164
166
 
167
+ /**
168
+ * Pre-built Mingo-mode state, supplied by the host instead of letting the
169
+ * built-in NATS adapter own it. When provided, the panel renders Mingo mode
170
+ * from this object and opens no subscription of its own — the host keeps
171
+ * chat data + streaming in its own store/cache so it survives the panel
172
+ * unmounting (no `keepMounted` needed). The host should then NOT pass
173
+ * `modes.mingo` (Guide-mode wiring via `modes.guide` is unaffected).
174
+ */
175
+ mingoState?: UnifiedChatState
176
+
177
+ /**
178
+ * Dialog-management capabilities for injected Mingo mode (`mingoState`).
179
+ *
180
+ * When the host injects `mingoState`, it doesn't pass `modes.mingo` (that
181
+ * would re-activate the idle built-in adapter), so the rename/archive/
182
+ * restore/archive-page affordances can't read their capability flags off the
183
+ * callback config. Supply them here instead. `canRename`/`canArchive` default
184
+ * to `true` when `mingoState` is set; the archive page + restore are shown
185
+ * only when their callbacks are provided. Ignored unless `mingoState` is set.
186
+ */
187
+ mingoDialogCapabilities?: {
188
+ canRename?: boolean
189
+ canArchive?: boolean
190
+ fetchArchivedDialogs?: (
191
+ params: FetchDialogsParams,
192
+ ) => Promise<FetchDialogsResult>
193
+ unarchiveDialog?: (id: string) => Promise<void>
194
+ }
195
+
165
196
  /**
166
197
  * Controlled active-mode. When provided, `onActiveModeChange` MUST
167
198
  * also be provided. For uncontrolled use see `defaultActiveMode`.
@@ -605,6 +636,8 @@ function EmbeddableChatInner({
605
636
  extras,
606
637
  tableIdForDocumentType,
607
638
  modes,
639
+ mingoState,
640
+ mingoDialogCapabilities,
608
641
  activeMode: controlledActiveMode,
609
642
  onActiveModeChange,
610
643
  defaultActiveMode,
@@ -698,35 +731,9 @@ function EmbeddableChatInner({
698
731
  // Imperative handle to the chat input.
699
732
  const chatInputRef = useRef<ChatInputRef | null>(null)
700
733
 
701
- // Async lookup of the slash-command registry for the current source.
702
- const [commandsById, setCommandsById] = useState<Map<string, SlashCommandSummary>>(
703
- () => new Map(),
704
- )
705
- const [commandsLoaded, setCommandsLoaded] = useState(false)
706
- useEffect(() => {
707
- if (!isOpen) return
708
- let cancelled = false
709
- const ctrl = new AbortController()
710
- setCommandsLoaded(false)
711
- fetchSlashCommands('', ctrl.signal, commandsUrl)
712
- .then((commands) => {
713
- if (cancelled) return
714
- const map = new Map<string, SlashCommandSummary>()
715
- for (const cmd of commands) map.set(cmd.id, cmd)
716
- setCommandsById(map)
717
- setCommandsLoaded(true)
718
- })
719
- .catch((err) => {
720
- if (!cancelled && (err as Error)?.name !== 'AbortError') {
721
- console.warn('[embeddable-chat] failed to fetch slash commands:', err)
722
- setCommandsLoaded(true)
723
- }
724
- })
725
- return () => {
726
- cancelled = true
727
- ctrl.abort()
728
- }
729
- }, [isOpen, source, commandsUrl])
734
+ // The slash-command registry (Guide onboarding cards) is loaded via
735
+ // react-query below, after `activeMode` is resolved — gated on Guide mode and
736
+ // cached across remounts. See `commandsById` / `commandsLoaded`.
730
737
 
731
738
  // Slash-command autocomplete config — passed to <ChatInput>.
732
739
  const slashCommandsProp = useMemo(
@@ -774,6 +781,36 @@ function EmbeddableChatInner({
774
781
  return { guide: guideOptions }
775
782
  }, [modes, tableIdForDocumentType])
776
783
 
784
+ // Resolve dialog-management capabilities (rename / archive / archive-page /
785
+ // restore) from a single source. With injected `mingoState` they come from
786
+ // `mingoDialogCapabilities` (the host doesn't pass `modes.mingo`); otherwise
787
+ // from the callback-config shape, where the presence of a callback IS the
788
+ // capability. Both feed the same gating below so the JSX has one source.
789
+ const mingoCaps = useMemo(() => {
790
+ if (mingoState) {
791
+ // Default OFF: the row ⋯ menu (Rename / Archive) and the archive page are
792
+ // shown only when the host explicitly opts in — same capability-gating as
793
+ // the archive button (`fetchArchivedDialogs` presence). Otherwise the menu
794
+ // would advertise actions the host hasn't actually wired (no-ops).
795
+ return {
796
+ canRename: mingoDialogCapabilities?.canRename ?? false,
797
+ canArchive: mingoDialogCapabilities?.canArchive ?? false,
798
+ fetchArchivedDialogs: mingoDialogCapabilities?.fetchArchivedDialogs,
799
+ unarchiveDialog: mingoDialogCapabilities?.unarchiveDialog,
800
+ }
801
+ }
802
+ return {
803
+ canRename: !!effectiveModes.mingo?.renameDialog,
804
+ canArchive: !!effectiveModes.mingo?.archiveDialog,
805
+ fetchArchivedDialogs: effectiveModes.mingo?.fetchArchivedDialogs,
806
+ unarchiveDialog: effectiveModes.mingo?.unarchiveDialog,
807
+ }
808
+ }, [mingoState, mingoDialogCapabilities, effectiveModes])
809
+
810
+ // "Does Mingo mode exist?" — true via either the callback config OR injected
811
+ // state. Gates the guide↔mingo back-chevron and the guide-mode banner.
812
+ const hasMingoMode = !!effectiveModes.mingo || !!mingoState
813
+
777
814
  // Initial active mode picks the first configured slot when neither
778
815
  // controlled `activeMode` nor `defaultActiveMode` is provided. Guide
779
816
  // wins ties so legacy callers see no behaviour change.
@@ -795,6 +832,22 @@ function EmbeddableChatInner({
795
832
  [controlledActiveMode, onActiveModeChange],
796
833
  )
797
834
 
835
+ // Slash-command registry (Guide onboarding cards) via the shared
836
+ // `useSlashCommandRegistry` react-query hook:
837
+ // - `enabled: activeMode === 'guide'` → never fetched in Mingo mode, so
838
+ // opening the default Mingo panel doesn't hit the commands endpoint.
839
+ // - Shares one cache entry (keyed on `commandsUrl`) with the SSE adapter's
840
+ // `displayRef` lookup, so Guide mode fetches `commands` ONCE, not twice.
841
+ // - Cached with `staleTime/gcTime: Infinity` inside the hook, so toggling
842
+ // to Guide or reopening the (remounting) drawer reads from cache.
843
+ const { commands: commandsList, loaded: commandsLoaded } =
844
+ useSlashCommandRegistry(commandsUrl, { enabled: activeMode === 'guide' })
845
+ const commandsById = useMemo(() => {
846
+ const map = new Map<string, SlashCommandSummary>()
847
+ for (const cmd of commandsList) map.set(cmd.id, cmd)
848
+ return map
849
+ }, [commandsList])
850
+
798
851
  const {
799
852
  messages: rawMessages,
800
853
  isLoading: chatLoading,
@@ -817,9 +870,12 @@ function EmbeddableChatInner({
817
870
  renameDialog,
818
871
  archiveDialog,
819
872
  isDialogsLoading,
873
+ dialogsError,
874
+ reloadDialogs,
875
+ isMessagesLoading,
820
876
  hasMoreDialogs,
821
877
  loadMoreDialogs,
822
- } = useUnifiedChat({ modes: effectiveModes, activeMode })
878
+ } = useUnifiedChat({ modes: effectiveModes, activeMode, mingoStateOverride: mingoState })
823
879
 
824
880
  // Chat-attachment hooks (v2 attachment feature).
825
881
  const {
@@ -878,21 +934,71 @@ function EmbeddableChatInner({
878
934
  [discussRef, displayRef, resolvedBaseRoute, chipBasePlatform, extras],
879
935
  )
880
936
 
937
+ // Stable assistant-icon element. `<ChatMessageList>` forwards this prop to
938
+ // every assistant `<ChatMessageEnhanced>`, whose memo compares it BY
939
+ // REFERENCE. Created inline in JSX it was a fresh element on every render,
940
+ // defeating the memo for all assistant messages (re-rendering completed
941
+ // bubbles — and re-mounting their inline cards — on every realtime chunk).
942
+ const mingoAssistantIcon = useMemo(
943
+ () => <MingoIcon className="h-6 w-6" cornerColor="var(--ods-flamingo-cyan-base)" />,
944
+ [],
945
+ )
946
+
947
+ // Stable per-message timestamps. The memoized `<ChatMessageEnhanced>`
948
+ // compares `timestamp.getTime()`, so we must NOT stamp `new Date()` fresh
949
+ // on each render: a moving clock makes every message's timestamp differ
950
+ // between renders, defeating memoization and forcing the WHOLE list (and
951
+ // every open menu/card inside it) to re-render on every realtime chunk.
952
+ // Prefer the host's real timestamp; otherwise stamp once per id and reuse
953
+ // it (keeps the displayed time stable and lets memoization hold even when
954
+ // the host omits a timestamp).
955
+ const timestampCacheRef = useRef<Map<string, Date>>(new Map())
956
+
881
957
  // Map docMessages → lib's Message type, forwarding chatRefs + scrollAnchor.
882
- const messages: Message[] = useMemo(
883
- () =>
884
- rawMessages.map((m) => ({
958
+ const messages: Message[] = useMemo(() => {
959
+ const cache = timestampCacheRef.current
960
+ const seenIds = new Set<string>()
961
+
962
+ const mapped = rawMessages.map((m) => {
963
+ seenIds.add(m.id)
964
+ let timestamp: Date
965
+ if (m.timestamp != null) {
966
+ timestamp = new Date(m.timestamp)
967
+ } else {
968
+ const cached = cache.get(m.id)
969
+ timestamp = cached ?? new Date()
970
+ if (!cached) cache.set(m.id, timestamp)
971
+ }
972
+
973
+ return {
885
974
  id: m.id,
886
975
  role: m.role as 'user' | 'assistant',
887
- name: m.role === 'assistant' ? 'Mingo AI' : 'You',
976
+ // Host-supplied per-message name/avatar win (e.g. the signed-in user's
977
+ // full name + photo on a `user` bubble); fall back to the role default
978
+ // when the host doesn't provide them.
979
+ name: m.name ?? (m.role === 'assistant' ? 'Mingo' : 'You'),
980
+ avatar: m.avatar ?? null,
981
+ // Forward the host's authorType so user bubbles get the same accent
982
+ // name color as the standalone /mingo page (user → 'admin').
983
+ ...(m.authorType ? { authorType: m.authorType } : {}),
888
984
  content: m.segments && m.segments.length > 0 ? m.segments : m.content,
889
- timestamp: new Date(),
985
+ timestamp,
890
986
  assistantType: m.role === 'assistant' ? ('mingo' as const) : undefined,
891
987
  ...(m.chatRefs ? { chatRefs: m.chatRefs } : {}),
892
988
  ...(m.scrollAnchor ? { scrollAnchor: m.scrollAnchor } : {}),
893
- })),
894
- [rawMessages],
895
- )
989
+ }
990
+ })
991
+
992
+ // Drop cached fallbacks for messages no longer present so the map can't
993
+ // grow unbounded across a long-lived session.
994
+ if (cache.size > seenIds.size) {
995
+ for (const id of cache.keys()) {
996
+ if (!seenIds.has(id)) cache.delete(id)
997
+ }
998
+ }
999
+
1000
+ return mapped
1001
+ }, [rawMessages])
896
1002
 
897
1003
  const handleSend = useCallback(
898
1004
  (text: string) => {
@@ -951,8 +1057,8 @@ function EmbeddableChatInner({
951
1057
  clearMessages,
952
1058
  renameDialog,
953
1059
  archiveDialog,
954
- fetchArchivedDialogs: effectiveModes.mingo?.fetchArchivedDialogs,
955
- unarchiveDialog: effectiveModes.mingo?.unarchiveDialog,
1060
+ fetchArchivedDialogs: mingoCaps.fetchArchivedDialogs,
1061
+ unarchiveDialog: mingoCaps.unarchiveDialog,
956
1062
  })
957
1063
 
958
1064
  const handleOpen = useCallback(() => setIsOpen(true), [setIsOpen])
@@ -983,12 +1089,24 @@ function EmbeddableChatInner({
983
1089
  }, [source, discussRef, setIsOpen])
984
1090
 
985
1091
  const hasMessages = messages.length > 0
1092
+ // First dialog page in flight and nothing cached yet — we don't yet know if
1093
+ // the user is new or returning, so the Mingo empty state shows a skeleton
1094
+ // rather than flashing the new-user greeting+grid before the list lands.
1095
+ const dialogsInitialLoading = isDialogsLoading && dialogs.length === 0
1096
+ // Dialog list failed to load (e.g. backend down) and we have nothing cached.
1097
+ // Distinct from "no chats" so the Mingo empty state can offer a retry instead
1098
+ // of the new-user greeting (which misleadingly advertises Guide).
1099
+ const dialogsLoadError = dialogsError && dialogs.length === 0
986
1100
  // A conversation is "open" the instant a chat is selected (`isOpeningDialog` /
987
1101
  // `isViewingArchived` are set synchronously on click) — not only once its
988
1102
  // history has loaded (`hasMessages`). Driving the surface + content branch
989
1103
  // off this makes the normal-chat open animate identically to the archived
990
1104
  // one instead of lagging behind the message fetch.
991
1105
  const hasConversation = hasMessages || isOpeningDialog || isViewingArchived
1106
+ // Opening a dialog whose history hasn't arrived yet — show a message-list
1107
+ // skeleton instead of an empty thread so the open reads as "loading" rather
1108
+ // than a blank flash before the bubbles stream in.
1109
+ const messagesInitialLoading = isMessagesLoading && !hasMessages
992
1110
  // Keys the content region so each distinct view (open conversation, Mingo
993
1111
  // welcome, Guide onboarding) remounts on switch → fades in for 200ms, the
994
1112
  // same feel as the surface flip. Switching dialogs stays "conversation" so
@@ -999,7 +1117,7 @@ function EmbeddableChatInner({
999
1117
  const isGuideEmpty = !hasConversation && activeMode === 'guide'
1000
1118
  // The guide-empty back-chevron returns to Mingo — only offer it when Mingo
1001
1119
  // mode actually exists to return to (guide is normally entered from Mingo).
1002
- const guideCanReturnToMingo = isGuideEmpty && !!effectiveModes.mingo
1120
+ const guideCanReturnToMingo = isGuideEmpty && hasMingoMode
1003
1121
  const sourceLabel = source === 'flamingo' ? 'Knowledge Base' : 'Data Room'
1004
1122
 
1005
1123
  // Empty-state chip grid — derived directly from the fetched slash commands.
@@ -1127,12 +1245,12 @@ function EmbeddableChatInner({
1127
1245
  : undefined
1128
1246
  }
1129
1247
  onRename={
1130
- activeDialogId && activeDialog && effectiveModes.mingo?.renameDialog
1248
+ activeDialogId && activeDialog && mingoCaps.canRename
1131
1249
  ? () => setRenameTarget(activeDialog)
1132
1250
  : undefined
1133
1251
  }
1134
1252
  onArchive={
1135
- activeDialogId && activeDialog && effectiveModes.mingo?.archiveDialog
1253
+ activeDialogId && activeDialog && mingoCaps.canArchive
1136
1254
  ? () => setArchiveTarget(activeDialog)
1137
1255
  : undefined
1138
1256
  }
@@ -1144,7 +1262,7 @@ function EmbeddableChatInner({
1144
1262
  Guide mode is active AND the default (Mingo) mode also exists:
1145
1263
  the banner contrasts the temporary Guide session against the
1146
1264
  default chat, so it's meaningless in a guide-only setup. */}
1147
- {activeMode === 'guide' && !!effectiveModes.mingo && (
1265
+ {activeMode === 'guide' && hasMingoMode && (
1148
1266
  <GuideModeBanner className="animate-in fade-in-0 duration-200" />
1149
1267
  )}
1150
1268
 
@@ -1164,17 +1282,33 @@ function EmbeddableChatInner({
1164
1282
  >
1165
1283
  {hasConversation ? (
1166
1284
  <div className="flex-1 flex flex-col min-h-0">
1285
+ {messagesInitialLoading ? (
1286
+ // Match the real list's `fullWidth` layout so swapping
1287
+ // skeleton → bubbles doesn't shift the column. The panel
1288
+ // wrapper above already pads (`p-[var(--spacing-system-m)]`),
1289
+ // so the skeleton sits flush (no inner px/pb).
1290
+ <ChatMessageListSkeleton
1291
+ fullWidth
1292
+ className="flex-1"
1293
+ />
1294
+ ) : (
1167
1295
  <ChatMessageList
1168
1296
  messages={messages}
1169
1297
  isTyping={chatLoading}
1170
1298
  autoScroll={true}
1171
1299
  assistantType="mingo"
1172
- assistantIcon={<MingoIcon className="h-4 w-4" color="white" />}
1300
+ assistantIcon={mingoAssistantIcon}
1173
1301
  renderEntityCard={renderEntityCard}
1174
1302
  NavLinkAnchor={NavLinkAnchorViaRuntime}
1175
1303
  className="flex-1"
1304
+ // No inner `px`/`pb`: the panel wrapper already pads with
1305
+ // `p-[var(--spacing-system-m)]`. The default content class
1306
+ // adds `px-[var(--spacing-system-m)]` + `pb-…xs`, which
1307
+ // double-inset the thread; `pb-0` overrides via twMerge.
1308
+ contentClassName="max-w-none pb-0"
1176
1309
  fullWidth
1177
1310
  />
1311
+ )}
1178
1312
  {lastSources &&
1179
1313
  (lastSources.cited.length > 0 ||
1180
1314
  lastSources.uncited.length > 0) &&
@@ -1207,20 +1341,19 @@ function EmbeddableChatInner({
1207
1341
  // Derived internally (returning-user variation) — placed
1208
1342
  // after the spread so it can't be overridden by the prop.
1209
1343
  hasExistingChats={dialogs.length > 0}
1344
+ isLoadingHistory={dialogsInitialLoading}
1345
+ loadError={dialogsLoadError}
1346
+ onRetry={reloadDialogs}
1210
1347
  dialogHistory={
1211
1348
  dialogs.length > 0 ? (
1212
1349
  <MingoChatHistory
1213
1350
  dialogs={dialogs}
1214
1351
  onSelectDialog={handleSelectDialog}
1215
1352
  onRequestRename={
1216
- effectiveModes.mingo?.renameDialog
1217
- ? setRenameTarget
1218
- : undefined
1353
+ mingoCaps.canRename ? setRenameTarget : undefined
1219
1354
  }
1220
1355
  onRequestArchive={
1221
- effectiveModes.mingo?.archiveDialog
1222
- ? setArchiveTarget
1223
- : undefined
1356
+ mingoCaps.canArchive ? setArchiveTarget : undefined
1224
1357
  }
1225
1358
  hasMore={hasMoreDialogs}
1226
1359
  isLoadingMore={isDialogsLoading && dialogs.length > 0}