@flamingo-stack/openframe-frontend-core 0.0.290 → 0.0.291-snapshot.20260618233000

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 (138) hide show
  1. package/dist/{chunk-EL5GCMPU.cjs → chunk-2BMVBPC7.cjs} +9 -9
  2. package/dist/{chunk-EL5GCMPU.cjs.map → chunk-2BMVBPC7.cjs.map} +1 -1
  3. package/dist/{chunk-OYXZIPNM.cjs → chunk-2NJ44RTT.cjs} +27 -27
  4. package/dist/{chunk-OYXZIPNM.cjs.map → chunk-2NJ44RTT.cjs.map} +1 -1
  5. package/dist/{chunk-R4CLIWAU.js → chunk-5FK7X3EE.js} +270 -172
  6. package/dist/chunk-5FK7X3EE.js.map +1 -0
  7. package/dist/{chunk-3SDBXXDP.cjs → chunk-5PELVUFT.cjs} +26 -26
  8. package/dist/{chunk-3SDBXXDP.cjs.map → chunk-5PELVUFT.cjs.map} +1 -1
  9. package/dist/{chunk-2V6RCQ5M.cjs → chunk-5R5OODNE.cjs} +40 -40
  10. package/dist/{chunk-2V6RCQ5M.cjs.map → chunk-5R5OODNE.cjs.map} +1 -1
  11. package/dist/{chunk-ODR6A6FC.js → chunk-6FHO73AP.js} +22 -10
  12. package/dist/{chunk-ODR6A6FC.js.map → chunk-6FHO73AP.js.map} +1 -1
  13. package/dist/{chunk-KJF7SRKH.js → chunk-B2U6INNO.js} +3 -3
  14. package/dist/{chunk-4F3X2AOB.js → chunk-C667P6LZ.js} +5 -5
  15. package/dist/{chunk-UC5GB255.cjs → chunk-CDJOKNCS.cjs} +17 -17
  16. package/dist/{chunk-UC5GB255.cjs.map → chunk-CDJOKNCS.cjs.map} +1 -1
  17. package/dist/{chunk-7NM7DEUK.js → chunk-CUQH4SHH.js} +2 -2
  18. package/dist/{chunk-ZLN6SM2U.js → chunk-DUIWR7RQ.js} +3 -3
  19. package/dist/{chunk-4XMYOZFO.js → chunk-E2YXRSDG.js} +5 -5
  20. package/dist/{chunk-AAK6IY6Y.cjs → chunk-FFP2A77V.cjs} +10 -10
  21. package/dist/{chunk-AAK6IY6Y.cjs.map → chunk-FFP2A77V.cjs.map} +1 -1
  22. package/dist/{chunk-Z5QIVHJW.js → chunk-HTYUZXQP.js} +5 -5
  23. package/dist/{chunk-LVOBI2M5.js → chunk-IXDTNQF4.js} +3 -3
  24. package/dist/{chunk-I6ZPGKZ2.cjs → chunk-JC5RN7ZS.cjs} +6 -6
  25. package/dist/{chunk-I6ZPGKZ2.cjs.map → chunk-JC5RN7ZS.cjs.map} +1 -1
  26. package/dist/{chunk-VJ4ZWD5G.cjs → chunk-MDLWEJAV.cjs} +1072 -974
  27. package/dist/chunk-MDLWEJAV.cjs.map +1 -0
  28. package/dist/{chunk-R2KT5GDD.js → chunk-N45M3TK3.js} +14 -4
  29. package/dist/chunk-N45M3TK3.js.map +1 -0
  30. package/dist/{chunk-EI4WALN2.cjs → chunk-OXOTKEYY.cjs} +39 -29
  31. package/dist/chunk-OXOTKEYY.cjs.map +1 -0
  32. package/dist/{chunk-7L22MF3U.cjs → chunk-PZZGDS5I.cjs} +17 -17
  33. package/dist/{chunk-7L22MF3U.cjs.map → chunk-PZZGDS5I.cjs.map} +1 -1
  34. package/dist/{chunk-VRSXJ5QJ.js → chunk-SLP4KXP6.js} +3 -2
  35. package/dist/chunk-SLP4KXP6.js.map +1 -0
  36. package/dist/{chunk-7EYWERFT.js → chunk-VK4B6UGU.js} +4 -4
  37. package/dist/{chunk-D6RK5YXX.cjs → chunk-Z6BK4XHH.cjs} +22 -10
  38. package/dist/chunk-Z6BK4XHH.cjs.map +1 -0
  39. package/dist/{chunk-Y4JNA4W6.cjs → chunk-ZHNL2IPK.cjs} +3 -2
  40. package/dist/chunk-ZHNL2IPK.cjs.map +1 -0
  41. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  42. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  43. package/dist/components/chat/embeddable-chat.d.ts +15 -0
  44. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  45. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
  46. package/dist/components/chat/index.cjs +7 -5
  47. package/dist/components/chat/index.cjs.map +1 -1
  48. package/dist/components/chat/index.d.ts +1 -0
  49. package/dist/components/chat/index.d.ts.map +1 -1
  50. package/dist/components/chat/index.js +6 -4
  51. package/dist/components/chat/remark-mention-chips.d.ts +30 -0
  52. package/dist/components/chat/remark-mention-chips.d.ts.map +1 -0
  53. package/dist/components/chat/types/api.types.d.ts +4 -0
  54. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  55. package/dist/components/chat/types/component.types.d.ts +24 -0
  56. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  57. package/dist/components/chat/types/context-item.types.d.ts +5 -0
  58. package/dist/components/chat/types/context-item.types.d.ts.map +1 -1
  59. package/dist/components/chat/types/processing.types.d.ts +4 -0
  60. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  61. package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -1
  62. package/dist/components/chat/utils/nav-anchor-props.d.ts +8 -3
  63. package/dist/components/chat/utils/nav-anchor-props.d.ts.map +1 -1
  64. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  65. package/dist/components/contact/index.cjs +6 -6
  66. package/dist/components/contact/index.js +5 -5
  67. package/dist/components/docs/index.cjs +5 -5
  68. package/dist/components/docs/index.js +4 -4
  69. package/dist/components/embeds/index.cjs +6 -6
  70. package/dist/components/embeds/index.js +5 -5
  71. package/dist/components/faq/index.cjs +6 -6
  72. package/dist/components/faq/index.js +5 -5
  73. package/dist/components/features/index.cjs +5 -5
  74. package/dist/components/features/index.js +4 -4
  75. package/dist/components/features/paths-display.d.ts +1 -1
  76. package/dist/components/features/paths-display.d.ts.map +1 -1
  77. package/dist/components/index.cjs +178 -176
  78. package/dist/components/index.cjs.map +1 -1
  79. package/dist/components/index.js +13 -11
  80. package/dist/components/index.js.map +1 -1
  81. package/dist/components/navigation/index.cjs +5 -5
  82. package/dist/components/navigation/index.js +4 -4
  83. package/dist/components/onboarding-guides/index.cjs +24 -24
  84. package/dist/components/onboarding-guides/index.js +4 -4
  85. package/dist/components/related-content/index.cjs +6 -6
  86. package/dist/components/related-content/index.js +5 -5
  87. package/dist/components/tickets/index.cjs +63 -63
  88. package/dist/components/tickets/index.js +6 -6
  89. package/dist/components/ui/index.cjs +7 -5
  90. package/dist/components/ui/index.cjs.map +1 -1
  91. package/dist/components/ui/index.js +6 -4
  92. package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
  93. package/dist/components/ui/tag.d.ts +10 -1
  94. package/dist/components/ui/tag.d.ts.map +1 -1
  95. package/dist/index.cjs +7 -5
  96. package/dist/index.cjs.map +1 -1
  97. package/dist/index.js +6 -4
  98. package/dist/utils/index.cjs +21 -9
  99. package/dist/utils/index.cjs.map +1 -1
  100. package/dist/utils/index.js +21 -9
  101. package/dist/utils/index.js.map +1 -1
  102. package/dist/utils/scroll-into-view.d.ts +12 -0
  103. package/dist/utils/scroll-into-view.d.ts.map +1 -1
  104. package/package.json +1 -1
  105. package/src/components/chat/chat-message-enhanced.tsx +71 -9
  106. package/src/components/chat/chat-message-list.tsx +2 -0
  107. package/src/components/chat/embeddable-chat.tsx +50 -6
  108. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +1 -0
  109. package/src/components/chat/index.ts +1 -0
  110. package/src/components/chat/remark-mention-chips.ts +72 -0
  111. package/src/components/chat/types/api.types.ts +1 -1
  112. package/src/components/chat/types/component.types.ts +18 -0
  113. package/src/components/chat/types/context-item.types.ts +5 -0
  114. package/src/components/chat/types/processing.types.ts +8 -1
  115. package/src/components/chat/utils/chunk-parser.ts +11 -0
  116. package/src/components/chat/utils/nav-anchor-props.ts +22 -4
  117. package/src/components/chat/utils/process-historical-messages.ts +22 -0
  118. package/src/components/features/.paths-display.md +1 -1
  119. package/src/components/features/command-box.tsx +1 -1
  120. package/src/components/features/paths-display.tsx +13 -14
  121. package/src/components/ui/simple-markdown-renderer.tsx +14 -11
  122. package/src/components/ui/tag.tsx +12 -2
  123. package/src/utils/scroll-into-view.ts +51 -9
  124. package/dist/chunk-D6RK5YXX.cjs.map +0 -1
  125. package/dist/chunk-EI4WALN2.cjs.map +0 -1
  126. package/dist/chunk-R2KT5GDD.js.map +0 -1
  127. package/dist/chunk-R4CLIWAU.js.map +0 -1
  128. package/dist/chunk-VJ4ZWD5G.cjs.map +0 -1
  129. package/dist/chunk-VRSXJ5QJ.js.map +0 -1
  130. package/dist/chunk-Y4JNA4W6.cjs.map +0 -1
  131. /package/dist/{chunk-KJF7SRKH.js.map → chunk-B2U6INNO.js.map} +0 -0
  132. /package/dist/{chunk-4F3X2AOB.js.map → chunk-C667P6LZ.js.map} +0 -0
  133. /package/dist/{chunk-7NM7DEUK.js.map → chunk-CUQH4SHH.js.map} +0 -0
  134. /package/dist/{chunk-ZLN6SM2U.js.map → chunk-DUIWR7RQ.js.map} +0 -0
  135. /package/dist/{chunk-4XMYOZFO.js.map → chunk-E2YXRSDG.js.map} +0 -0
  136. /package/dist/{chunk-Z5QIVHJW.js.map → chunk-HTYUZXQP.js.map} +0 -0
  137. /package/dist/{chunk-LVOBI2M5.js.map → chunk-IXDTNQF4.js.map} +0 -0
  138. /package/dist/{chunk-7EYWERFT.js.map → chunk-VK4B6UGU.js.map} +0 -0
