@flamingo-stack/openframe-frontend-core 0.0.319 → 0.0.321

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 (115) hide show
  1. package/dist/{chunk-XAGD3XZV.js → chunk-563D7EB2.js} +4 -4
  2. package/dist/{chunk-B34YGPIB.cjs → chunk-5HQIFRRE.cjs} +7 -7
  3. package/dist/{chunk-B34YGPIB.cjs.map → chunk-5HQIFRRE.cjs.map} +1 -1
  4. package/dist/{chunk-ARQ3R7WZ.js → chunk-6DH2TCRH.js} +2 -2
  5. package/dist/{chunk-ZD5ZDEL5.cjs → chunk-7C5G45KU.cjs} +1502 -1298
  6. package/dist/chunk-7C5G45KU.cjs.map +1 -0
  7. package/dist/{chunk-CGG7EWYG.cjs → chunk-7M3QIYFW.cjs} +56 -56
  8. package/dist/{chunk-CGG7EWYG.cjs.map → chunk-7M3QIYFW.cjs.map} +1 -1
  9. package/dist/{chunk-4YFOBTFE.js → chunk-BRERE4E5.js} +4 -4
  10. package/dist/{chunk-7ZHCRY2V.cjs → chunk-CD56IAXU.cjs} +5 -5
  11. package/dist/{chunk-7ZHCRY2V.cjs.map → chunk-CD56IAXU.cjs.map} +1 -1
  12. package/dist/{chunk-2RQV7JLA.js → chunk-DEEBJBFQ.js} +2 -2
  13. package/dist/{chunk-NG7P653A.js → chunk-FNK67HTC.js} +2 -2
  14. package/dist/{chunk-4XHPUYL7.cjs → chunk-GAQAPTBU.cjs} +37 -37
  15. package/dist/{chunk-4XHPUYL7.cjs.map → chunk-GAQAPTBU.cjs.map} +1 -1
  16. package/dist/{chunk-LF5LDJOP.js → chunk-HALW7FPF.js} +2 -2
  17. package/dist/{chunk-G7CRSNFS.js → chunk-J65CQLX6.js} +2 -2
  18. package/dist/{chunk-5B37J3B5.cjs → chunk-MN2WQYXS.cjs} +15 -15
  19. package/dist/{chunk-5B37J3B5.cjs.map → chunk-MN2WQYXS.cjs.map} +1 -1
  20. package/dist/{chunk-APEJR6DV.cjs → chunk-NEY347ME.cjs} +26 -26
  21. package/dist/{chunk-APEJR6DV.cjs.map → chunk-NEY347ME.cjs.map} +1 -1
  22. package/dist/{chunk-V2OAUUF6.cjs → chunk-PDOTUMJC.cjs} +31 -31
  23. package/dist/{chunk-V2OAUUF6.cjs.map → chunk-PDOTUMJC.cjs.map} +1 -1
  24. package/dist/{chunk-JIU2I6LU.cjs → chunk-TFCGMQPK.cjs} +25 -25
  25. package/dist/{chunk-JIU2I6LU.cjs.map → chunk-TFCGMQPK.cjs.map} +1 -1
  26. package/dist/{chunk-SVPVDSHA.js → chunk-TPJJJ6KC.js} +4152 -3948
  27. package/dist/chunk-TPJJJ6KC.js.map +1 -0
  28. package/dist/{chunk-7FIWPFLE.js → chunk-TV6VD6LP.js} +4 -4
  29. package/dist/{chunk-2D3RINOJ.js → chunk-V4U7ASRM.js} +2 -2
  30. package/dist/{chunk-QJOSQVIR.js → chunk-VNWWOPIY.js} +2 -2
  31. package/dist/{chunk-J4FCWWO6.cjs → chunk-WCCZGPPP.cjs} +62 -62
  32. package/dist/{chunk-J4FCWWO6.cjs.map → chunk-WCCZGPPP.cjs.map} +1 -1
  33. package/dist/{chunk-ODMCD663.js → chunk-WCRJWZUA.js} +4 -4
  34. package/dist/{chunk-7WMHKVV7.cjs → chunk-Y2TJ64S5.cjs} +12 -12
  35. package/dist/{chunk-7WMHKVV7.cjs.map → chunk-Y2TJ64S5.cjs.map} +1 -1
  36. package/dist/{chunk-RXCP4OV4.cjs → chunk-ZFER75HU.cjs} +9 -9
  37. package/dist/{chunk-RXCP4OV4.cjs.map → chunk-ZFER75HU.cjs.map} +1 -1
  38. package/dist/components/case-studies/index.cjs +8 -8
  39. package/dist/components/case-studies/index.js +2 -2
  40. package/dist/components/chat/chat-context-picker.d.ts +11 -1
  41. package/dist/components/chat/chat-context-picker.d.ts.map +1 -1
  42. package/dist/components/chat/chat-input.d.ts.map +1 -1
  43. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  44. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  45. package/dist/components/chat/context-items-list.d.ts.map +1 -1
  46. package/dist/components/chat/embeddable-chat.d.ts +11 -1
  47. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  48. package/dist/components/chat/index.cjs +2 -2
  49. package/dist/components/chat/index.js +1 -1
  50. package/dist/components/chat/remark-mention-chips.d.ts +11 -5
  51. package/dist/components/chat/remark-mention-chips.d.ts.map +1 -1
  52. package/dist/components/chat/types/component.types.d.ts +30 -7
  53. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  54. package/dist/components/contact/index.cjs +3 -3
  55. package/dist/components/contact/index.js +2 -2
  56. package/dist/components/docs/index.cjs +5 -5
  57. package/dist/components/docs/index.js +4 -4
  58. package/dist/components/embeds/index.cjs +3 -3
  59. package/dist/components/embeds/index.js +2 -2
  60. package/dist/components/faq/index.cjs +3 -3
  61. package/dist/components/faq/index.js +2 -2
  62. package/dist/components/features/index.cjs +8 -2
  63. package/dist/components/features/index.cjs.map +1 -1
  64. package/dist/components/features/index.d.ts +1 -0
  65. package/dist/components/features/index.d.ts.map +1 -1
  66. package/dist/components/features/index.js +7 -1
  67. package/dist/components/features/paths-display.d.ts +16 -1
  68. package/dist/components/features/paths-display.d.ts.map +1 -1
  69. package/dist/components/features/warning-block.d.ts +32 -0
  70. package/dist/components/features/warning-block.d.ts.map +1 -0
  71. package/dist/components/help-center-pages/index.cjs +20 -20
  72. package/dist/components/help-center-pages/index.js +11 -11
  73. package/dist/components/index.cjs +126 -120
  74. package/dist/components/index.cjs.map +1 -1
  75. package/dist/components/index.js +15 -9
  76. package/dist/components/index.js.map +1 -1
  77. package/dist/components/navigation/index.cjs +2 -2
  78. package/dist/components/navigation/index.js +1 -1
  79. package/dist/components/onboarding-guides/index.cjs +5 -5
  80. package/dist/components/onboarding-guides/index.js +4 -4
  81. package/dist/components/related-content/index.cjs +3 -3
  82. package/dist/components/related-content/index.js +2 -2
  83. package/dist/components/tickets/index.cjs +5 -5
  84. package/dist/components/tickets/index.js +4 -4
  85. package/dist/components/ui/index.cjs +2 -2
  86. package/dist/components/ui/index.js +1 -1
  87. package/dist/index.cjs +8 -2
  88. package/dist/index.cjs.map +1 -1
  89. package/dist/index.js +7 -1
  90. package/package.json +1 -1
  91. package/src/components/chat/chat-context-picker.tsx +77 -9
  92. package/src/components/chat/chat-input.tsx +524 -489
  93. package/src/components/chat/chat-message-enhanced.tsx +4 -2
  94. package/src/components/chat/chat-message-list.tsx +2 -0
  95. package/src/components/chat/context-items-list.tsx +9 -1
  96. package/src/components/chat/embeddable-chat.tsx +55 -56
  97. package/src/components/chat/remark-mention-chips.ts +13 -7
  98. package/src/components/chat/types/component.types.ts +31 -7
  99. package/src/components/features/index.ts +1 -0
  100. package/src/components/features/paths-display.tsx +32 -1
  101. package/src/components/features/warning-block.tsx +54 -0
  102. package/src/stories/WarningBlock.stories.tsx +167 -0
  103. package/dist/chunk-SVPVDSHA.js.map +0 -1
  104. package/dist/chunk-ZD5ZDEL5.cjs.map +0 -1
  105. /package/dist/{chunk-XAGD3XZV.js.map → chunk-563D7EB2.js.map} +0 -0
  106. /package/dist/{chunk-ARQ3R7WZ.js.map → chunk-6DH2TCRH.js.map} +0 -0
  107. /package/dist/{chunk-4YFOBTFE.js.map → chunk-BRERE4E5.js.map} +0 -0
  108. /package/dist/{chunk-2RQV7JLA.js.map → chunk-DEEBJBFQ.js.map} +0 -0
  109. /package/dist/{chunk-NG7P653A.js.map → chunk-FNK67HTC.js.map} +0 -0
  110. /package/dist/{chunk-LF5LDJOP.js.map → chunk-HALW7FPF.js.map} +0 -0
  111. /package/dist/{chunk-G7CRSNFS.js.map → chunk-J65CQLX6.js.map} +0 -0
  112. /package/dist/{chunk-7FIWPFLE.js.map → chunk-TV6VD6LP.js.map} +0 -0
  113. /package/dist/{chunk-2D3RINOJ.js.map → chunk-V4U7ASRM.js.map} +0 -0
  114. /package/dist/{chunk-QJOSQVIR.js.map → chunk-VNWWOPIY.js.map} +0 -0
  115. /package/dist/{chunk-ODMCD663.js.map → chunk-WCRJWZUA.js.map} +0 -0
