@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
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import React, { forwardRef, memo, useMemo } from "react"
3
+ import React, { forwardRef, memo, useMemo, useRef } from "react"
4
4
  import { cn } from "../../utils/cn"
5
5
  import { SquareAvatar } from "../ui/square-avatar"
6
6
  import { ToolExecutionDisplay } from "./tool-execution-display"
@@ -57,6 +57,19 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
57
57
 
58
58
  const segments = useMemo(() => normalizeContent(content), [content])
59
59
 
60
+ // Cross-render cache of rendered inline-card nodes, keyed by `type:id`.
61
+ // A fetch-mode card lives inside the assistant message, which re-renders on
62
+ // every stream chunk (and the whole list re-renders when a new message
63
+ // arrives). `renderEntityCard` produces a FRESH element each time, so React
64
+ // re-mounts the card — closing any open menu/popover and re-triggering its
65
+ // fetch. Caching the produced node by key and returning the SAME element
66
+ // reference lets React bail out of re-rendering that subtree, so the card
67
+ // (and its open menu) survives across chunks. Invalidated per key when the
68
+ // backing ref or the render fn identity changes.
69
+ const renderedCardNodeCache = useRef(
70
+ new Map<string, { refMatch: ChatRef | undefined; render: ((ref: ChatRef) => React.ReactNode) | undefined; node: React.ReactNode }>(),
71
+ )
72
+
60
73
  /**
61
74
  * Per-message rendering plan for `[card://type:id]` markers.
62
75
  *
@@ -94,7 +107,13 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
94
107
  | { kind: 'block'; key: string; node: React.ReactNode }
95
108
  const partsBySegment = new Map<number, SegmentPart[]>()
96
109
  if (!render) return { inlineByKey, partsBySegment }
97
- const seenRendered = new Map<string, React.ReactNode>()
110
+ const cache = renderedCardNodeCache.current
111
+ const usedKeys = new Set<string>()
112
+ // Card keys already emitted as a hoisted block (`b-<key>`). The same
113
+ // marker can legitimately appear twice in one message (LLM references
114
+ // the same entity twice); we hoist the FIRST occurrence and skip the
115
+ // duplicates so two siblings never collide on the same React key.
116
+ const emittedBlockKeys = new Set<string>()
98
117
  segments.forEach((segment, segIdx) => {
99
118
  if (segment.type !== 'text') return
100
119
  const text = segment.text
@@ -106,11 +125,15 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
106
125
  const cardType = match[1]
107
126
  const cardId = match[2]
108
127
  const key = `${cardType}:${cardId}`
109
- // Dedup renderEntityCard calls per unique key across the
110
- // whole message — same key emitted twice (rare but possible)
111
- // returns the cached node so React reuses the same instance.
112
- let rendered = seenRendered.get(key)
113
- if (!seenRendered.has(key)) {
128
+ usedKeys.add(key)
129
+ // Reuse the cached node when neither the backing ref nor the render
130
+ // fn changed — returning the SAME element reference across renders is
131
+ // what stops React from re-mounting the card (and closing its open
132
+ // menu / re-fetching) on every stream chunk. Also dedups the same key
133
+ // emitted twice within one message.
134
+ const refMatch = refs[key]
135
+ let entry = cache.get(key)
136
+ if (!entry || entry.refMatch !== refMatch || entry.render !== render) {
114
137
  // Always invoke render() — even when the metadata map has
115
138
  // no entry for this marker. Fetch-mode card types
116
139
  // (delivery_item, roadmap_item, internal_task, etc.) don't
@@ -129,25 +152,30 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
129
152
  // `ref.title` shows the id rather than `undefined`) and
130
153
  // `url` to null (matches the no-link semantics fetch-mode
131
154
  // cards rely on — they resolve their own URL after fetch).
132
- const refMatch = refs[key]
133
155
  const refForRender: ChatRef = refMatch ?? {
134
156
  type: cardType,
135
157
  id: cardId,
136
158
  title: cardId,
137
159
  url: null,
138
160
  }
139
- rendered = render(refForRender)
140
- seenRendered.set(key, rendered)
161
+ entry = { refMatch, render, node: render(refForRender) }
162
+ cache.set(key, entry)
141
163
  }
164
+ const rendered = entry.node
142
165
  if (React.isValidElement(rendered) && rendered.type === BlockCard) {
143
166
  const props = rendered.props as BlockCardProps
144
167
  const markerEnd = match.index + match[0].length
145
168
  // Text chunk INCLUDING the marker — the inline pill renders
146
169
  // at the marker position via the `<a>` override.
147
170
  parts.push({ kind: 'text', text: text.slice(cursor, markerEnd) })
148
- parts.push({ kind: 'block', key, node: props.children })
171
+ // Hoist the block payload only on the FIRST occurrence of this key
172
+ // — a repeated marker still gets its inline pill (text + override
173
+ // above) but must not push a second `b-<key>` sibling.
174
+ if (!emittedBlockKeys.has(key)) {
175
+ emittedBlockKeys.add(key)
176
+ parts.push({ kind: 'block', key, node: props.children })
177
+ }
149
178
  cursor = markerEnd
150
- const refMatch = refs[key]
151
179
  inlineByKey.set(
152
180
  key,
153
181
  props.inline != null
@@ -155,8 +183,26 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
155
183
  : <span className="text-ods-text-primary font-medium">{refMatch?.title ?? cardId}</span>,
156
184
  )
157
185
  } else if (rendered != null) {
158
- // Inline-only — no split; remembered for the override.
159
- inlineByKey.set(key, rendered)
186
+ // Hoist fetch-mode entity cards (roadmap/blog/case-study/release/…)
187
+ // OUT of the markdown as stable-keyed block siblings — exactly like
188
+ // BlockCard. Rendered INSIDE `<SimpleMarkdownRenderer>` they remount
189
+ // on every streaming re-parse (react-markdown rebuilds its subtree
190
+ // token-by-token), which closes any open menu/popover and re-fires
191
+ // the card's fetch. As a sibling keyed by the card key (`b-<key>`)
192
+ // the card survives streaming: appending more text/markers only adds
193
+ // NEW siblings, existing cards keep their React instance.
194
+ //
195
+ // We EXCLUDE the marker from the surrounding text (cursor jumps past
196
+ // it) so no inline pill renders — the full card IS the content.
197
+ // Hoist only the FIRST occurrence; a duplicate marker is dropped
198
+ // entirely (no inline pill for hoisted cards) so we never push a
199
+ // second sibling colliding on the same `b-<key>` React key.
200
+ parts.push({ kind: 'text', text: text.slice(cursor, match.index) })
201
+ if (!emittedBlockKeys.has(key)) {
202
+ emittedBlockKeys.add(key)
203
+ parts.push({ kind: 'block', key, node: rendered })
204
+ }
205
+ cursor = match.index + match[0].length
160
206
  }
161
207
  }
162
208
  // Trailing text after the last block marker (or the entire
@@ -172,6 +218,13 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
172
218
  partsBySegment.set(segIdx, parts)
173
219
  }
174
220
  })
221
+ // Drop cached nodes for markers no longer present so the cache can't
222
+ // grow unbounded as a long message's markers change.
223
+ if (cache.size > usedKeys.size) {
224
+ for (const k of cache.keys()) {
225
+ if (!usedKeys.has(k)) cache.delete(k)
226
+ }
227
+ }
175
228
  return { inlineByKey, partsBySegment }
176
229
  }, [hasMarkerSupport, chatRefs, renderEntityCard, segments])
177
230
 
@@ -263,18 +316,29 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
263
316
 
264
317
  const getAvatarProps = () => {
265
318
  const displayName = name || (isUser ? "User" : assistantType === 'mingo' ? "Mingo" : "Fae")
266
- const initials = displayName.split(' ').map(n => n[0]).join('').toUpperCase()
267
319
  const isMingo = assistantType === 'mingo'
268
-
320
+
269
321
  return {
270
322
  src: avatar || undefined,
271
323
  alt: `${displayName} avatar`,
272
- fallback: initials,
324
+ // Pass the FULL name — SquareAvatar derives first+last initials itself
325
+ // (passing pre-joined initials like "PS" would collapse to one letter,
326
+ // since getFirstLastInitials treats it as a single word).
327
+ fallback: displayName,
273
328
  size: "sm" as const,
274
329
  variant: "round" as const,
330
+ // User avatar: compact 20×20 with 2px padding and a subtle gray fill
331
+ // (`bg-ods-card`) so the `border-ods-border` ring stays visible — the
332
+ // brand fill reads poorly for a user. Assistant/Fae keep their brand
333
+ // fill. Initials are smaller + muted gray for the user placeholder.
334
+ ...(isUser ? { initialsClassName: "text-[9px] text-ods-text-secondary" } : {}),
275
335
  className: cn(
276
336
  "flex-shrink-0",
277
- isMingo ? "bg-ods-flamingo-cyan" : "bg-ods-flamingo-pink"
337
+ isUser
338
+ ? "h-5 w-5 p-0.5 bg-ods-card"
339
+ : isMingo
340
+ ? "bg-ods-flamingo-cyan"
341
+ : "bg-ods-flamingo-pink"
278
342
  )
279
343
  }
280
344
  }
@@ -315,9 +379,15 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
315
379
  should be sized at ~50-60% of the wrapper (h-4 w-4 =
316
380
  16px works well for a 32px circle). */}
