@flamingo-stack/openframe-frontend-core 0.0.319 → 0.0.321-snapshot.20260625085235

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 (121) hide show
  1. package/dist/{chunk-V2OAUUF6.cjs → chunk-27UIZKPP.cjs} +31 -31
  2. package/dist/{chunk-V2OAUUF6.cjs.map → chunk-27UIZKPP.cjs.map} +1 -1
  3. package/dist/{chunk-JIU2I6LU.cjs → chunk-2JFIPRZM.cjs} +25 -25
  4. package/dist/{chunk-JIU2I6LU.cjs.map → chunk-2JFIPRZM.cjs.map} +1 -1
  5. package/dist/{chunk-2RQV7JLA.js → chunk-2L6XJRWU.js} +2 -2
  6. package/dist/{chunk-ODMCD663.js → chunk-2ZMEIVHB.js} +4 -4
  7. package/dist/{chunk-7FIWPFLE.js → chunk-3CUHOXPN.js} +4 -4
  8. package/dist/{chunk-G7CRSNFS.js → chunk-3NOGHXE5.js} +2 -2
  9. package/dist/{chunk-7ZHCRY2V.cjs → chunk-6GHEXJ6H.cjs} +5 -5
  10. package/dist/{chunk-7ZHCRY2V.cjs.map → chunk-6GHEXJ6H.cjs.map} +1 -1
  11. package/dist/{chunk-J4FCWWO6.cjs → chunk-AOD7KNNL.cjs} +62 -62
  12. package/dist/{chunk-J4FCWWO6.cjs.map → chunk-AOD7KNNL.cjs.map} +1 -1
  13. package/dist/{chunk-5B37J3B5.cjs → chunk-APGXPHP2.cjs} +15 -15
  14. package/dist/{chunk-5B37J3B5.cjs.map → chunk-APGXPHP2.cjs.map} +1 -1
  15. package/dist/{chunk-NG7P653A.js → chunk-HYW3BYDX.js} +2 -2
  16. package/dist/{chunk-ARQ3R7WZ.js → chunk-KFEIMA2T.js} +2 -2
  17. package/dist/{chunk-XAGD3XZV.js → chunk-KJNTCTV4.js} +4 -4
  18. package/dist/{chunk-APEJR6DV.cjs → chunk-M5RW5ASP.cjs} +26 -26
  19. package/dist/{chunk-APEJR6DV.cjs.map → chunk-M5RW5ASP.cjs.map} +1 -1
  20. package/dist/{chunk-7WMHKVV7.cjs → chunk-MQFGEYOG.cjs} +12 -12
  21. package/dist/{chunk-7WMHKVV7.cjs.map → chunk-MQFGEYOG.cjs.map} +1 -1
  22. package/dist/{chunk-SVPVDSHA.js → chunk-MQHD6CTN.js} +4280 -4021
  23. package/dist/chunk-MQHD6CTN.js.map +1 -0
  24. package/dist/{chunk-CGG7EWYG.cjs → chunk-NCFCQWMH.cjs} +56 -56
  25. package/dist/{chunk-CGG7EWYG.cjs.map → chunk-NCFCQWMH.cjs.map} +1 -1
  26. package/dist/{chunk-4XHPUYL7.cjs → chunk-NTEAD4ZM.cjs} +37 -37
  27. package/dist/{chunk-4XHPUYL7.cjs.map → chunk-NTEAD4ZM.cjs.map} +1 -1
  28. package/dist/{chunk-QJOSQVIR.js → chunk-PZYSR23M.js} +2 -2
  29. package/dist/{chunk-LF5LDJOP.js → chunk-S3X2KEFA.js} +2 -2
  30. package/dist/{chunk-RXCP4OV4.cjs → chunk-S6W5YPAB.cjs} +9 -9
  31. package/dist/{chunk-RXCP4OV4.cjs.map → chunk-S6W5YPAB.cjs.map} +1 -1
  32. package/dist/{chunk-ZD5ZDEL5.cjs → chunk-UFUOM6ZT.cjs} +1644 -1385
  33. package/dist/chunk-UFUOM6ZT.cjs.map +1 -0
  34. package/dist/{chunk-2D3RINOJ.js → chunk-WQ4OIM4E.js} +2 -2
  35. package/dist/{chunk-4YFOBTFE.js → chunk-XF3GIE5U.js} +4 -4
  36. package/dist/{chunk-B34YGPIB.cjs → chunk-YKIU2GXU.cjs} +7 -7
  37. package/dist/{chunk-B34YGPIB.cjs.map → chunk-YKIU2GXU.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 +13 -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/mingo-chat-history.d.ts +11 -2
  51. package/dist/components/chat/mingo-chat-history.d.ts.map +1 -1
  52. package/dist/components/chat/mingo-welcome.d.ts +5 -1
  53. package/dist/components/chat/mingo-welcome.d.ts.map +1 -1
  54. package/dist/components/chat/remark-mention-chips.d.ts +11 -5
  55. package/dist/components/chat/remark-mention-chips.d.ts.map +1 -1
  56. package/dist/components/chat/types/component.types.d.ts +30 -7
  57. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  58. package/dist/components/contact/index.cjs +3 -3
  59. package/dist/components/contact/index.js +2 -2
  60. package/dist/components/docs/index.cjs +5 -5
  61. package/dist/components/docs/index.js +4 -4
  62. package/dist/components/embeds/index.cjs +3 -3
  63. package/dist/components/embeds/index.js +2 -2
  64. package/dist/components/faq/index.cjs +3 -3
  65. package/dist/components/faq/index.js +2 -2
  66. package/dist/components/features/index.cjs +8 -2
  67. package/dist/components/features/index.cjs.map +1 -1
  68. package/dist/components/features/index.d.ts +1 -0
  69. package/dist/components/features/index.d.ts.map +1 -1
  70. package/dist/components/features/index.js +7 -1
  71. package/dist/components/features/paths-display.d.ts +16 -1
  72. package/dist/components/features/paths-display.d.ts.map +1 -1
  73. package/dist/components/features/warning-block.d.ts +32 -0
  74. package/dist/components/features/warning-block.d.ts.map +1 -0
  75. package/dist/components/help-center-pages/index.cjs +20 -20
  76. package/dist/components/help-center-pages/index.js +11 -11
  77. package/dist/components/index.cjs +126 -120
  78. package/dist/components/index.cjs.map +1 -1
  79. package/dist/components/index.js +15 -9
  80. package/dist/components/index.js.map +1 -1
  81. package/dist/components/navigation/index.cjs +2 -2
  82. package/dist/components/navigation/index.js +1 -1
  83. package/dist/components/onboarding-guides/index.cjs +5 -5
  84. package/dist/components/onboarding-guides/index.js +4 -4
  85. package/dist/components/related-content/index.cjs +3 -3
  86. package/dist/components/related-content/index.js +2 -2
  87. package/dist/components/tickets/index.cjs +5 -5
  88. package/dist/components/tickets/index.js +4 -4
  89. package/dist/components/ui/index.cjs +2 -2
  90. package/dist/components/ui/index.js +1 -1
  91. package/dist/index.cjs +8 -2
  92. package/dist/index.cjs.map +1 -1
  93. package/dist/index.js +7 -1
  94. package/package.json +1 -1
  95. package/src/components/chat/chat-context-picker.tsx +77 -9
  96. package/src/components/chat/chat-input.tsx +524 -489
  97. package/src/components/chat/chat-message-enhanced.tsx +4 -2
  98. package/src/components/chat/chat-message-list.tsx +2 -0
  99. package/src/components/chat/context-items-list.tsx +9 -1
  100. package/src/components/chat/embeddable-chat.tsx +70 -57
  101. package/src/components/chat/mingo-chat-history.tsx +136 -53
  102. package/src/components/chat/mingo-welcome.tsx +6 -1
  103. package/src/components/chat/remark-mention-chips.ts +13 -7
  104. package/src/components/chat/types/component.types.ts +31 -7
  105. package/src/components/features/index.ts +1 -0
  106. package/src/components/features/paths-display.tsx +32 -1
  107. package/src/components/features/warning-block.tsx +54 -0
  108. package/src/stories/WarningBlock.stories.tsx +167 -0
  109. package/dist/chunk-SVPVDSHA.js.map +0 -1
  110. package/dist/chunk-ZD5ZDEL5.cjs.map +0 -1
  111. /package/dist/{chunk-2RQV7JLA.js.map → chunk-2L6XJRWU.js.map} +0 -0
  112. /package/dist/{chunk-ODMCD663.js.map → chunk-2ZMEIVHB.js.map} +0 -0
  113. /package/dist/{chunk-7FIWPFLE.js.map → chunk-3CUHOXPN.js.map} +0 -0
  114. /package/dist/{chunk-G7CRSNFS.js.map → chunk-3NOGHXE5.js.map} +0 -0
  115. /package/dist/{chunk-NG7P653A.js.map → chunk-HYW3BYDX.js.map} +0 -0
  116. /package/dist/{chunk-ARQ3R7WZ.js.map → chunk-KFEIMA2T.js.map} +0 -0
  117. /package/dist/{chunk-XAGD3XZV.js.map → chunk-KJNTCTV4.js.map} +0 -0
  118. /package/dist/{chunk-QJOSQVIR.js.map → chunk-PZYSR23M.js.map} +0 -0
  119. /package/dist/{chunk-LF5LDJOP.js.map → chunk-S3X2KEFA.js.map} +0 -0
  120. /package/dist/{chunk-2D3RINOJ.js.map → chunk-WQ4OIM4E.js.map} +0 -0
  121. /package/dist/{chunk-4YFOBTFE.js.map → chunk-XF3GIE5U.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 (
@@ -196,6 +196,8 @@ export interface EmbeddableChatProps {
196
196
  params: FetchDialogsParams,
197
197
  ) => Promise<FetchDialogsResult>
198
198
  unarchiveDialog?: (id: string) => Promise<void>
199
+ searchQuery?: string
200
+ onSearchChange?: (query: string) => void
199
201
  }
200
202
 
201
203
  /**
@@ -271,6 +273,16 @@ export interface EmbeddableChatProps {
271
273
  * marker the host can't render → the lib falls back to the bare token.
272
274
  */
273
275
  renderMention?: (reference: { marker: string; id: string }) => React.ReactNode
276
+ /**
277
+ * Host renderer that REPLACES the default label-only context chip on a sent
278
+ * user bubble with a self-fetching entity chip — so a user's manually
279
+ * attached context (`contextItems`) renders IDENTICALLY to an inline
280
+ * `@marker:id` mention (same live name resolution + link). Mirror of
281
+ * `renderMention`, for the attached-chip strip instead of inline tokens.
282
+ * Return null for an item the host can't render → the lib falls back to the
283
+ * label pill. Keep the identity stable (module const / `useCallback`).
284
+ */
285
+ renderContextItem?: (item: ChatContextItem) => React.ReactNode
274
286
  }
275
287
 
276
288
  // =============================================================================
@@ -691,6 +703,7 @@ function EmbeddableChatInner({
691
703
  guideWelcome,
692
704
  contextPicker,
693
705
  renderMention,
706
+ renderContextItem,
694
707
  }: EmbeddableChatProps) {
695
708
  // `shell === 'none'` means the consumer hosts us inside their own panel
696
709
  // (e.g. AppLayoutDrawer in openframe-frontend). Several drawer-shell
@@ -844,6 +857,8 @@ function EmbeddableChatInner({
844
857
  canArchive: mingoDialogCapabilities?.canArchive ?? false,
845
858
  fetchArchivedDialogs: mingoDialogCapabilities?.fetchArchivedDialogs,
846
859
  unarchiveDialog: mingoDialogCapabilities?.unarchiveDialog,
860
+ searchQuery: mingoDialogCapabilities?.searchQuery,
861
+ onSearchChange: mingoDialogCapabilities?.onSearchChange,
847
862
  }
848
863
  }
849
864
  return {
@@ -851,6 +866,8 @@ function EmbeddableChatInner({
851
866
  canArchive: !!effectiveModes.mingo?.archiveDialog,
852
867
  fetchArchivedDialogs: effectiveModes.mingo?.fetchArchivedDialogs,
853
868
  unarchiveDialog: effectiveModes.mingo?.unarchiveDialog,
869
+ searchQuery: undefined as string | undefined,
870
+ onSearchChange: undefined as ((query: string) => void) | undefined,
854
871
  }
855
872
  }, [mingoState, mingoDialogCapabilities, effectiveModes])
856
873
 
@@ -986,9 +1003,10 @@ function EmbeddableChatInner({
986
1003
  useEffect(() => {
987
1004
  mentionActiveRef.current = mentionQuery !== null
988
1005
  }, [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)
1006
+ // Keys (`"<type>:<id>"`) of context items that were committed via `@` and so
1007
+ // have a `@type:id` token in the draft text. MULTI-mention: several coexist.
1008
+ // Source of truth = the input text — deleting a token's chip drops its item.
1009
+ const mentionKeysRef = useRef<Set<string>>(new Set())
992
1010
 
993
1011
  // Staged picker selection is per-conversation and Mingo-only. Clear it (and the
994
1012
  // mention bookkeeping) whenever the active dialog changes or the mode toggles,
@@ -998,7 +1016,7 @@ function EmbeddableChatInner({
998
1016
  useEffect(() => {
999
1017
  setContextItems([])
1000
1018
  setMentionQuery(null)
1001
- mentionKeyRef.current = null
1019
+ mentionKeysRef.current.clear()
1002
1020
  }, [activeDialogId, activeMode])
1003
1021
 
1004
1022
  // Map each entity type to its backend mention marker (host-declared on the
@@ -1010,35 +1028,35 @@ function EmbeddableChatInner({
1010
1028
  return m
1011
1029
  }, [contextPicker?.entityTypes])
1012
1030
 
1031
+ // Stable type → icon resolver (entity-type glyph). Used both for the inline
1032
+ // composer chips (`commitMention` meta) and the message-bubble context chips.
1033
+ // Keyed on `entityTypes` only so an inline `contextPicker={{…}}` doesn't
1034
+ // rebuild it every render (its identity flows into the message memo).
1035
+ const contextEntityTypes = contextPicker?.entityTypes
1036
+ const resolveContextIcon = useMemo(() => {
1037
+ const byType = new Map<string, React.ReactNode>()
1038
+ for (const t of contextEntityTypes ?? []) byType.set(t.type, t.icon)
1039
+ return (item: ChatContextItem) => byType.get(item.type)
1040
+ }, [contextEntityTypes])
1041
+
1013
1042
  const toggleContextItem = useCallback(
1014
1043
  (item: ChatContextItem) => {
1015
1044
  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.
1045
+ // `@`-mention flow → MULTI-select: commit a `@type:id` chip into the draft
1046
+ // (prior mentions left in place) so each picked entity stays visible inline
1047
+ // AND rides out in the context. The `+` flow (mention inactive) is the same
1048
+ // multi-select on the chip strip.
1020
1049
  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]
1050
+ // Already attached → no-op (the picker's `@` flow is add-then-close; the
1051
+ // chip already lives in the draft). Honour `maxItems` on growth.
1052
+ if (contextItems.some((p) => `${p.type}:${p.id}` === key)) return
1053
+ if (contextItems.length >= contextMaxItems) return
1054
+ chatInputRef.current?.commitMention(mentionTokenOf(key, mentionMarkerByType), {
1055
+ label: item.label,
1056
+ icon: resolveContextIcon(item),
1040
1057
  })
1041
- mentionKeyRef.current = key
1058
+ setContextItems((prev) => [...prev, item])
1059
+ mentionKeysRef.current.add(key)
1042
1060
  return
1043
1061
  }
1044
1062
  setContextItems((prev) => {
@@ -1048,32 +1066,32 @@ function EmbeddableChatInner({
1048
1066
  return [...prev, item]
1049
1067
  })
1050
1068
  },
1051
- [contextMaxItems, contextItems, mentionMarkerByType],
1069
+ [contextMaxItems, contextItems, mentionMarkerByType, resolveContextIcon],
1052
1070
  )
1053
1071
 
1054
1072
  const removeContextItem = useCallback((item: ChatContextItem) => {
1055
1073
  const key = `${item.type}:${item.id}`
1056
1074
  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
1075
+ // Removing a mention-backed chip must also strip its `@type:id` token from
1076
+ // the draft, keeping text and context in lockstep (the inverse of deleting
1077
+ // the chip in the input, which drops the item via `handleContextValueChange`).
1078
+ if (mentionKeysRef.current.has(key)) {
1079
+ mentionKeysRef.current.delete(key)
1062
1080
  const cur = chatInputRef.current?.getValue() ?? ''
1063
1081
  const next = cur.replace(`@${mentionTokenOf(key, mentionMarkerByType)}`, '').replace(/\s{2,}/g, ' ').trimStart()
1064
1082
  chatInputRef.current?.setValue(next)
1065
1083
  }
1066
1084
  }, [mentionMarkerByType])
1067
1085
 
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.
1086
+ // Draft → context reconciliation: when the user deletes a `@type:id` chip from
1087
+ // the input, drop the matching mention item. Checks EVERY token-backed key
1088
+ // (multi-mention). `+`-added items have no token in the text and are untouched.
1071
1089
  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
1090
+ for (const key of Array.from(mentionKeysRef.current)) {
1091
+ if (!value.includes(`@${mentionTokenOf(key, mentionMarkerByType)}`)) {
1092
+ mentionKeysRef.current.delete(key)
1093
+ setContextItems((prev) => prev.filter((p) => `${p.type}:${p.id}` !== key))
1094
+ }
1077
1095
  }
1078
1096
  }, [mentionMarkerByType])
1079
1097
 
@@ -1102,20 +1120,6 @@ function EmbeddableChatInner({
1102
1120
  setContextPickerOpen(query !== null)
1103
1121
  }, [])
1104
1122
 
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
1123
  // `renderMention` is HOST-provided (see the prop doc): the per-type renderer
1120
1124
  // for inline AI mentions `@marker:id` (mirror of `renderEntityCard`). The lib
1121
1125
  // forwards it verbatim to the message list and calls it from the `mention://`
@@ -1580,6 +1584,7 @@ function EmbeddableChatInner({
1580
1584
  assistantIcon={mingoAssistantIcon}
1581
1585
  renderEntityCard={renderEntityCard}
1582
1586
  resolveContextIcon={resolveContextIcon}
1587
+ renderContextItem={renderContextItem}
1583
1588
  renderMention={renderMention}
1584
1589
  NavLinkAnchor={NavLinkAnchorViaRuntime}
1585
1590
  className="flex-1"
@@ -1633,8 +1638,14 @@ function EmbeddableChatInner({
1633
1638
  isLoadingHistory={dialogsInitialLoading}
1634
1639
  loadError={dialogsLoadError}
1635
1640
  onRetry={reloadDialogs}
1641
+ historySearchable={!!mingoCaps.onSearchChange}
1636
1642
  dialogHistory={
1637
- dialogs.length > 0 ? (
1643
+ // Keep the history (and its search bar) mounted during an
1644
+ // active search even at 0 results — otherwise a no-match
1645
+ // query would unmount the bar and flash the new-user
1646
+ // greeting. `MingoWelcome` renders `dialogHistory` with top
1647
+ // priority, so this wins over the greeting.
1648
+ dialogs.length > 0 || !!mingoCaps.searchQuery ? (
1638
1649
  <MingoChatHistory
1639
1650
  dialogs={dialogs}
1640
1651
  onSelectDialog={handleSelectDialog}
@@ -1644,6 +1655,8 @@ function EmbeddableChatInner({
1644
1655
  onRequestArchive={
1645
1656
  mingoCaps.canArchive ? setArchiveTarget : undefined
1646
1657
  }
1658
+ searchQuery={mingoCaps.searchQuery}
1659
+ onSearchChange={mingoCaps.onSearchChange}
1647
1660
  hasMore={hasMoreDialogs}
1648
1661
  isLoadingMore={isDialogsLoading && dialogs.length > 0}
1649
1662
  onLoadMore={() => {
@@ -2,9 +2,11 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import { cn } from '../../utils/cn'
5
+ import { useDebounce } from '../../hooks/ui/use-debounce'
5
6
  import { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu'
6
7
  import { Button } from '../ui/button'
7
- import { Ellipsis01Icon } from '../icons-v2-generated'
8
+ import { Input } from '../ui/input'
9
+ import { Ellipsis01Icon, SearchIcon } from '../icons-v2-generated'
8
10
  import type { DialogItem } from './types/component.types'
9
11
 
10
12
  // =============================================================================
@@ -24,6 +26,12 @@ export interface MingoChatHistoryProps {
24
26
  /** Request archive — enables the row "Archive chat" action. The host opens
25
27
  * the Archive confirmation modal. */
26
28
  onRequestArchive?: (dialog: DialogItem) => void
29
+ /** Seed value for the search input. The host owns the search term and refetches
30
+ * the dialog list server-side; the list itself does no filtering. */
31
+ searchQuery?: string
32
+ /** Emit the (debounced) search term — enables the dialog search bar above the
33
+ * list. Omit to hide the search bar entirely. */
34
+ onSearchChange?: (query: string) => void
27
35
  /** Whether more dialogs remain (cursor pagination). */
28
36
  hasMore?: boolean
29
37
  /** True while the next page is loading. */
@@ -211,7 +219,15 @@ function MingoChatHistoryRow({
211
219
  * two layouts are mutually exclusive — swapping between them mid-load reads as
212
220
  * a flicker). Pure ODS tokens, `animate-pulse`.
213
221
  */
214
- export function MingoChatHistorySkeleton({ className }: { className?: string }) {
222
+ export function MingoChatHistorySkeleton({
223
+ className,
224
+ searchable = false,
225
+ }: {
226
+ className?: string
227
+ /** Render a search-bar placeholder above the groups — match the real list,
228
+ * which shows the search bar only when search is wired. */
229
+ searchable?: boolean
230
+ }) {
215
231
  // Mirror the real grouped list: a group label, then a bordered container of
216
232
  // `h-12` rows. `withBadge` adds the leading unread-badge square on the first
217
233
  // N rows; `widths` varies the title-bar length so the placeholder reads like
@@ -234,6 +250,15 @@ export function MingoChatHistorySkeleton({ className }: { className?: string })
234
250
  className,
235
251
  )}
236
252
  >
253
+ {/* Search-bar placeholder — mirrors the real `Input` (border, rounded,
254
+ h-11/12) with a leading icon + text bar so the loading state matches
255
+ the searchable layout. */}
256
+ {searchable && (
257
+ <div className="flex h-11 md:h-12 shrink-0 items-center gap-2 rounded-[6px] border border-ods-border bg-ods-card px-3">
258
+ <div className="size-4 md:size-6 shrink-0 animate-pulse rounded bg-ods-border" />
259
+ <div className="h-4 w-32 animate-pulse rounded bg-ods-border" />
260
+ </div>
261
+ )}
237
262
  {groups.map((group, g) => (
238
263
  <div key={g} className="flex shrink-0 flex-col gap-[var(--spacing-system-xxs)]">
239
264
  {/* Group label (Today / Yesterday). */}
@@ -261,6 +286,46 @@ export function MingoChatHistorySkeleton({ className }: { className?: string })
261
286
  )
262
287
  }
263
288
 
289
+ // =============================================================================
290
+ // Search bar
291
+ // =============================================================================
292
+
293
+ /**
294
+ * Dialog search field. Holds its own input text for snappy typing and emits
295
+ * the DEBOUNCED term via `onSearchChange` — the host owns the search state
296
+ * and refetches the dialog list server-side, so the list never
297
+ * filters locally. `initialValue` only seeds the field on mount.
298
+ */
299
+ function DialogSearchBar({
300
+ initialValue,
301
+ onSearchChange,
302
+ }: {
303
+ initialValue?: string
304
+ onSearchChange: (query: string) => void
305
+ }) {
306
+ const [value, setValue] = React.useState(initialValue ?? '')
307
+ const debounced = useDebounce(value, 300)
308
+ const lastEmitted = React.useRef(initialValue ?? '')
309
+
310
+ React.useEffect(() => {
311
+ if (debounced === lastEmitted.current) return
312
+ lastEmitted.current = debounced
313
+ onSearchChange(debounced)
314
+ }, [debounced, onSearchChange])
315
+
316
+ return (
317
+ <Input
318
+ type="text"
319
+ value={value}
320
+ onChange={(e) => setValue(e.target.value)}
321
+ placeholder="Search for Chats"
322
+ aria-label="Search chats"
323
+ startAdornment={<SearchIcon />}
324
+ className="shrink-0"
325
+ />
326
+ )
327
+ }
328
+
264
329
  // =============================================================================
265
330
  // Component
266
331
  // =============================================================================
@@ -282,12 +347,15 @@ export function MingoChatHistory({
282
347
  onSelectDialog,
283
348
  onRequestRename,
284
349
  onRequestArchive,
350
+ searchQuery,
351
+ onSearchChange,
285
352
  hasMore = false,
286
353
  isLoadingMore = false,
287
354
  onLoadMore,
288
355
  className,
289
356
  }: MingoChatHistoryProps) {
290
357
  const groups = React.useMemo(() => groupDialogs(dialogs), [dialogs])
358
+ const noSearchResults = groups.length === 0 && !!searchQuery?.trim()
291
359
 
292
360
  // Scroll-fade affordances (same pattern as MingoWelcome's greeting region).
293
361
  const scrollRef = React.useRef<HTMLDivElement>(null)
@@ -333,58 +401,73 @@ export function MingoChatHistory({
333
401
  }, [hasMore])
334
402
 
335
403
  return (
336
- <div className={cn('relative flex flex-1 min-h-0 flex-col', className)}>
337
- <div
338
- ref={scrollRef}
339
- onScroll={updateFade}
340
- className="flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-y-auto"
341
- >
342
- {groups.map((group) => (
343
- <div
344
- key={group.key}
345
- className="flex flex-col gap-[var(--spacing-system-xxs)]"
346
- >
347
- <p className="text-h5 text-ods-text-secondary">{group.label}</p>
348
- <div className="overflow-hidden rounded-md border border-ods-border">
349
- {group.items.map((dialog) => (
350
- <MingoChatHistoryRow
351
- key={dialog.id}
352
- dialog={dialog}
353
- isActive={dialog.id === activeDialogId}
354
- onSelect={onSelectDialog}
355
- onRequestRename={onRequestRename}
356
- onRequestArchive={onRequestArchive}
357
- />
358
- ))}
359
- </div>
360
- </div>
361
- ))}
362
- {hasMore ? <div ref={sentinelRef} className="h-px shrink-0" /> : null}
363
- </div>
404
+ <div className={cn('relative flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)]', className)}>
405
+ {/* Pinned search bar — stays put while the grouped list scrolls below. */}
406
+ {onSearchChange ? (
407
+ <DialogSearchBar initialValue={searchQuery} onSearchChange={onSearchChange} />
408
+ ) : null}
364
409
 
365
- {/* Scroll-fade — only while content is hidden in that direction. */}
366
- <div
367
- aria-hidden
368
- className={cn(
369
- 'pointer-events-none absolute inset-x-0 top-0 h-12 transition-opacity duration-150',
370
- fade.top ? 'opacity-100' : 'opacity-0',
371
- )}
372
- style={{
373
- background:
374
- 'linear-gradient(0deg, transparent 0%, var(--color-bg-card) 100%)',
375
- }}
376
- />
377
- <div
378
- aria-hidden
379
- className={cn(
380
- 'pointer-events-none absolute inset-x-0 bottom-0 h-12 transition-opacity duration-150',
381
- fade.bottom ? 'opacity-100' : 'opacity-0',
382
- )}
383
- style={{
384
- background:
385
- 'linear-gradient(180deg, transparent 0%, var(--color-bg-card) 100%)',
386
- }}
387
- />
410
+ {/* Scroll region — `relative` so the fades anchor here (below the search
411
+ bar), not over it. */}
412
+ <div className="relative flex flex-1 min-h-0 flex-col">
413
+ <div
414
+ ref={scrollRef}
415
+ onScroll={updateFade}
416
+ className="flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-y-auto"
417
+ >
418
+ {noSearchResults ? (
419
+ <p className="py-[var(--spacing-system-m)] text-center text-h5 text-ods-text-secondary">
420
+ No chats found
421
+ </p>
422
+ ) : (
423
+ groups.map((group) => (
424
+ <div
425
+ key={group.key}
426
+ className="flex flex-col gap-[var(--spacing-system-xxs)]"
427
+ >
428
+ <p className="text-h5 text-ods-text-secondary">{group.label}</p>
429
+ <div className="overflow-hidden rounded-md border border-ods-border">
430
+ {group.items.map((dialog) => (
431
+ <MingoChatHistoryRow
432
+ key={dialog.id}
433
+ dialog={dialog}
434
+ isActive={dialog.id === activeDialogId}
435
+ onSelect={onSelectDialog}
436
+ onRequestRename={onRequestRename}
437
+ onRequestArchive={onRequestArchive}
438
+ />
439
+ ))}
440
+ </div>
441
+ </div>
442
+ ))
443
+ )}
444
+ {hasMore ? <div ref={sentinelRef} className="h-px shrink-0" /> : null}
445
+ </div>
446
+
447
+ {/* Scroll-fade — only while content is hidden in that direction. */}
448
+ <div
449
+ aria-hidden
450
+ className={cn(
451
+ 'pointer-events-none absolute inset-x-0 top-0 h-12 transition-opacity duration-150',
452
+ fade.top ? 'opacity-100' : 'opacity-0',
453
+ )}
454
+ style={{
455
+ background:
456
+ 'linear-gradient(0deg, transparent 0%, var(--color-bg-card) 100%)',
457
+ }}
458
+ />
459
+ <div
460
+ aria-hidden
461
+ className={cn(
462
+ 'pointer-events-none absolute inset-x-0 bottom-0 h-12 transition-opacity duration-150',
463
+ fade.bottom ? 'opacity-100' : 'opacity-0',
464
+ )}
465
+ style={{
466
+ background:
467
+ 'linear-gradient(180deg, transparent 0%, var(--color-bg-card) 100%)',
468
+ }}
469
+ />
470
+ </div>
388
471
  </div>
389
472
  )
390
473
  }
@@ -92,6 +92,10 @@ export interface MingoWelcomeProps {
92
92
  loadError?: boolean
93
93
  /** Retry handler for the `loadError` state. */
94
94
  onRetry?: () => void
95
+ /** Whether the dialog history has a search bar — when true, the loading
96
+ * skeleton includes a search-bar placeholder so it matches the loaded
97
+ * layout. Ignored once `dialogHistory` is provided. */
98
+ historySearchable?: boolean
95
99
  /** When provided, renders the "Start Guide Chat" chip (the only wired
96
100
  * action — switches the host chat to Guide mode) and enables the default
97
101
  * promo notification. When omitted, both are suppressed. */
@@ -170,6 +174,7 @@ export function MingoWelcome({
170
174
  isLoadingHistory = false,
171
175
  loadError = false,
172
176
  onRetry,
177
+ historySearchable = false,
173
178
  onStartGuideChat,
174
179
  className,
175
180
  }: MingoWelcomeProps) {
@@ -287,7 +292,7 @@ export function MingoWelcome({
287
292
  )}
288
293
  </div>
289
294
  ) : isLoadingHistory ? (
290
- <MingoChatHistorySkeleton />
295
+ <MingoChatHistorySkeleton searchable={historySearchable} />
291
296
  ) : (
292
297
  <>
293
298
  {/* Scrollable region — only the greeting + grid scroll; the notification
@@ -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 '@'