@@ -25,7 +25,7 @@ import type { MessageSegment, MessageContent, ChatMessageEnhancedProps } from ".
25
25
  * item would be stripped from the chip strip yet never rendered inline — lost
26
26
  * from display entirely. The id is capture group 2 (group 1 is the boundary).
27
27
  * Marker lowercase; id is the mention-token charset. */
28
- const MENTION_MARKER_REGEX = /(^|\s)@[a-z]+:([A-Za-z0-9_.+/=-]+)/g
28
+ const MENTION_MARKER_REGEX = /(^|[^\w@])@[a-z]+:([A-Za-z0-9_.+/=-]*[A-Za-z0-9_+/=])/g
29
29
 
30
30
  /**
31
31
  * Same regex shape as `remarkCardLinks` — kept in lockstep so the
@@ -43,7 +43,7 @@ function normalizeContent(content: MessageContent): MessageSegment[] {
43
43
  }
44
44
 
45
45
  const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>(
46
- ({ className, role, content, name, avatar, isTyping = false, timestamp, showAvatar = true, assistantType, authorType: authorTypeProp, assistantIcon, chatRefs, contextItems, resolveContextIcon, renderMention, renderEntityCard, NavLinkAnchor, ...props }, ref) => {
46
+ ({ className, role, content, name, avatar, isTyping = false, timestamp, showAvatar = true, assistantType, authorType: authorTypeProp, assistantIcon, chatRefs, contextItems, resolveContextIcon, renderContextItem, renderMention, renderEntityCard, NavLinkAnchor, ...props }, ref) => {
47
47
  const isUser = role === 'user'
48
48
  const isError = role === 'error'
49
49
  const authorType = authorTypeProp ?? (isUser ? 'user' : assistantType === 'mingo' ? 'mingo' : 'fae')
@@ -595,6 +595,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
595
595
  <ChatContextChipStrip
596
596
  items={stripContextItems}
597
597
  resolveIcon={resolveContextIcon}
598
+ renderItem={renderContextItem}
598
599
  className="mt-2"
599
600
  />
600
601
  )}
@@ -628,6 +629,7 @@ const MemoizedChatMessageEnhanced = memo(ChatMessageEnhanced, (prevProps, nextPr
628
629
  // message (it's set once on the optimistic send and never mutated).
629
630
  prevProps.contextItems === nextProps.contextItems &&
630
631
  prevProps.resolveContextIcon === nextProps.resolveContextIcon &&
632
+ prevProps.renderContextItem === nextProps.renderContextItem &&
631
633
  // Host keeps this stable (module const / useCallback), so reference
632
634
  // equality holds across streaming chunks.
633
635
  prevProps.renderMention === nextProps.renderMention &&
@@ -107,6 +107,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
107
107
  onLoadMore,
108
108
  renderEntityCard,
109
109
  resolveContextIcon,
110
+ renderContextItem,
110
111
  renderMention,
111
112
  NavLinkAnchor,
112
113
  ...props
@@ -539,6 +540,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
539
540
  chatRefs={message.chatRefs}
540
541
  contextItems={message.contextItems}
541
542
  resolveContextIcon={resolveContextIcon}
543
+ renderContextItem={renderContextItem}
542
544
  renderMention={renderMention}
543
545
  renderEntityCard={renderEntityCard}
544
546
  NavLinkAnchor={NavLinkAnchor}
@@ -141,7 +141,15 @@ export function ContextItemsList({
141
141
  }
142
142
 
143
143
  return (
144
- <div role="listbox" aria-label="Items" onScroll={onScroll} className={cn('max-h-[340px] overflow-y-auto', className)}>
144
+ <div
145
+ role="listbox"
146
+ aria-label="Items"
147
+ onScroll={onScroll}
148
+ // Cap to 340px but never exceed ~half the viewport, so on short screens the
149
+ // results list (inside the bottom-anchored picker popover) still fits and
150
+ // scrolls instead of overflowing past the top edge.
151
+ className={cn('max-h-[min(340px,45vh)] overflow-y-auto', className)}
152
+ >
145
153
  {items.map(item => {
146
154
  const selected = selectedKeys.has(itemKey(item))
147
155
  return (
@@ -271,6 +271,16 @@ export interface EmbeddableChatProps {
271
271
  * marker the host can't render → the lib falls back to the bare token.
272
272
  */