317
381
  <div className="flex items-center gap-[var(--spacing-system-xs)]">
318
- {showAvatar && !isSystem && !isUser && (
319
- assistantIcon && !avatar ? (
320
- <div className="flex items-center justify-center h-8 w-8 rounded-full bg-ods-accent flex-shrink-0">
382
+ {/* All non-system rows render an avatar. User bubbles always show
383
+ the SquareAvatar (image, or an initials fallback on missing/
384
+ failed image). Assistant/Fae use the host brand icon when no
385
+ avatar is supplied, else the filled SquareAvatar. */}
386
+ {showAvatar && !isSystem && (
387
+ !isUser && assistantIcon && !avatar ? (
388
+ // Host-supplied brand icon (e.g. Mingo): render it directly,
389
+ // no filled pill — the icon carries its own brand accent.
390
+ <div className="flex items-center justify-center flex-shrink-0">
321
391
  {assistantIcon}
322
392
  </div>
323
393
  ) : (
@@ -411,7 +411,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
411
411
  <ChatMessageListSkeleton
412
412
  className={className}
413
413
  showAvatars={showAvatars}
414
- assistantType={assistantType}
414
+ fullWidth={fullWidth}
415
415
  contentClassName={contentClassName}
416
416
  messageCount={6}
417
417
  />
@@ -6,54 +6,40 @@ interface ChatMessageSkeletonProps {
6
6
  className?: string
7
7
  showAvatar?: boolean
8
8
  isUser?: boolean
9
- assistantType?: 'mingo' | 'fae'
10
9
  }
11
10
 
12
- export function ChatMessageSkeleton({
13
- className,
14
- showAvatar = true,
15
- isUser = false,
16
- assistantType = 'fae'
11
+ /**
12
+ * One message-row skeleton. Mirrors `ChatMessageEnhanced`'s real layout:
13
+ * full panel width, no card background, and an INLINE avatar in the name row
14
+ * (the 2025 chat pattern — Claude.ai / ChatGPT / Gemini), NOT the legacy
15
+ * `absolute -left-16` hanging avatar. Body lines use percentage widths so they
16
+ * reflow cleanly as the panel resizes.
17
+ */
18
+ export function ChatMessageSkeleton({
19
+ className,
20
+ showAvatar = true,
21
+ isUser = false,
17
22
  }: ChatMessageSkeletonProps) {
18
- const isMingo = assistantType === 'mingo'
19
-
20
23
  return (
21
- <div
22
- className={cn(
23
- "relative",
24
- !isUser && "bg-ods-card/50 rounded-lg px-4 -mx-4",
25
- className
26
- )}
27
- >
28
- {/* Avatar Skeleton - absolutely positioned outside content area to match
29
- the real ChatMessageEnhanced hanging-avatar layout. */}
30
- {showAvatar && !isUser && (
31
- <div className={cn(
32
- "absolute -left-16 top-[var(--spacing-system-s)] w-12 h-12 rounded-full animate-pulse",
33
- isMingo
34
- ? "bg-gradient-to-br from-cyan-400/30 to-cyan-600/30"
35
- : "bg-gradient-to-br from-pink-400/30 to-pink-600/30"
36
- )} />
37
- )}
38
-
39
- {/* Message Content Skeleton */}
40
- <div className="flex flex-col gap-1 min-w-0">
41
- {/* Name and Timestamp Row Skeleton */}
42
- <div className="flex items-center justify-between pr-2">
43
- <div className="h-5 w-16 bg-ods-border rounded animate-pulse" />
44
- <div className="h-4 w-12 bg-ods-border rounded animate-pulse" />
24
+ <div className={cn("relative py-[var(--spacing-system-s)]", className)}>
25
+ <div className="flex min-w-0 flex-col gap-[var(--spacing-system-xxs)]">
26
+ {/* Name row — inline avatar (assistant only) + name + timestamp,
27
+ matching ChatMessageEnhanced's inline-avatar header. Neutral
28
+ `bg-ods-border` so the placeholders are actually visible (a tinted
29
+ low-alpha fill reads as blank on the dark surface). */}
30
+ <div className="flex items-center gap-[var(--spacing-system-xs)]">
31
+ {showAvatar && !isUser && (
32
+ <div className="h-8 w-8 flex-shrink-0 rounded-full bg-ods-border animate-pulse" />
33
+ )}
34
+ <div className="h-5 w-32 rounded bg-ods-border animate-pulse" />
35
+ <div className="ml-auto h-4 w-14 rounded bg-ods-border animate-pulse" />
45
36
  </div>
46
-
47
- {/* Message Content Skeleton */}
48
- <div className="flex flex-col gap-2">
49
- <div className="space-y-2">
50
- {/* First line - full width */}
51
- <div className="h-4 w-full bg-ods-border rounded animate-pulse" />
52
- {/* Second line - 80% width */}
53
- <div className="h-4 w-4/5 bg-ods-border rounded animate-pulse" />
54
- {/* Third line - 60% width (optional for variation) */}
55
- <div className="h-4 w-3/5 bg-ods-border rounded animate-pulse" />
56
- </div>
37
+
38
+ {/* Body lines — percentage widths so they reflow on panel resize. */}
39
+ <div className="flex flex-col gap-2 pt-[var(--spacing-system-xxs)]">
40
+ <div className="h-4 w-full rounded bg-ods-border animate-pulse" />
41
+ <div className="h-4 w-11/12 rounded bg-ods-border animate-pulse" />
42
+ <div className="h-4 w-3/5 rounded bg-ods-border animate-pulse" />
57
43
  </div>
58
44
  </div>
59
45
  </div>
@@ -64,7 +50,16 @@ interface ChatMessageListSkeletonProps {
64
50
  className?: string
65
51
  messageCount?: number
66
52
  showAvatars?: boolean
67
- assistantType?: 'mingo' | 'fae'
53
+ /**
54
+ * Match `ChatMessageList`'s `fullWidth`: drop the centered
55
+ * `max-w-ods-content-narrow` column so the skeleton fills the panel and
56
+ * reflows on resize (instead of staying a fixed narrow column with large
57
+ * side gutters). Same semantics as the real list.
58
+ */
59
+ fullWidth?: boolean
60
+ /** Extra classes on the content column (e.g. host-provided padding). The
61
+ * host wrapper is expected to own the panel padding; this stays empty by
62
+ * default so the skeleton sits flush. */
68
63
  contentClassName?: string
69
64
  }
70
65
 
@@ -72,13 +67,12 @@ export function ChatMessageListSkeleton({
72
67
  className,
73
68
  messageCount = 6,
74
69
  showAvatars = true,
75
- assistantType = 'fae',
76
- contentClassName
70
+ fullWidth = false,
71
+ contentClassName,
77
72
  }: ChatMessageListSkeletonProps) {
78
73
  const messages = Array.from({ length: messageCount }, (_, index) => ({
79
74
  id: index,
80
75
  isUser: index % 3 === 0,
81
- assistantType: assistantType
82
76
  }))
83
77
 
84
78
  return (
@@ -88,23 +82,29 @@ export function ChatMessageListSkeleton({
88
82
  "flex h-full w-full flex-col overflow-y-auto overflow-x-hidden flex-1",
89
83
  "[scroll-behavior:smooth]",
90
84
  "scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30",
91
- className
85
+ className,
92
86
  )}
93
87
  >
94
- <div className={cn("mx-auto flex w-full max-w-ods-content-narrow flex-col pb-[var(--spacing-system-xs)] min-w-0", contentClassName ?? "px-[var(--spacing-system-m)]")} style={{ minHeight: '100%' }}>
88
+ <div
89
+ className={cn(
90
+ fullWidth
91
+ ? "flex w-full flex-col min-w-0"
92
+ : "mx-auto flex w-full max-w-ods-content-narrow flex-col min-w-0",
93
+ contentClassName,
94
+ )}
95
+ style={{ minHeight: '100%' }}
96
+ >
97
+ {/* Bottom-anchor the rows like the real list. */}
95
98
  <div className="flex-1" />
96
- <div className="space-y-6">
97
- {messages.map((message) => (
98
- <ChatMessageSkeleton
99
- key={message.id}
100
- showAvatar={showAvatars}
101
- isUser={message.isUser}
102
- assistantType={message.assistantType}
103
- />
104
- ))}
105
- </div>
99
+ {messages.map((message) => (
100
+ <ChatMessageSkeleton
101
+ key={message.id}
102
+ showAvatar={showAvatars}
103
+ isUser={message.isUser}
104
+ />
105
+ ))}
106
106
  </div>
107
107
  </div>
108
108
  </div>
109
109
  )
110
- }
110
+ }
@@ -0,0 +1,94 @@
1
+ 'use client'
2
+
3
+ import { Chevron02LeftIcon, ClockHistoryIcon } from '../icons-v2-generated'
4
+ import { Button } from '../ui/button'
5
+ import { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu'
6
+ import { cn } from '../../utils/cn'
7
+ import type { ChatPanelHeaderProps } from './chat-panel-header'
8
+
9
+ export interface ChatPanelHeaderMobileProps extends ChatPanelHeaderProps {
10
+ className?: string
11
+ }
12
+
13
+ /**
14
+ * Mobile chat panel header — Figma node `7363:85532`. A distinct layout from
15
+ * the desktop bar (which packs full-height divider cells into a fixed `h-14`):
16
+ *
17
+ * - Surface: `ods-card` with a bottom border; generous `l` side padding,
18
+ * `l` bottom padding, and `xl` top padding on the title block (so the row
19
+ * bottom-aligns under a tall header).
20
+ * - Title: `text-h2` (vs the desktop `h3`/`h4`), truncated.
21
+ * - Actions: 44px outlined icon buttons (`Button variant="outline"
22
+ * size="icon"`) — a back chevron (when applicable) and a single trailing
23
+ * `⋯` menu carrying the view-dependent actions. There is no Close action
24
+ * (the drawer is dismissed via its backdrop / the back chevron); when no
25
+ * action applies the `⋯` trigger is hidden entirely.
26
+ *
27
+ * Same prop contract as `ChatPanelHeader` (`onClose` is accepted for parity but
28
+ * unused here); the orchestrator renders this on mobile and the desktop bar on
29
+ * `md+`.
30
+ */
31
+ export function ChatPanelHeaderMobile({
32
+ showBack = false,
33
+ title,
34
+ backAriaLabel = 'Back',
35
+ isArchivedView = false,
36
+ onBack,
37
+ onRestore,
38
+ onRename,
39
+ onArchive,
40
+ onOpenArchive,
41
+ className,
42
+ }: ChatPanelHeaderMobileProps) {
43
+ // The right-hand ⋯ menu carries the view-dependent actions. Close is not
44
+ // listed (the panel is dismissed elsewhere); the trigger is hidden entirely
45
+ // when no action applies (e.g. the archive list view).
46
+ const menuItems = [
47
+ !showBack && onOpenArchive && { id: 'open-archive', label: 'Chat archive', icon: <ClockHistoryIcon className="w-full h-full" />, onClick: onOpenArchive },
48
+ isArchivedView && onRestore && { id: 'unarchive', label: 'Unarchive chat', onClick: onRestore },
49
+ !isArchivedView && onRename && { id: 'rename', label: 'Rename chat', onClick: onRename },
50
+ !isArchivedView && onArchive && { id: 'archive', label: 'Archive chat', onClick: onArchive },
51
+ ].filter(Boolean) as ActionsMenuItem[]
52
+
53
+ return (
54
+ <div
55
+ className={cn(
56
+ 'flex-shrink-0 flex flex-col border-b border-ods-border bg-ods-card',
57
+ 'px-[var(--spacing-system-l)] pb-[var(--spacing-system-l)]',
58
+ className,
59
+ )}
60
+ >
61
+ <div className="flex w-full items-end gap-[var(--spacing-system-sf)]">
62
+ {showBack && (
63
+ <Button
64
+ variant="outline"
65
+ size="icon"
66
+ aria-label={backAriaLabel}
67
+ onClick={onBack}
68
+ className="shrink-0"
69
+ >
70
+ <Chevron02LeftIcon />
71
+ </Button>
72
+ )}
73
+
74
+ {/* Title block — `xl` top padding pushes the title down so the row
75
+ bottom-aligns the title, back chevron, and ⋯ menu. */}
76
+ <div className="flex flex-1 min-w-0 items-end gap-[var(--spacing-system-m)] pt-[var(--spacing-system-xl)]">
77
+ <div className="flex flex-1 min-w-0 flex-col gap-[var(--spacing-system-xs)]">
78
+ <p className="min-w-0 truncate text-h2 text-ods-text-primary" title={title}>
79
+ {title}
80
+ </p>
81
+ </div>
82
+
83
+ {menuItems.length > 0 && (
84
+ <ActionsMenuDropdown
85
+ groups={[{ items: menuItems }]}
86
+ triggerAriaLabel="Chat actions"
87
+ triggerClassName="shrink-0 bg-ods-card border-ods-border hover:bg-ods-bg-hover flex items-center justify-center focus-visible:ring-0"
88
+ />
89
+ )}
90
+ </div>
91
+ </div>
92
+ </div>
93
+ )
94
+ }
@@ -9,7 +9,16 @@ import {
9
9
  } from '../icons-v2-generated'
10
10
  import { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon'
11
11
  import { ChatHeaderIconButton } from './chat-header-icon-button'
12
- import { MoreActionsMenu } from '../ui/more-actions-menu'
12
+ import { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu'
13
+ import { ChatPanelHeaderMobile } from './chat-panel-header-mobile'
14
+
15
+ /**
16
+ * Compact rounded header button used for the desktop back chevron. Borderless
17
+ * square with a hover background. (Mobile uses its own header component —
18
+ * `ChatPanelHeaderMobile`.)
19
+ */
20
+ export const COMPACT_HEADER_BUTTON =
21
+ 'inline-flex shrink-0 items-center justify-center size-8 rounded-md text-ods-text-secondary transition-colors hover:bg-ods-bg-hover hover:text-ods-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent'
13
22
 
14
23
  export interface ChatPanelHeaderProps {
15
24
  /** Show the back-chevron + bold (h3) title. When false, the static list
@@ -53,62 +62,81 @@ export function ChatPanelHeader({
53
62
  onArchive,
54
63
  onOpenArchive,
55
64
  }: ChatPanelHeaderProps) {
65
+ // Desktop ⋯ menu (active, non-archived conversation only) — rename / archive.
56
66
  const menuItems = [
57
- onRename && { label: 'Rename chat', onClick: onRename },
58
- onArchive && { label: 'Archive chat', onClick: onArchive },
59
- ].filter(Boolean) as { label: string; onClick: () => void }[]
67
+ onRename && { id: 'rename', label: 'Rename chat', onClick: onRename },
68
+ onArchive && { id: 'archive', label: 'Archive chat', onClick: onArchive },
69
+ ].filter(Boolean) as ActionsMenuItem[]
60
70
 
61
71
  return (
62
- <div className="flex-shrink-0 flex h-14 w-full overflow-hidden border-b border-ods-border bg-ods-card">
63
- <div className="flex flex-1 min-w-0 items-center gap-2 px-4 py-3">
64
- {showBack ? (
65
- <>
66
- <button
67
- type="button"
68
- onClick={onBack}
69
- aria-label={backAriaLabel}
70
- className="inline-flex shrink-0 items-center justify-center size-8 -ml-1 rounded-md text-ods-text-secondary transition-colors hover:bg-ods-bg-hover hover:text-ods-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent"
71
- >
72
- <Chevron01LeftIcon size={20} />
73
- </button>
74
- <span className="truncate text-h3 text-ods-text-primary">{title}</span>
75
- </>
76
- ) : (
77
- <p className="truncate text-h4 text-ods-text-primary">{title}</p>
72
+ <>
73
+ {/* Mobile (<md): a distinct layout (Figma node 7363:85532). */}
74
+ <ChatPanelHeaderMobile
75
+ className="flex md:hidden"
76
+ showBack={showBack}
77
+ title={title}
78
+ backAriaLabel={backAriaLabel}
79
+ isArchivedView={isArchivedView}
80
+ onBack={onBack}
81
+ onClose={onClose}
82
+ onRestore={onRestore}
83
+ onRename={onRename}
84
+ onArchive={onArchive}
85
+ onOpenArchive={onOpenArchive}
86
+ />
87
+
88
+ {/* Desktop (md+): fixed-height bar with full-height divider action cells. */}
89
+ <div className="hidden md:flex flex-shrink-0 h-14 w-full overflow-hidden border-b border-ods-border bg-ods-card">
90
+ <div className="flex flex-1 min-w-0 items-center gap-2 px-4 py-3">
91
+ {showBack ? (
92
+ <>
93
+ <button
94
+ type="button"
95
+ onClick={onBack}
96
+ aria-label={backAriaLabel}
97
+ className={`${COMPACT_HEADER_BUTTON} -ml-1`}
98
+ >
99
+ <Chevron01LeftIcon size={20} />
100
+ </button>
101
+ <span className="truncate text-h3 text-ods-text-primary">{title}</span>
102
+ </>
103
+ ) : (
104
+ <p className="truncate text-h3 text-ods-text-primary">{title}</p>
105
+ )}
106
+ </div>
107
+
108
+ {/* Restore (refresh) — archived chat header (Figma node 7361:425441). */}
109
+ {isArchivedView && onRestore && (
110
+ <ChatHeaderIconButton onClick={onRestore} aria-label="Unarchive chat">
111
+ <Refresh01LeftIcon size={24} />
112
+ </ChatHeaderIconButton>
78
113
  )}
79
- </div>
80
114
 
81
- {/* Restore (refresh) — archived chat header (Figma node 7361:425441). */}
82
- {isArchivedView && onRestore && (
83
- <ChatHeaderIconButton onClick={onRestore} aria-label="Unarchive chat">
84
- <Refresh01LeftIcon size={24} />
85
- </ChatHeaderIconButton>
86
- )}
115
+ {/* Rename / Archive menu — active (non-archived) conversation only. */}
116
+ {showBack && !isArchivedView && menuItems.length > 0 && (
117
+ <ActionsMenuDropdown
118
+ triggerAriaLabel="Chat actions"
119
+ onCloseAutoFocus={(e) => e.preventDefault()}
120
+ groups={[{ items: menuItems }]}
121
+ customTrigger={
122
+ <ChatHeaderIconButton aria-label="Chat actions">
123
+ <Ellipsis01Icon size={24} />
124
+ </ChatHeaderIconButton>
125
+ }
126
+ />
127
+ )}
87
128
 
88
- {/* Rename / Archive menu — active (non-archived) conversation only. */}
89
- {showBack && !isArchivedView && menuItems.length > 0 && (
90
- <MoreActionsMenu
91
- ariaLabel="Chat actions"
92
- onCloseAutoFocus={(e) => e.preventDefault()}
93
- items={menuItems}
94
- trigger={
95
- <ChatHeaderIconButton aria-label="Chat actions">
96
- <Ellipsis01Icon size={24} />
97
- </ChatHeaderIconButton>
98
- }
99
- />
100
- )}
129
+ {/* Chat Archive entry (Figma node 7532:225034) — list view only. */}
130
+ {!showBack && onOpenArchive && (
131
+ <ChatHeaderIconButton onClick={onOpenArchive} aria-label="Chat archive">
132
+ <ClockHistoryIcon size={24} />
133
+ </ChatHeaderIconButton>
134
+ )}
101
135
 
102
- {/* Chat Archive entry (Figma node 7532:225034) — list view only. */}
103
- {!showBack && onOpenArchive && (
104
- <ChatHeaderIconButton onClick={onOpenArchive} aria-label="Chat archive">
105
- <ClockHistoryIcon size={24} />
136
+ <ChatHeaderIconButton onClick={onClose} aria-label="Close">
137
+ <XmarkIcon size={24} />
106
138
  </ChatHeaderIconButton>
107
- )}
108
-
109
- <ChatHeaderIconButton onClick={onClose} aria-label="Close">
110
- <XmarkIcon size={24} />
111
- </ChatHeaderIconButton>
112
- </div>
139
+ </div>
140
+ </>
113
141
  )
114
142
  }