@@ -33,6 +33,18 @@
33
33
  *
34
34
  * Honors `prefers-reduced-motion` (jumps instantly) and cancels on genuine user
35
35
  * scroll intent (wheel / touch) so we never fight the user.
36
+ *
37
+ * WINDOW *OR* A SCROLLABLE ANCESTOR: the helper is not hard-wired to the window
38
+ * scroller. It walks up from the target to the nearest ancestor that is an
39
+ * actual scroll container (`overflow-y: auto | scroll | overlay` AND
40
+ * `scrollHeight > clientHeight`) and drives THAT element; only when none exists
41
+ * does it fall back to `window`. This is what makes it work inside app shells
42
+ * that put page content in a fixed-height `<main class="overflow-y-auto">`
43
+ * (e.g. OpenFrame's `AppLayout`) where the document/window never scrolls — the
44
+ * old window-only version was a silent no-op there. Note `overflow: clip` /
45
+ * `hidden` are deliberately NOT treated as scroll containers, so a list wrapper
46
+ * that uses `overflow-clip` only to round its corners still bubbles the scroll
47
+ * up to the real container (matches the `<HelpCenterCard>` list intent).
36
48
  */
37
49
  export interface ScrollElementIntoViewOptions {
38
50
  /** Pixels to subtract from the target element's `top` so it lands BELOW
@@ -1 +1 @@
1
- {"version":3,"file":"scroll-into-view.d.ts","sourceRoot":"","sources":["../../src/utils/scroll-into-view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,MAAM,WAAW,4BAA4B;IAC3C;+EAC2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;oFACgF;IAChF,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB;;;6EAGyE;IACzE,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAA;IAC9C,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAqBD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,EACtC,OAAO,GAAE,4BAAiC,GACzC,IAAI,CAmEN"}
1
+ {"version":3,"file":"scroll-into-view.d.ts","sourceRoot":"","sources":["../../src/utils/scroll-into-view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH,MAAM,WAAW,4BAA4B;IAC3C;+EAC2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;oFACgF;IAChF,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB;;;6EAGyE;IACzE,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAA;IAC9C,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAsCD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,EACtC,OAAO,GAAE,4BAAiC,GACzC,IAAI,CAgFN"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.290",
3
+ "version": "0.0.291-snapshot.20260618233000",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,10 +12,21 @@ import { ThinkingDisplay } from "./thinking-display"
12
12
  import { SimpleMarkdownRenderer } from "../ui/simple-markdown-renderer"
13
13
  import type { ChatRef } from "./chat-ref.types"
14
14
  import { remarkCardLinks } from "./remark-card-links"
15
+ import { remarkMentionChips } from "./remark-mention-chips"
15
16
  import { BlockCard, type BlockCardProps } from "./entity-cards/block-card"
16
17
  import { ChatContextChipStrip } from "./chat-context-picker"
17
18
  import type { MessageSegment, MessageContent, ChatMessageEnhancedProps } from "./types"
18
19
 
20
+ /** Inline `@marker:id` mention token in the message body (sibling of the
21
+ * `[card://]` grammar) — used to filter out items rendered inline from the
22
+ * chip strip below. MUST mirror the left-boundary `(^|\s)` of `MENTION_REGEX`
23
+ * in `remark-mention-chips.ts`: without it this regex is WIDER than the plugin
24
+ * (e.g. it matches `x@device:1` mid-word, which the plugin skips), so a context
25
+ * item would be stripped from the chip strip yet never rendered inline — lost
26
+ * from display entirely. The id is capture group 2 (group 1 is the boundary).
27
+ * Marker lowercase; id is the mention-token charset. */
28
+ const MENTION_MARKER_REGEX = /(^|\s)@[a-z]+:([A-Za-z0-9_.+/=-]+)/g
29
+
19
30
  /**
20
31
  * Same regex shape as `remarkCardLinks` — kept in lockstep so the
21
32
  * pre-scan and the remark plugin see the SAME set of markers. If the
@@ -32,7 +43,7 @@ function normalizeContent(content: MessageContent): MessageSegment[] {
32
43
  }
33
44
 
34
45
  const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>(
35
- ({ className, role, content, name, avatar, isTyping = false, timestamp, showAvatar = true, assistantType, authorType: authorTypeProp, assistantIcon, chatRefs, contextItems, resolveContextIcon, renderEntityCard, NavLinkAnchor, ...props }, ref) => {
46
+ ({ className, role, content, name, avatar, isTyping = false, timestamp, showAvatar = true, assistantType, authorType: authorTypeProp, assistantIcon, chatRefs, contextItems, resolveContextIcon, renderMention, renderEntityCard, NavLinkAnchor, ...props }, ref) => {
36
47
  const isUser = role === 'user'
37
48
  const isError = role === 'error'
38
49
  const authorType = authorTypeProp ?? (isUser ? 'user' : assistantType === 'mingo' ? 'mingo' : 'fae')
@@ -51,13 +62,48 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
51
62
  // title — or, if even the ref is unknown, the bare cardId. Never
52
63
  // renders the literal `[card://...]` URL.
53
64
  const hasMarkerSupport = !!chatRefs || !!renderEntityCard
54
- const cardRemarkPlugins = useMemo(
55
- () => (hasMarkerSupport ? [remarkCardLinks] : []),
56
- [hasMarkerSupport],
57
- )
58
65
 
59
66
  const segments = useMemo(() => normalizeContent(content), [content])
60
67
 
68
+ // Inline `@marker:id` mentions: the composer commits these tokens when the
69
+ // user picks context via the `@`-flow, and the ASSISTANT routinely echoes
70
+ // the same `@device:machineId` token back in its reply. The lib detects the
71
+ // token and delegates rendering to the host's `renderMention` (mirror of
72
+ // `renderEntityCard`): the host returns a SELF-FETCHING chip per entity
73
+ // type. Enabled whenever the host opts in by supplying `renderMention`.
74
+ const hasMentionSupport = !!renderMention
75
+
76
+ // Ids that appear as `@marker:id` in the body → rendered inline, so they are
77
+ // excluded from the chip strip below (no duplicate inline + strip).
78
+ const inlineMentionIds = useMemo(() => {
79
+ const ids = new Set<string>()
80
+ if (!hasMentionSupport) return ids
81
+ for (const seg of segments) {
82
+ if (seg.type !== 'text' || !seg.text || !seg.text.includes('@')) continue
83
+ for (const mm of seg.text.matchAll(MENTION_MARKER_REGEX)) ids.add(mm[2])
84
+ }
85
+ return ids
86
+ }, [hasMentionSupport, segments])
87
+
88
+ // Chip strip = only context NOT already shown inline (e.g. `+`-added items).
89
+ const stripContextItems = useMemo(
90
+ () =>
91
+ inlineMentionIds.size > 0 && contextItems
92
+ ? contextItems.filter((it) => !inlineMentionIds.has(it.id))
93
+ : contextItems,
94
+ [contextItems, inlineMentionIds],
95
+ )
96
+
97
+ // Markdown plugins per message: card markers (assistant) + mention tokens
98
+ // (user). Each is gated independently so neither fires without its data.
99
+ const cardRemarkPlugins = useMemo(
100
+ () => [
101
+ ...(hasMarkerSupport ? [remarkCardLinks] : []),
102
+ ...(hasMentionSupport ? [remarkMentionChips] : []),
103
+ ],
104
+ [hasMarkerSupport, hasMentionSupport],
105
+ )
106
+
61
107
  // Cross-render cache of rendered inline-card nodes, keyed by `type:id`.
62
108
  // A fetch-mode card lives inside the assistant message, which re-renders on
63
109
  // every stream chunk (and the whole list re-renders when a new message
@@ -230,7 +276,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
230
276
  }, [hasMarkerSupport, chatRefs, renderEntityCard, segments])
231
277
 
232
278
  const cardComponentOverrides = useMemo(() => {
233
- if (!hasMarkerSupport) return undefined
279
+ if (!hasMarkerSupport && !hasMentionSupport) return undefined
234
280
  const refs = chatRefs ?? {}
235
281
  const inlineByKey = renderingPlan?.inlineByKey
236
282
  return {
@@ -240,6 +286,19 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
240
286
  // paragraph as siblings — the inline pill stays at the marker
241
287
  // position. Other href schemes pass through unchanged.
242
288
  a: ({ href, children, className: linkClassName, ...rest }: any) => {
289
+ // Inline entity mention `@marker:id`, emitted as a `mention://marker:id`
290
+ // link by `remarkMentionChips`. Delegate to the host's `renderMention`
291
+ // (mirror of the `card://` → `renderEntityCard` path): the host returns
292
+ // a self-fetching chip for that entity type. Null/unknown → raw token.
293
+ if (typeof href === 'string' && href.startsWith('mention://')) {
294
+ const stripped = href.slice('mention://'.length)
295
+ const sepIdx = stripped.indexOf(':')
296
+ const marker = sepIdx === -1 ? stripped : stripped.slice(0, sepIdx)
297
+ const id = sepIdx === -1 ? '' : stripped.slice(sepIdx + 1)
298
+ const node = renderMention?.({ marker, id })
299
+ if (node != null) return <>{node}</>
300
+ return <span className="text-ods-text-secondary opacity-60">{children}</span>
301
+ }
243
302
  if (typeof href === 'string' && href.startsWith('card://')) {
244
303
  const stripped = href.slice('card://'.length)
245
304
  const sepIdx = stripped.lastIndexOf(':')
@@ -313,7 +372,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
313
372
  )
314
373
  },
315
374
  }
316
- }, [hasMarkerSupport, chatRefs, renderingPlan, NavLinkAnchor])
375
+ }, [hasMarkerSupport, hasMentionSupport, renderMention, chatRefs, renderingPlan, NavLinkAnchor])
317
376
 
318
377
  const getAvatarProps = () => {
319
378
  const displayName = name || (isUser ? "User" : assistantType === 'mingo' ? "Mingo" : "Fae")
@@ -529,9 +588,9 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
529
588
 
530
589
  {/* Attached entity-context chips (user bubbles). Read-only — no
531
590
  remove affordance once the message is sent (Figma 31:28709). */}
532
- {contextItems && contextItems.length > 0 && (
591
+ {stripContextItems && stripContextItems.length > 0 && (
533
592
  <ChatContextChipStrip
534
- items={contextItems}
593
+ items={stripContextItems}
535
594
  resolveIcon={resolveContextIcon}
536
595
  className="mt-2"
537
596
  />
@@ -566,6 +625,9 @@ const MemoizedChatMessageEnhanced = memo(ChatMessageEnhanced, (prevProps, nextPr
566
625
  // message (it's set once on the optimistic send and never mutated).
567
626
  prevProps.contextItems === nextProps.contextItems &&
568
627
  prevProps.resolveContextIcon === nextProps.resolveContextIcon &&
628
+ // Host keeps this stable (module const / useCallback), so reference
629
+ // equality holds across streaming chunks.
630
+ prevProps.renderMention === nextProps.renderMention &&
569
631
  prevProps.renderEntityCard === nextProps.renderEntityCard &&
570
632
  prevProps.NavLinkAnchor === nextProps.NavLinkAnchor
571
633
  )
@@ -107,6 +107,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
107
107
  onLoadMore,
108
108
  renderEntityCard,
109
109
  resolveContextIcon,
110
+ renderMention,
110
111
  NavLinkAnchor,
111
112
  ...props
112
113
  },
@@ -538,6 +539,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
538
539
  chatRefs={message.chatRefs}
539
540
  contextItems={message.contextItems}
540
541
  resolveContextIcon={resolveContextIcon}
542
+ renderMention={renderMention}
541
543
  renderEntityCard={renderEntityCard}
542
544
  NavLinkAnchor={NavLinkAnchor}
543
545
  />
@@ -259,6 +259,18 @@ export interface EmbeddableChatProps {
259
259
  * `config.search`. Omit to disable the feature entirely.
260
260
  */
261
261
  contextPicker?: ChatContextPickerConfig
262
+ /**
263
+ * Host renderer for inline AI mentions `@marker:id` (e.g. the assistant
264
+ * echoing `@device:<machineId>` in its reply). DIRECT MIRROR of
265
+ * `renderEntityCard` for the `[card://]` grammar: the lib detects the token,
266
+ * parses `{marker, id}`, and renders whatever the host returns — typically a
267
+ * SELF-FETCHING chip (each entity type has its own fetcher) that resolves its
268
+ * own display name by id. SEPARATE from `contextPicker`/`contextItems` (the
269
+ * USER's attachments). Keep the function identity stable (module const /
270
+ * `useCallback`) so the thread's streaming memo holds. Return null for a
271
+ * marker the host can't render → the lib falls back to the bare token.
272
+ */
273
+ renderMention?: (reference: { marker: string; id: string }) => React.ReactNode
262
274
  }
263
275
 
264
276
  // =============================================================================
@@ -269,6 +281,22 @@ export interface EmbeddableChatProps {
269
281
  const formatRelativePath = (p: string): string =>
270
282
  p.replace(/^\/+/, '').replace(/\/+$/, '')
271
283
 
284
+ /**
285
+ * The committed `@`-mention text token, derived from a `type:id` identity key.
286
+ * The TYPE is replaced with its backend mention MARKER (`markerByType`, e.g.
287
+ * `KB_ARTICLE → 'kb'`), falling back to a lowercased type when the host didn't
288
+ * declare one (`@device:…`, not `@DEVICE:…`); the `id` is left verbatim. The
289
+ * structured context items keep the original (upper) entity-kind for the wire
290
+ * enum, so this only affects the inline draft token. `[A-Za-z…]` in the input
291
+ * regex matches either case, so commit/strip/detect stay symmetric.
292
+ */
293
+ const mentionTokenOf = (key: string, markerByType: Map<string, string>): string => {
294
+ const ci = key.indexOf(':')
295
+ const type = ci === -1 ? key : key.slice(0, ci)
296
+ const id = ci === -1 ? '' : key.slice(ci + 1)
297
+ return `${markerByType.get(type) ?? type.toLowerCase()}:${id}`
298
+ }
299
+
272
300
  /**
273
301
  * Fallback fan-out when the model didn't cite any source. Show the top-N
274
302
  * retrieved sources instead of zero chips. Mirrors Perplexity's behavior.
@@ -662,6 +690,7 @@ function EmbeddableChatInner({
662
690
  mingoWelcome,
663
691
  guideWelcome,
664
692
  contextPicker,
693
+ renderMention,
665
694
  }: EmbeddableChatProps) {
666
695
  // `shell === 'none'` means the consumer hosts us inside their own panel
667
696
  // (e.g. AppLayoutDrawer in openframe-frontend). Several drawer-shell
@@ -972,6 +1001,15 @@ function EmbeddableChatInner({
972
1001
  mentionKeyRef.current = null
973
1002
  }, [activeDialogId, activeMode])
974
1003
 
1004
+ // Map each entity type to its backend mention marker (host-declared on the
1005
+ // entity type). Drives the committed `@marker:id` token; missing markers fall
1006
+ // back to a lowercased type inside `mentionTokenOf`.
1007
+ const mentionMarkerByType = useMemo(() => {
1008
+ const m = new Map<string, string>()
1009
+ for (const t of contextPicker?.entityTypes ?? []) if (t.marker) m.set(t.type, t.marker)
1010
+ return m
1011
+ }, [contextPicker?.entityTypes])
1012
+
975
1013
  const toggleContextItem = useCallback(
976
1014
  (item: ChatContextItem) => {
977
1015
  const key = `${item.type}:${item.id}`
@@ -990,7 +1028,7 @@ function EmbeddableChatInner({
990
1028
  )
991
1029
  const grows = !alreadySelected && !mentionKeyRef.current
992
1030
  if (grows && contextItems.length >= contextMaxItems) return
993
- chatInputRef.current?.commitMention(key)
1031
+ chatInputRef.current?.commitMention(mentionTokenOf(key, mentionMarkerByType))
994
1032
  setContextItems((prev) => {
995
1033
  const withoutPrevMention = mentionKeyRef.current
996
1034
  ? prev.filter((p) => `${p.type}:${p.id}` !== mentionKeyRef.current)
@@ -1010,7 +1048,7 @@ function EmbeddableChatInner({
1010
1048
  return [...prev, item]
1011
1049
  })
1012
1050
  },
1013
- [contextMaxItems, contextItems],
1051
+ [contextMaxItems, contextItems, mentionMarkerByType],
1014
1052
  )
1015
1053
 
1016
1054
  const removeContextItem = useCallback((item: ChatContextItem) => {
@@ -1022,10 +1060,10 @@ function EmbeddableChatInner({
1022
1060
  if (mentionKeyRef.current === key) {
1023
1061
  mentionKeyRef.current = null
1024
1062
  const cur = chatInputRef.current?.getValue() ?? ''
1025
- const next = cur.replace(`@${key}`, '').replace(/\s{2,}/g, ' ').trimStart()
1063
+ const next = cur.replace(`@${mentionTokenOf(key, mentionMarkerByType)}`, '').replace(/\s{2,}/g, ' ').trimStart()
1026
1064
  chatInputRef.current?.setValue(next)
1027
1065
  }
1028
- }, [])
1066
+ }, [mentionMarkerByType])
1029
1067
 
1030
1068
  // Draft → context reconciliation: when the user deletes the `@type:id` token
1031
1069
  // text, drop the matching mention item. `+`-added items have no token in the
@@ -1033,11 +1071,11 @@ function EmbeddableChatInner({
1033
1071
  const handleContextValueChange = useCallback((value: string) => {
1034
1072
  const key = mentionKeyRef.current
1035
1073
  if (!key) return
1036
- if (!value.includes(`@${key}`)) {
1074
+ if (!value.includes(`@${mentionTokenOf(key, mentionMarkerByType)}`)) {
1037
1075
  setContextItems((prev) => prev.filter((p) => `${p.type}:${p.id}` !== key))
1038
1076
  mentionKeyRef.current = null
1039
1077
  }
1040
- }, [])
1078
+ }, [mentionMarkerByType])
1041
1079
 
1042
1080
  const openContextPicker = useCallback(() => {
1043
1081
  setMentionQuery(null)
@@ -1078,6 +1116,11 @@ function EmbeddableChatInner({
1078
1116
  return (item: ChatContextItem) => byType.get(item.type)
1079
1117
  }, [contextEntityTypes])
1080
1118
 
1119
+ // `renderMention` is HOST-provided (see the prop doc): the per-type renderer
1120
+ // for inline AI mentions `@marker:id` (mirror of `renderEntityCard`). The lib
1121
+ // forwards it verbatim to the message list and calls it from the `mention://`
1122
+ // override; the host returns a self-fetching chip.
1123
+
1081
1124
  // Resolve base route. Hub default mapping: flamingo → /knowledge-base,
1082
1125
  // anything else → /data-room. Embedders override per platform. An embedder that
1083
1126
  // doesn't host an in-app doc viewer should NOT pass an empty baseRoute (that just
@@ -1515,6 +1558,7 @@ function EmbeddableChatInner({
1515
1558
  assistantIcon={mingoAssistantIcon}
1516
1559
  renderEntityCard={renderEntityCard}
1517
1560
  resolveContextIcon={resolveContextIcon}
1561
+ renderMention={renderMention}
1518
1562
  NavLinkAnchor={NavLinkAnchorViaRuntime}
1519
1563
  className="flex-1"
1520
1564
  // No inner `px`/`pb`: the panel wrapper already pads with
@@ -365,6 +365,7 @@ export function useRealtimeChunkProcessor(
365
365
  displayName: action.displayName,
366
366
  userId: action.userId,
367
367
  streamSeq,
368
+ contextItems: action.contextItems,
368
369
  })
369
370
  break
370
371
 
@@ -38,6 +38,7 @@ export * from './model-display'
38
38
  export * from './chat-sidebar'
39
39
  export type { ChatRef } from './chat-ref.types'
40
40
  export { remarkCardLinks } from './remark-card-links'
41
+ export { remarkMentionChips } from './remark-mention-chips'
41
42
 
42
43
  // Card-supporting UI migrated from hub `components/shared/*` + `components/blog/*`
43
44
  export {
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Remark plugin that walks markdown text leaves and replaces inline entity
3
+ * mentions `@<marker>:<id>` (e.g. `@device:64f0a1`, `@kb:5`) with synthetic
4
+ * `link` mdast nodes whose `url` is `mention://<marker>:<id>` (a non-standard
5
+ * scheme). The downstream `<a>` component override in `chat-message-enhanced`
6
+ * detects that scheme and renders an inline `<Tag>`-style chip, resolving the
7
+ * display name from the message's `contextItems`.
8
+ *
9
+ * Sibling of `remark-card-links` (which handles the ASSISTANT-side
10
+ * `[card://type:id]` markers). This one handles the USER-side `@marker:id`
11
+ * tokens the composer commits when picking context via the `@`-mention flow.
12
+ *
13
+ * Why a remark plugin (NOT a regex on the raw markdown source): it splits only
14
+ * TEXT leaves, so bold / italic / list / code wrappers around a mention stay
15
+ * intact, and the marker disappears before any rehype-raw HTML pass.
16
+ *
17
+ * Grammar:
18
+ * - marker: `[a-z]+` — the backend `ContextItemType.marker()` short form
19
+ * (`device`, `script`, `ticket`, `organization`, `user`, `kb`, `policy`,
20
+ * `query`, …). Always lowercase.
21
+ * - id: `[A-Za-z0-9_.+/=-]+` — the mention-token charset (UUIDs with hyphens,
22
+ * fleet numeric ids, etc.). Matches the composer's `MENTION_TOKEN`.
23
+ * - A LEFT boundary (start-of-text or whitespace) keeps `@marker:` from
24
+ * matching inside emails / mid-word (`user@host:1234` never starts a token
25
+ * because the `@` isn't at a boundary).
26
+ */
27
+
28
+ import type { Plugin } from 'unified'
29
+ import type { Root, Text, Link } from 'mdast'
30
+ import { visit, SKIP } from 'unist-util-visit'
31
+
32
+ const MENTION_REGEX = /(^|\s)@([a-z]+):([A-Za-z0-9_.+/=-]+)/g
33
+
34
+ export const remarkMentionChips: Plugin<[], Root> = () => {
35
+ return (tree: Root) => {
36
+ visit(tree, 'text', (node: Text, index, parent) => {
37
+ if (!parent || typeof index !== 'number') return
38
+ const text = node.value
39
+ if (!text || !text.includes('@')) return
40
+
41
+ const parts: Array<Text | Link> = []
42
+ let lastIndex = 0
43
+ MENTION_REGEX.lastIndex = 0
44
+ let match: RegExpExecArray | null
45
+ while ((match = MENTION_REGEX.exec(text)) !== null) {
46
+ const lead = match[1] // '' or the whitespace char before '@'
47
+ const marker = match[2]
48
+ const id = match[3]
49
+ const tokenStart = match.index + lead.length // position of '@'
50
+ if (tokenStart > lastIndex) {
51
+ parts.push({ type: 'text', value: text.slice(lastIndex, tokenStart) })
52
+ }
53
+ parts.push({
54
+ type: 'link',
55
+ url: `mention://${marker}:${id}`,
56
+ // Keep the raw token as the visible children so an unresolved mention
57
+ // (no matching context item) falls back to the literal text.
58
+ children: [{ type: 'text', value: `@${marker}:${id}` }],
59
+ })
60
+ lastIndex = match.index + match[0].length
61
+ }
62
+ if (lastIndex === 0) return // no matches
63
+
64
+ if (lastIndex < text.length) {
65
+ parts.push({ type: 'text', value: text.slice(lastIndex) })
66
+ }
67
+
68
+ parent.children.splice(index, 1, ...parts)
69
+ return [SKIP, index + parts.length]
70
+ })
71
+ }
72
+ }
@@ -167,7 +167,7 @@ export interface RealtimeChunkCallbacks {
167
167
  /** Called when a user message request is received (echo). `streamSeq` (when
168
168
  * the transport carries one) lets hosts stamp the synthetic so the history
169
169
  * merge can dedup it against its persisted twin by sequence. */
170
- onUserMessage?: (text: string, metadata?: { ownerType?: string; displayName?: string; userId?: string; streamSeq?: number }) => void
170
+ onUserMessage?: (text: string, metadata?: { ownerType?: string; displayName?: string; userId?: string; streamSeq?: number, contextItems?: Array<{ type: string; id: string }> }) => void
171
171
  /** Called when TOKEN_USAGE chunk is received with token stats */
172
172
  onTokenUsage?: (data: TokenUsageData) => void
173
173
  /** Called when a direct message is received (immediately displayed). Carries
@@ -132,6 +132,20 @@ export interface ChatMessageEnhancedProps extends Omit<HTMLAttributes<HTMLDivEle
132
132
  * glyph). Optional — chips render label-only when omitted.
133
133
  */
134
134
  resolveContextIcon?: (item: ChatContextItem) => ReactNode
135
+ /**
136
+ * Host renderer for inline AI mentions `@marker:id` — the ASSISTANT echoing
137
+ * `@device:<machineId>` (etc.) in its reply. DIRECT MIRROR of
138
+ * `renderEntityCard` for the `[card://]` grammar: the lib detects the token
139
+ * (via `remark-mention-chips`), parses `{marker, id}`, and renders whatever
140
+ * node the host returns — typically a SELF-FETCHING chip (each entity type
141
+ * has its own fetcher) that resolves its own display name by id. The lib
142
+ * stays data-agnostic: it knows nothing about entity types or how to fetch
143
+ * them. Return null/undefined for a marker the host can't render → the lib
144
+ * falls back to the bare token text. SEPARATE from `contextItems` (the user's
145
+ * own attachments). Keep the function identity stable (e.g. a module const or
146
+ * `useCallback`) so the message memo holds across streaming chunks.
147
+ */
148
+ renderMention?: (reference: { marker: string; id: string }) => React.ReactNode
135
149
  /**
136
150
  * Host-provided renderer for inline entity cards (v6.1 §B.2.7 — DRY
137
151
  * duplications #2). The OSS-lib delegates all entity-specific rendering
@@ -201,6 +215,10 @@ export interface ChatMessageListProps extends HTMLAttributes<HTMLDivElement> {
201
215
  /** Lead-icon resolver for per-message context chips (maps a context item to
202
216
  * its entity-type glyph). Forwarded to every message's ChatMessageEnhanced. */
203
217
  resolveContextIcon?: (item: ChatContextItem) => ReactNode
218
+ /** Host renderer for inline AI mentions `@marker:id` (mirror of
219
+ * `renderEntityCard`). Forwarded verbatim to every message's
220
+ * ChatMessageEnhanced; returns a self-fetching chip per entity type. */
221
+ renderMention?: (reference: { marker: string; id: string }) => React.ReactNode
204
222
  /** Host-provided renderer for inline entity cards. Forwarded verbatim
205
223
  * to every message's ChatMessageEnhanced. v6.1 §B.2.7. */
206
224
  renderEntityCard?: (reference: ChatRef) => React.ReactNode
@@ -33,6 +33,11 @@ export interface ChatContextEntityType {
33
33
  label: string
34
34
  /** Optional icon element rendered at the row lead (host-supplied). */
35
35
  icon?: React.ReactNode
36
+ /** Optional backend mention marker — the SHORT token used for the committed
37
+ * inline `@marker:id` reference (e.g. `'device'`, `'kb'`). When omitted the
38
+ * composer falls back to a lowercased `type`. Lets the host map each kind to
39
+ * the backend's exact mention vocabulary without the lib knowing it. */
40
+ marker?: string
36
41
  }
37
42
 
38
43
  /**
@@ -20,7 +20,14 @@ export type ParsedChunkAction =
20
20
  | { action: 'approval_request'; requestId: string; command: string; explanation?: string; approvalType: string }
21
21
  | { action: 'approval_batch'; requestId: string; approvalType: string; toolCalls: PendingToolCallData[] }
22
22
  | { action: 'approval_result'; requestId: string; approved: boolean; approvalType: string; resolvedByName?: string | null }
23
- | { action: 'message_request'; text: string; ownerType?: string; displayName?: string; userId?: string }
23
+ | {
24
+ action: 'message_request'
25
+ text: string
26
+ ownerType?: string
27
+ displayName?: string
28
+ userId?: string
29
+ contextItems?: Array<{ type: string; id: string }>
30
+ }
24
31
  | { action: 'token_usage'; data: TokenUsageData }
25
32
  | { action: 'direct_message'; text: string; ownerType?: string; displayName?: string; userId?: string }
26
33
  | { action: 'system'; text: string }
@@ -155,6 +155,17 @@ export function parseChunkToAction(chunk: unknown): ParsedChunkAction | null {
155
155
  ownerType: typeof data.ownerType === 'string' ? data.ownerType : undefined,
156
156
  displayName: typeof data.displayName === 'string' ? data.displayName : undefined,
157
157
  userId: typeof data.userId === 'string' ? data.userId : undefined,
158
+ // Entity-context refs the user attached to this message (backend
159
+ // `MESSAGE_REQUEST` chunk → `contextItems: [{ type, id }]`). The wire
160
+ // shape carries no label; the host resolves display text + icon. `label`
161
+ // is REQUIRED on `ChatContextItem`, so fall back to the id (same as the
162
+ // historical path in `process-historical-messages.ts`) — keeps both
163
+ // message sources producing an identical shape.
164
+ contextItems: Array.isArray(data.contextItems)
165
+ ? (data.contextItems as Array<{ type?: unknown; id?: unknown }>)
166
+ .filter((it) => typeof it?.type === 'string' && typeof it?.id === 'string')
167
+ .map((it) => ({ type: it.type as string, id: it.id as string, label: it.id as string }))
168
+ : undefined,
158
169
  }
159
170
 
160
171
  case MESSAGE_TYPE.TOKEN_USAGE:
@@ -15,9 +15,14 @@
15
15
  * `decideNewTab` fallback chain anywhere else.
16
16
  *
17
17
  * Rules:
18
- * - `embed` mode → ALWAYS new-tab. The chat lives on the embedder
19
- * origin; content lives on the hub. Same-tab nav would leave the
20
- * embedder entirely.
18
+ * - `embed` mode → new-tab, EXCEPT for same-origin absolute hrefs. The chat
19
+ * lives on the embedder origin and most content lives on the hub (→ new
20
+ * tab), but an embedder may host a few content types in-app (e.g. OpenFrame
21
+ * serves releases / roadmap / guides under `/help-center`). Those are
22
+ * emitted as ABSOLUTE same-origin URLs by the embedder's `composeContentUrl`;
23
+ * keep them same-tab so they soft-nav in-app instead of opening a redundant
24
+ * new tab on the same origin. Relative hrefs stay new-tab — in embed mode
25
+ * they're hub-relative (absolutized against `defaultContentOrigin`).
21
26
  * - `host` mode → defer to the runtime's `decideNewTab` callback
22
27
  * (cross-platform → new-tab) or the lib's default.
23
28
  */
@@ -25,13 +30,26 @@
25
30
  import type { ChatRuntime } from '../../../contexts/chat-runtime-context'
26
31
  import { decideNewTab as libDecideNewTab } from './decide-new-tab'
27
32
 
33
+ /** An ABSOLUTE `http(s)` URL on the current page's origin. Relative hrefs return
34
+ * false (in embed mode they're hub-relative, not in-app), as do cross-origin
35
+ * and non-http URLs. SSR-safe: no `window` → false. */
36
+ function isSameOriginAbsoluteHref(href: string): boolean {
37
+ if (typeof window === 'undefined') return false
38
+ if (!/^https?:\/\//i.test(href)) return false
39
+ try {
40
+ return new URL(href).origin === window.location.origin
41
+ } catch {
42
+ return false
43
+ }
44
+ }
45
+
28
46
  export function computeIsNewTab(
29
47
  runtime: ChatRuntime,
30
48
  href: string | null | undefined,
31
49
  targetPlatform: string | null,
32
50
  ): boolean {
33
51
  if (!href) return false
34
- if (runtime.navigation.mode === 'embed') return true
52
+ if (runtime.navigation.mode === 'embed') return !isSameOriginAbsoluteHref(href)
35
53
  return (
36
54
  runtime.navigation.decideNewTab?.({ href, targetPlatform }) ??
37
55
  libDecideNewTab({
@@ -144,6 +144,16 @@ export function processHistoricalMessages(
144
144
  const userAuthorType: AuthorType = msg.owner?.type === OWNER_TYPE.ADMIN ? 'admin' : 'user'
145
145
  messageDataArray.forEach((data) => {
146
146
  if (data.type === MESSAGE_TYPE.TEXT && 'text' in data && data.text) {
147
+ // `TextData.contextItems` (server: `[{ type, id }]`) — the entity
148
+ // context the user attached to this message. Surface it so the bubble
149
+ // renders its chip strip from history (no label on the wire → fall
150
+ // back to the id, matching the realtime path).
151
+ const rawContext = (data as { contextItems?: Array<{ type?: unknown; id?: unknown }> }).contextItems
152
+ const contextItems = Array.isArray(rawContext)
153
+ ? rawContext
154
+ .filter((c) => typeof c?.type === 'string' && typeof c?.id === 'string')
155
+ .map((c) => ({ type: c.type as string, id: c.id as string, label: c.id as string }))
156
+ : undefined
147
157
  processedMessages.push({
148
158
  id: msg.id,
149
159
  role: 'user',
@@ -152,6 +162,7 @@ export function processHistoricalMessages(
152
162
  avatar: getOwnerAvatar(msg.owner),
153
163
  authorType: userAuthorType,
154
164
  timestamp: new Date(msg.createdAt),
165
+ ...(contextItems && contextItems.length > 0 ? { contextItems } : {}),
155
166
  })
156
167
  }
157
168
  })
@@ -522,6 +533,16 @@ export function processHistoricalMessagesWithErrors(
522
533
  const userAuthorType: AuthorType = msg.owner?.type === OWNER_TYPE.ADMIN ? 'admin' : 'user'
523
534
  messageDataArray.forEach((data) => {
524
535
  if (data.type === MESSAGE_TYPE.TEXT && 'text' in data && data.text) {
536
+ // `TextData.contextItems` (server: `[{ type, id }]`) — the entity
537
+ // context the user attached to this message. Surface it so the bubble
538
+ // renders its chip strip from history (no label on the wire → fall
539
+ // back to the id, matching the realtime path).
540
+ const rawContext = (data as { contextItems?: Array<{ type?: unknown; id?: unknown }> }).contextItems
541
+ const contextItems = Array.isArray(rawContext)
542
+ ? rawContext
543
+ .filter((c) => typeof c?.type === 'string' && typeof c?.id === 'string')
544
+ .map((c) => ({ type: c.type as string, id: c.id as string, label: c.id as string }))
545
+ : undefined
525
546
  processedMessages.push({
526
547
  id: msg.id,
527
548
  role: 'user',
@@ -530,6 +551,7 @@ export function processHistoricalMessagesWithErrors(
530
551
  avatar: getOwnerAvatar(msg.owner),
531
552
  authorType: userAuthorType,
532
553
  timestamp: new Date(msg.createdAt),
554
+ ...(contextItems && contextItems.length > 0 ? { contextItems } : {}),
533
555
  })
534
556
  }
535
557
  })
@@ -44,7 +44,7 @@ import { PathsDisplay, getOpenFramePaths } from '@flamingo/ui-kit/components/fea
44
44
  | `title` | `string` | — | Optional heading above the list |
45
45
  | `description` | `string` | — | Optional subtext below the title |
46
46
  | `showCopyButtons` | `boolean` | `true` | Toggle copy button visibility |
47
- | `copyIconSize` | `string` | `'w-5 h-5'` | Tailwind size classes for the copy icon |
47
+ | `copyIconSize` | `string` | `'w-6 h-6'` | Tailwind size classes for the copy icon |
48
48
  | `className` | `string` | — | Additional CSS classes for the container |
49
49
 
50
50
  > **Note:** The component renders `null` when `paths` is empty or undefined. Copy buttons only render when both `showCopyButtons` is `true` **and** `onCopyPath` is provided.
@@ -98,7 +98,7 @@ export function CommandBox({
98
98
  {title}
99
99
  </div>
100
100
  )}
101
- <div className="bg-ods-card border border-ods-border rounded-[6px] p-4">
101
+ <div className="bg-ods-bg border border-ods-border rounded-[6px] p-4">
102
102
  <div
103
103
  className={cn(
104
104
  'text-ods-text-primary font-mono text-[14px] md:text-[16px] leading-relaxed break-all',