273
273
  renderMention?: (reference: { marker: string; id: string }) => React.ReactNode
274
+ /**
275
+ * Host renderer that REPLACES the default label-only context chip on a sent
276
+ * user bubble with a self-fetching entity chip — so a user's manually
277
+ * attached context (`contextItems`) renders IDENTICALLY to an inline
278
+ * `@marker:id` mention (same live name resolution + link). Mirror of
279
+ * `renderMention`, for the attached-chip strip instead of inline tokens.
280
+ * Return null for an item the host can't render → the lib falls back to the
281
+ * label pill. Keep the identity stable (module const / `useCallback`).
282
+ */
283
+ renderContextItem?: (item: ChatContextItem) => React.ReactNode
274
284
  }
275
285
 
276
286
  // =============================================================================
@@ -691,6 +701,7 @@ function EmbeddableChatInner({
691
701
  guideWelcome,
692
702
  contextPicker,
693
703
  renderMention,
704
+ renderContextItem,
694
705
  }: EmbeddableChatProps) {
695
706
  // `shell === 'none'` means the consumer hosts us inside their own panel
696
707
  // (e.g. AppLayoutDrawer in openframe-frontend). Several drawer-shell
@@ -986,9 +997,10 @@ function EmbeddableChatInner({
986
997
  useEffect(() => {
987
998
  mentionActiveRef.current = mentionQuery !== null
988
999
  }, [mentionQuery])
989
- // The one mention reference currently in the draft (`"<type>:<id>"`), or null.
990
- // Source of truth = the input text: deleting the `@type:id` token drops it.
991
- const mentionKeyRef = useRef<string | null>(null)
1000
+ // Keys (`"<type>:<id>"`) of context items that were committed via `@` and so
1001
+ // have a `@type:id` token in the draft text. MULTI-mention: several coexist.
1002
+ // Source of truth = the input text — deleting a token's chip drops its item.
1003
+ const mentionKeysRef = useRef<Set<string>>(new Set())
992
1004
 
993
1005
  // Staged picker selection is per-conversation and Mingo-only. Clear it (and the
994
1006
  // mention bookkeeping) whenever the active dialog changes or the mode toggles,
@@ -998,7 +1010,7 @@ function EmbeddableChatInner({
998
1010
  useEffect(() => {
999
1011
  setContextItems([])
1000
1012
  setMentionQuery(null)
1001
- mentionKeyRef.current = null
1013
+ mentionKeysRef.current.clear()
1002
1014
  }, [activeDialogId, activeMode])
1003
1015
 
1004
1016
  // Map each entity type to its backend mention marker (host-declared on the
@@ -1010,35 +1022,35 @@ function EmbeddableChatInner({
1010
1022
  return m
1011
1023
  }, [contextPicker?.entityTypes])
1012
1024
 
1025
+ // Stable type → icon resolver (entity-type glyph). Used both for the inline
1026
+ // composer chips (`commitMention` meta) and the message-bubble context chips.
1027
+ // Keyed on `entityTypes` only so an inline `contextPicker={{…}}` doesn't
1028
+ // rebuild it every render (its identity flows into the message memo).
1029
+ const contextEntityTypes = contextPicker?.entityTypes
1030
+ const resolveContextIcon = useMemo(() => {
1031
+ const byType = new Map<string, React.ReactNode>()
1032
+ for (const t of contextEntityTypes ?? []) byType.set(t.type, t.icon)
1033
+ return (item: ChatContextItem) => byType.get(item.type)
1034
+ }, [contextEntityTypes])
1035
+
1013
1036
  const toggleContextItem = useCallback(
1014
1037
  (item: ChatContextItem) => {
1015
1038
  const key = `${item.type}:${item.id}`
1016
- // `@`-mention flow → SINGLE-SELECT: commit `@type:id` into the draft
1017
- // (which also strips any prior mention token) so the picked entity stays
1018
- // visible in the input AND rides out in the context. Exactly one mention
1019
- // item is kept. The `+` flow (mention inactive) stays multi-select.
1039
+ // `@`-mention flow → MULTI-select: commit a `@type:id` chip into the draft
1040
+ // (prior mentions left in place) so each picked entity stays visible inline
1041
+ // AND rides out in the context. The `+` flow (mention inactive) is the same
1042
+ // multi-select on the chip strip.
1020
1043
  if (mentionActiveRef.current) {
1021
- // Single-select replaces any prior mention item and dedupes, so it only
1022
- // grows the selection when adding a brand-new item with no prior mention
1023
- // to swap out. Honour `maxItems` in that one growth case (parity with
1024
- // the `+` branch) so the `@` flow can't push past the cap — and don't
1025
- // commit the `@type:id` token into the draft if we're going to ignore it.
1026
- const alreadySelected = contextItems.some(
1027
- (p) => `${p.type}:${p.id}` === key,
1028
- )
1029
- const grows = !alreadySelected && !mentionKeyRef.current
1030
- if (grows && contextItems.length >= contextMaxItems) return
1031
- chatInputRef.current?.commitMention(mentionTokenOf(key, mentionMarkerByType))
1032
- setContextItems((prev) => {
1033
- const withoutPrevMention = mentionKeyRef.current
1034
- ? prev.filter((p) => `${p.type}:${p.id}` !== mentionKeyRef.current)
1035
- : prev
1036
- const deduped = withoutPrevMention.filter(
1037
- (p) => `${p.type}:${p.id}` !== key,
1038
- )
1039
- return [...deduped, item]
1044
+ // Already attached no-op (the picker's `@` flow is add-then-close; the
1045
+ // chip already lives in the draft). Honour `maxItems` on growth.
1046
+ if (contextItems.some((p) => `${p.type}:${p.id}` === key)) return
1047
+ if (contextItems.length >= contextMaxItems) return
1048
+ chatInputRef.current?.commitMention(mentionTokenOf(key, mentionMarkerByType), {
1049
+ label: item.label,
1050
+ icon: resolveContextIcon(item),
1040
1051
  })
1041
- mentionKeyRef.current = key
1052
+ setContextItems((prev) => [...prev, item])
1053
+ mentionKeysRef.current.add(key)
1042
1054
  return
1043
1055
  }
1044
1056
  setContextItems((prev) => {
@@ -1048,32 +1060,32 @@ function EmbeddableChatInner({
1048
1060
  return [...prev, item]
1049
1061
  })
1050
1062
  },
1051
- [contextMaxItems, contextItems, mentionMarkerByType],
1063
+ [contextMaxItems, contextItems, mentionMarkerByType, resolveContextIcon],
1052
1064
  )
1053
1065
 
1054
1066
  const removeContextItem = useCallback((item: ChatContextItem) => {
1055
1067
  const key = `${item.type}:${item.id}`
1056
1068
  setContextItems((prev) => prev.filter((p) => `${p.type}:${p.id}` !== key))
1057
- // Removing the mention chip must also strip its `@type:id` token from the
1058
- // draft, keeping text and context in lockstep (the inverse of deleting the
1059
- // token text, which drops the chip via `handleContextValueChange`).
1060
- if (mentionKeyRef.current === key) {
1061
- mentionKeyRef.current = null
1069
+ // Removing a mention-backed chip must also strip its `@type:id` token from
1070
+ // the draft, keeping text and context in lockstep (the inverse of deleting
1071
+ // the chip in the input, which drops the item via `handleContextValueChange`).
1072
+ if (mentionKeysRef.current.has(key)) {
1073
+ mentionKeysRef.current.delete(key)
1062
1074
  const cur = chatInputRef.current?.getValue() ?? ''
1063
1075
  const next = cur.replace(`@${mentionTokenOf(key, mentionMarkerByType)}`, '').replace(/\s{2,}/g, ' ').trimStart()
1064
1076
  chatInputRef.current?.setValue(next)
1065
1077
  }
1066
1078
  }, [mentionMarkerByType])
1067
1079
 
1068
- // Draft → context reconciliation: when the user deletes the `@type:id` token
1069
- // text, drop the matching mention item. `+`-added items have no token in the
1070
- // text and are never touched here.
1080
+ // Draft → context reconciliation: when the user deletes a `@type:id` chip from
1081
+ // the input, drop the matching mention item. Checks EVERY token-backed key
1082
+ // (multi-mention). `+`-added items have no token in the text and are untouched.
1071
1083
  const handleContextValueChange = useCallback((value: string) => {
1072
- const key = mentionKeyRef.current
1073
- if (!key) return
1074
- if (!value.includes(`@${mentionTokenOf(key, mentionMarkerByType)}`)) {
1075
- setContextItems((prev) => prev.filter((p) => `${p.type}:${p.id}` !== key))
1076
- mentionKeyRef.current = null
1084
+ for (const key of Array.from(mentionKeysRef.current)) {
1085
+ if (!value.includes(`@${mentionTokenOf(key, mentionMarkerByType)}`)) {
1086
+ mentionKeysRef.current.delete(key)
1087
+ setContextItems((prev) => prev.filter((p) => `${p.type}:${p.id}` !== key))
1088
+ }
1077
1089
  }
1078
1090
  }, [mentionMarkerByType])
1079
1091
 
@@ -1102,20 +1114,6 @@ function EmbeddableChatInner({
1102
1114
  setContextPickerOpen(query !== null)
1103
1115
  }, [])
1104
1116
 
1105
- // Stable type → icon resolver for the message-bubble context chips. Built
1106
- // from the host's `contextPicker.entityTypes`; null-safe when the feature
1107
- // is off. Keyed on `entityTypes` (NOT the whole `contextPicker` object) so an
1108
- // inline `contextPicker={{…}}` on the host doesn't rebuild this resolver every
1109
- // render — that identity flows into the `ChatMessageEnhanced` memo, and a new
1110
- // function each render would force a full markdown re-render of every message
1111
- // on each keystroke.
1112
- const contextEntityTypes = contextPicker?.entityTypes
1113
- const resolveContextIcon = useMemo(() => {
1114
- const byType = new Map<string, React.ReactNode>()
1115
- for (const t of contextEntityTypes ?? []) byType.set(t.type, t.icon)
1116
- return (item: ChatContextItem) => byType.get(item.type)
1117
- }, [contextEntityTypes])
1118
-
1119
1117
  // `renderMention` is HOST-provided (see the prop doc): the per-type renderer
1120
1118
  // for inline AI mentions `@marker:id` (mirror of `renderEntityCard`). The lib
1121
1119
  // forwards it verbatim to the message list and calls it from the `mention://`
@@ -1580,6 +1578,7 @@ function EmbeddableChatInner({
1580
1578
  assistantIcon={mingoAssistantIcon}
1581
1579
  renderEntityCard={renderEntityCard}
1582
1580
  resolveContextIcon={resolveContextIcon}
1581
+ renderContextItem={renderContextItem}
1583
1582
  renderMention={renderMention}
1584
1583
  NavLinkAnchor={NavLinkAnchorViaRuntime}
1585
1584
  className="flex-1"
@@ -18,18 +18,24 @@
18
18
  * - marker: `[a-z]+` — the backend `ContextItemType.marker()` short form
19
19
  * (`device`, `script`, `ticket`, `organization`, `user`, `kb`, `policy`,
20
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).
21
+ * - id: charset `[A-Za-z0-9_.+/=-]` (UUIDs with hyphens, fleet numeric ids,
22
+ * base64 global ids, etc.; matches the composer's `MENTION_TOKEN`) — but the
23
+ * id may NOT END in `.` or `-`, so a trailing SENTENCE period/dash isn't
24
+ * swallowed into the id (`@script:38.` → id `38`, with the `.` left as text).
25
+ * Base64 padding `=` and `+`/`/` are still valid final chars.
26
+ * - A LEFT boundary (start-of-text or any non-word, non-`@` character) keeps
27
+ * `@marker:` from matching inside emails / mid-word (`user@host:1234` never
28
+ * starts a token because the `@` is preceded by a word char), while still
29
+ * firing when the agent wraps a mention in punctuation — parentheses,
30
+ * brackets, quotes — e.g. `(@device:64f0a1)`. The boundary character (if
31
+ * any) is captured as `lead` and re-emitted as text, so it is preserved.
26
32
  */
27
33
 
28
34
  import type { Plugin } from 'unified'
29
35
  import type { Root, Text, Link } from 'mdast'
30
36
  import { visit, SKIP } from 'unist-util-visit'
31
37
 
32
- const MENTION_REGEX = /(^|\s)@([a-z]+):([A-Za-z0-9_.+/=-]+)/g
38
+ const MENTION_REGEX = /(^|[^\w@])@([a-z]+):([A-Za-z0-9_.+/=-]*[A-Za-z0-9_+/=])/g
33
39
 
34
40
  export const remarkMentionChips: Plugin<[], Root> = () => {
35
41
  return (tree: Root) => {
@@ -43,7 +49,7 @@ export const remarkMentionChips: Plugin<[], Root> = () => {
43
49
  MENTION_REGEX.lastIndex = 0
44
50
  let match: RegExpExecArray | null
45
51
  while ((match = MENTION_REGEX.exec(text)) !== null) {
46
- const lead = match[1] // '' or the whitespace char before '@'
52
+ const lead = match[1] // '' or the boundary char before '@' (space, '(', etc.)
47
53
  const marker = match[2]
48
54
  const id = match[3]
49
55
  const tokenStart = match.index + lead.length // position of '@'
@@ -132,6 +132,17 @@ 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 that REPLACES the default label-only context chip for an
137
+ * attached item with a self-fetching entity chip — so a user's manually
138
+ * attached context renders IDENTICALLY to an inline `@marker:id` mention
139
+ * (same live name resolution, same link). Mirror of `renderMention`, but for
140
+ * the `contextItems` strip instead of inline tokens. Return `null` for an
141
+ * item the host can't render → the lib falls back to the label pill. Keep the
142
+ * function identity stable (module const / `useCallback`) so the message memo
143
+ * holds across streaming chunks.
144
+ */
145
+ renderContextItem?: (item: ChatContextItem) => ReactNode
135
146
  /**
136
147
  * Host renderer for inline AI mentions `@marker:id` — the ASSISTANT echoing
137
148
  * `@device:<machineId>` (etc.) in its reply. DIRECT MIRROR of
@@ -215,6 +226,10 @@ export interface ChatMessageListProps extends HTMLAttributes<HTMLDivElement> {
215
226
  /** Lead-icon resolver for per-message context chips (maps a context item to
216
227
  * its entity-type glyph). Forwarded to every message's ChatMessageEnhanced. */
217
228
  resolveContextIcon?: (item: ChatContextItem) => ReactNode
229
+ /** Host renderer that REPLACES the default context chip with a self-fetching
230
+ * entity chip (so attached context renders like an inline mention).
231
+ * Forwarded verbatim to every message's ChatMessageEnhanced. */
232
+ renderContextItem?: (item: ChatContextItem) => ReactNode
218
233
  /** Host renderer for inline AI mentions `@marker:id` (mirror of
219
234
  * `renderEntityCard`). Forwarded verbatim to every message's
220
235
  * ChatMessageEnhanced; returns a self-fetching chip per entity type. */
@@ -408,13 +423,22 @@ export interface ChatInputRef {
408
423
  * context item / closes the picker so the `@query` scaffolding doesn't get
409
424
  * sent as literal text. No-op when no mention token is active. */
410
425
  removeMentionTrigger: () => void
411
- /** Commit a SINGLE-SELECT mention: replace the trailing `@query` being typed
412
- * with the literal `@<type>:<id>` token (`token` = `"<type>:<id>"`, plus a
413
- * trailing space) and strip any previously committed token so only one
414
- * mention reference lives in the draft. Used by the composer's `@`-mention
415
- * flow so the picked entity stays visible in the input AND rides out in the
416
- * context deleting the token text removes it from context. */
417
- commitMention: (token: string) => void
426
+ /** Commit a mention: replace the trailing `@query` being typed with the
427
+ * literal `@<type>:<id>` token (`token` = `"<type>:<id>"`, plus a trailing
428
+ * space). MULTIPLE mentions coexist prior committed tokens are left in
429
+ * place. The optional `meta` gives the inline chip its display name + icon
430
+ * (the contenteditable renders the token as a chip); without it the chip
431
+ * falls back to the id. Used by the composer's `@`-mention flow so the picked
432
+ * entity rides out in the context — deleting the chip removes it from context. */
433
+ commitMention: (token: string, meta?: MentionMeta) => void
434
+ }
435
+
436
+ /** Display metadata for an inline mention chip rendered inside the composer. */
437
+ export interface MentionMeta {
438
+ /** Resolved entity name shown in the chip (e.g. `'ELK-PROD-07'`). */
439
+ label: string
440
+ /** Optional lead icon (entity-type glyph). */
441
+ icon?: ReactNode
418
442
  }
419
443
 
420
444
  // ========== Chat Typing Indicator Props ==========
@@ -48,6 +48,7 @@ export * from './social-links-manager'
48
48
  export * from './start-with-openframe-button'
49
49
  export * from './status-filter-component'
50
50
  export * from './tags-selector'
51
+ export * from './warning-block'
51
52
  // Unified video primitives — single source of truth across all platforms.
52
53
  // `<Video>` replaces the deleted `<VideoPlayer>` + `<YouTubeEmbed>` pair;
53
54
  // `<EntityVideoSection>` + `<VideoBitesDisplay>` are the public detail-page
@@ -39,6 +39,13 @@ export interface PathsDisplayProps {
39
39
  * Size of the copy icon (default: 'w-6 h-6')
40
40
  */
41
41
  copyIconSize?: string
42
+
43
+ /**
44
+ * Optional icon rendered at the start of every row, before the path text.
45
+ * Useful for showing a platform logo next to a command (e.g. the Windows
46
+ * logo beside "Run PowerShell as Local Administrator").
47
+ */
48
+ leadingIcon?: React.ReactNode
42
49
  }
43
50
 
44
51
  /**
@@ -77,7 +84,8 @@ export function PathsDisplay({
77
84
  description,
78
85
  className,
79
86
  showCopyButtons = true,
80
- copyIconSize = 'w-6 h-6'
87
+ copyIconSize = 'w-6 h-6',
88
+ leadingIcon
81
89
  }: PathsDisplayProps) {
82
90
  if (!paths || paths.length === 0) {
83
91
  return null
@@ -101,6 +109,9 @@ export function PathsDisplay({
101
109
  key={path}
102
110
  className="flex items-center gap-4 p-4 border-b border-ods-border last:border-b-0"
103
111
  >
112
+ {leadingIcon && (
113
+ <span className="shrink-0 text-ods-text-primary">{leadingIcon}</span>
114
+ )}
104
115
  <span className="flex-1 min-w-0 text-h4 text-ods-text-primary truncate">
105
116
  {path}
106
117
  </span>
@@ -152,3 +163,23 @@ export type OpenFramePathsPlatform = keyof typeof OPENFRAME_PATHS
152
163
  export function getOpenFramePaths(platform: OpenFramePathsPlatform): string[] {
153
164
  return [...OPENFRAME_PATHS[platform]]
154
165
  }
166
+
167
+ /**
168
+ * Doctor command per platform. Run to diagnose installation issues and repair
169
+ * the agent (works even if the agent didn't install correctly).
170
+ */
171
+ export const OPENFRAME_DOCTOR_COMMANDS: Record<OpenFramePathsPlatform, string> = {
172
+ // PowerShell: Invoke-WebRequest / .\openframe-client.exe, mirroring the
173
+ // Windows install command pattern.
174
+ windows:
175
+ "Set-Location ~; Invoke-WebRequest -Uri 'github.com/openframe-client' -OutFile 'openframe-client.exe'; .\\openframe-client.exe doctor",
176
+ darwin: 'cd ~; wget github.com/openframe-client; openframe-client doctor',
177
+ linux: 'cd ~; wget github.com/openframe-client; openframe-client doctor'
178
+ } as const
179
+
180
+ /**
181
+ * Get the OpenFrame doctor command for a specific platform
182
+ */
183
+ export function getOpenFrameDoctorCommand(platform: OpenFramePathsPlatform): string {
184
+ return OPENFRAME_DOCTOR_COMMANDS[platform]
185
+ }
@@ -0,0 +1,54 @@
1
+ 'use client'
2
+
3
+ import { AlertTriangle } from 'lucide-react'
4
+ import React from 'react'
5
+ import { cn } from '../../utils/cn'
6
+
7
+ export interface WarningBlockProps {
8
+ /** Text rendered inside the highlighted warning banner header. */
9
+ title: React.ReactNode
10
+ /**
11
+ * Icon shown at the start of the banner. Defaults to a triangle alert icon.
12
+ * Pass `null` to hide the icon entirely.
13
+ */
14
+ icon?: React.ReactNode
15
+ /**
16
+ * Body content rendered below the banner — description paragraphs, one or more
17
+ * `PathsDisplay` lists, command rows, etc. Fully composable so a single block
18
+ * can mix text and multiple path groups (see the New Device warning blocks).
19
+ */
20
+ children?: React.ReactNode
21
+ /** Extra classes for the outer card container. */
22
+ className?: string
23
+ }
24
+
25
+ const DEFAULT_ICON = <AlertTriangle className="h-6 w-6" />
26
+
27
+ /**
28
+ * WarningBlock - a card with a highlighted "attention / warning" banner header
29
+ * followed by arbitrary body content.
30
+ *
31
+ * The banner reproduces the ODS warning callout (warning color on a muted
32
+ * warning background). Body content is fully composable: pass description
33
+ * paragraphs, one or more `PathsDisplay` lists, command rows, etc. as children.
34
+ *
35
+ * Spacing follows the ODS scale: card padding 16px / gap 12px, banner padding
36
+ * 12px / gap 16px. Typography uses `text-h3`, which is responsive (14px on
37
+ * mobile, 18px from `md` up) and bold by design-token default.
38
+ */
39
+ export function WarningBlock({ title, icon = DEFAULT_ICON, children, className }: WarningBlockProps) {
40
+ return (
41
+ <div
42
+ className={cn(
43
+ 'flex flex-col gap-3 rounded-[6px] border border-ods-border bg-ods-card p-4',
44
+ className
45
+ )}
46
+ >
47
+ <div className="flex items-start gap-4 overflow-hidden rounded-[6px] bg-ods-warning-secondary p-3 text-ods-warning">
48
+ {icon != null && <span className="shrink-0">{icon}</span>}
49
+ <p className="min-w-0 flex-1 text-h3">{title}</p>
50
+ </div>
51
+ {children}
52
+ </div>
53
+ )
54
+ }
@@ -0,0 +1,167 @@
1
+ import type { Meta, StoryObj } from '@storybook/nextjs-vite';
2
+ import { OSTypeIcon } from '../components/features/os-type-badge';
3
+ import { OPENFRAME_DOCTOR_COMMANDS, PathsDisplay } from '../components/features/paths-display';
4
+ import { WarningBlock } from '../components/features/warning-block';
5
+
6
+ const meta = {
7
+ title: 'Features/WarningBlock',
8
+ component: WarningBlock,
9
+ parameters: {
10
+ docs: {
11
+ description: {
12
+ component:
13
+ 'A card with a highlighted "attention / warning" banner header followed by composable body content. Body is passed as children, so a single block can mix description paragraphs with one or more `PathsDisplay` lists (see the New Device warning blocks). Typography is responsive via ODS tokens (14px on mobile, 18px from `md` up).',
14
+ },
15
+ },
16
+ },
17
+ argTypes: {
18
+ title: { control: 'text' },
19
+ },
20
+ decorators: [
21
+ (Story) => (
22
+ <div style={{ maxWidth: 720 }}>
23
+ <Story />
24
+ </div>
25
+ ),
26
+ ],
27
+ } satisfies Meta<typeof WarningBlock>;
28
+
29
+ export default meta;
30
+ type Story = StoryObj<typeof meta>;
31
+
32
+ const copyPath = (path: string) => {
33
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
34
+ void navigator.clipboard.writeText(path);
35
+ }
36
+ };
37
+
38
+ /** Minimal usage — a banner title with a single description paragraph. */
39
+ export const Default: Story = {
40
+ args: {
41
+ title: 'Administrator privileges are required to install the OpenFrame agent.',
42
+ children: (
43
+ <p className="text-h4 text-ods-text-primary">
44
+ Run the command with the appropriate permissions for your platform.
45
+ </p>
46
+ ),
47
+ },
48
+ };
49
+
50
+ /**
51
+ * Antivirus warning from the New Device screen — description, a `PathsDisplay`
52
+ * list of exclusion folders, then a closing paragraph.
53
+ */
54
+ export const AntivirusWarning: Story = {
55
+ args: {
56
+ title: 'Your antivirus may block OpenFrame installation. This is a false positive.',
57
+ children: (
58
+ <>
59
+ <PathsDisplay
60
+ title="If blocked, add these folders to your antivirus exclusions list:"
61
+ paths={['C:\\Program Files\\OpenFrame\\', 'C:\\Users\\[Username]\\AppData\\Local\\OpenFrame\\']}
62
+ onCopyPath={copyPath}
63
+ />
64
+ <p className="text-h4 text-ods-text-primary">
65
+ Or temporarily disable protection during installation. OpenFrame is safe open-source software.
66
+ Blocks happen because new software needs time to build reputation with security vendors.
67
+ </p>
68
+ </>
69
+ ),
70
+ },
71
+ };
72
+
73
+ /**
74
+ * Doctor mode warning from the New Device screen — description plus a single
75
+ * copyable command row.
76
+ */
77
+ export const DoctorMode: Story = {
78
+ args: {
79
+ title: 'Device not appearing or stuck pending?',
80
+ children: (
81
+ <>
82
+ <p className="text-h4 text-ods-text-primary">
83
+ Run the doctor command to diagnose installation issues and repair the agent. It works even if
84
+ the agent didn&apos;t install correctly.
85
+ </p>
86
+ <PathsDisplay
87
+ paths={[OPENFRAME_DOCTOR_COMMANDS.windows]}
88
+ onCopyPath={copyPath}
89
+ />
90
+ </>
91
+ ),
92
+ },
93
+ };
94
+
95
+ /**
96
+ * Administrator privileges warning — a platform-specific instruction row (no
97
+ * copy button) framed by description paragraphs.
98
+ */
99
+ export const AdministratorPrivileges: Story = {
100
+ args: {
101
+ title: 'Administrator privileges are required to install the OpenFrame agent.',
102
+ children: (
103
+ <>
104
+ <p className="text-h4 text-ods-text-primary">
105
+ Run the command with the appropriate permissions for your platform:
106
+ </p>
107
+ <PathsDisplay
108
+ paths={['Run PowerShell as Local Administrator']}
109
+ showCopyButtons={false}
110
+ leadingIcon={<OSTypeIcon osType="windows" size="w-6 h-6" />}
111
+ />
112
+ <p className="text-h4 text-ods-text-primary">
113
+ Without elevated privileges, the installation will fail silently. Right-click PowerShell and
114
+ select &quot;Run as administrator,&quot; or contact your IT admin if you don&apos;t have local
115
+ admin rights.
116
+ </p>
117
+ </>
118
+ ),
119
+ },
120
+ };
121
+
122
+ /** All three New Device warning blocks stacked, matching the Figma layout. */
123
+ export const NewDeviceBlocks: Story = {
124
+ args: { title: '' },
125
+ render: () => (
126
+ <div className="flex flex-col gap-6">
127
+ <WarningBlock title="Administrator privileges are required to install the OpenFrame agent.">
128
+ <p className="text-h4 text-ods-text-primary">
129
+ Run the command with the appropriate permissions for your platform:
130
+ </p>
131
+ <PathsDisplay
132
+ paths={['Run PowerShell as Local Administrator']}
133
+ showCopyButtons={false}
134
+ leadingIcon={<OSTypeIcon osType="windows" size="w-6 h-6" />}
135
+ />
136
+ <p className="text-h4 text-ods-text-primary">
137
+ Without elevated privileges, the installation will fail silently. Right-click PowerShell and
138
+ select &quot;Run as administrator,&quot; or contact your IT admin if you don&apos;t have local
139
+ admin rights.
140
+ </p>
141
+ </WarningBlock>
142
+
143
+ <WarningBlock title="Your antivirus may block OpenFrame installation. This is a false positive.">
144
+ <PathsDisplay
145
+ title="If blocked, add these folders to your antivirus exclusions list:"
146
+ paths={['C:\\Program Files\\OpenFrame\\', 'C:\\Users\\[Username]\\AppData\\Local\\OpenFrame\\']}
147
+ onCopyPath={copyPath}
148
+ />
149
+ <p className="text-h4 text-ods-text-primary">
150
+ Or temporarily disable protection during installation. OpenFrame is safe open-source software.
151
+ Blocks happen because new software needs time to build reputation with security vendors.
152
+ </p>
153
+ </WarningBlock>
154
+
155
+ <WarningBlock title="Device not appearing or stuck pending?">
156
+ <p className="text-h4 text-ods-text-primary">
157
+ Run the doctor command to diagnose installation issues and repair the agent. It works even if
158
+ the agent didn&apos;t install correctly.
159
+ </p>
160
+ <PathsDisplay
161
+ paths={[OPENFRAME_DOCTOR_COMMANDS.windows]}
162
+ onCopyPath={copyPath}
163
+ />
164
+ </WarningBlock>
165
+ </div>
166
+ ),
167
+ };