@flamingo-stack/openframe-frontend-core 0.0.685 → 0.0.686-2463.7461.1

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 (174) hide show
  1. package/dist/{chunk-T6KWNJDW.js → chunk-22RSJ57Z.js} +470 -465
  2. package/dist/chunk-22RSJ57Z.js.map +1 -0
  3. package/dist/{chunk-65KTXRBY.js → chunk-2G2CB7ZZ.js} +6 -6
  4. package/dist/{chunk-K2HZG4RM.cjs → chunk-2OD2PPNO.cjs} +32 -32
  5. package/dist/{chunk-K2HZG4RM.cjs.map → chunk-2OD2PPNO.cjs.map} +1 -1
  6. package/dist/{chunk-HMXUOLZR.cjs → chunk-3DO76UAB.cjs} +20 -20
  7. package/dist/{chunk-HMXUOLZR.cjs.map → chunk-3DO76UAB.cjs.map} +1 -1
  8. package/dist/{chunk-XW7MUIW4.js → chunk-3NIZ2VLQ.js} +3 -3
  9. package/dist/{chunk-5LJZPJHX.cjs → chunk-4LBCUBVL.cjs} +59 -59
  10. package/dist/{chunk-5LJZPJHX.cjs.map → chunk-4LBCUBVL.cjs.map} +1 -1
  11. package/dist/{chunk-ZF4Z36G4.js → chunk-4RJQMOQW.js} +7 -7
  12. package/dist/{chunk-TJI5V5BR.cjs → chunk-AF4XOURN.cjs} +7 -7
  13. package/dist/{chunk-TJI5V5BR.cjs.map → chunk-AF4XOURN.cjs.map} +1 -1
  14. package/dist/{chunk-7XF63THA.js → chunk-ASBBGP36.js} +6 -6
  15. package/dist/{chunk-O7LOZW4R.js → chunk-C5TFPMOM.js} +5 -5
  16. package/dist/{chunk-CMQXPD5C.cjs → chunk-GETQTNRV.cjs} +28 -28
  17. package/dist/{chunk-CMQXPD5C.cjs.map → chunk-GETQTNRV.cjs.map} +1 -1
  18. package/dist/{chunk-TUG7IT6H.js → chunk-HLEXYTOE.js} +2 -2
  19. package/dist/{chunk-M4HYIXP6.cjs → chunk-IBUFFKAK.cjs} +399 -383
  20. package/dist/chunk-IBUFFKAK.cjs.map +1 -0
  21. package/dist/{chunk-GXV6UEVY.js → chunk-IELOPDTA.js} +3 -3
  22. package/dist/{chunk-T4DUTP5C.cjs → chunk-II5GVLTN.cjs} +5 -5
  23. package/dist/{chunk-T4DUTP5C.cjs.map → chunk-II5GVLTN.cjs.map} +1 -1
  24. package/dist/{chunk-B32YLOYQ.js → chunk-JBJZL5PI.js} +4 -4
  25. package/dist/{chunk-UT333EOT.js → chunk-LCOMNQLX.js} +4 -4
  26. package/dist/{chunk-VDPCK437.js → chunk-MHTB67ZH.js} +21 -5
  27. package/dist/{chunk-VDPCK437.js.map → chunk-MHTB67ZH.js.map} +1 -1
  28. package/dist/{chunk-EFBMNVSH.cjs → chunk-MZAZUSPQ.cjs} +15 -15
  29. package/dist/{chunk-EFBMNVSH.cjs.map → chunk-MZAZUSPQ.cjs.map} +1 -1
  30. package/dist/{chunk-C3B36OYZ.cjs → chunk-N3I53B5W.cjs} +3 -3
  31. package/dist/{chunk-C3B36OYZ.cjs.map → chunk-N3I53B5W.cjs.map} +1 -1
  32. package/dist/{chunk-WJK2EETW.js → chunk-NINO3PI7.js} +2 -2
  33. package/dist/{chunk-YCTVLN47.cjs → chunk-P4SBHO7U.cjs} +10 -10
  34. package/dist/{chunk-YCTVLN47.cjs.map → chunk-P4SBHO7U.cjs.map} +1 -1
  35. package/dist/{chunk-PSQKD6ZN.js → chunk-PG65AE3I.js} +3 -3
  36. package/dist/{chunk-DQOQ3KTP.cjs → chunk-QNWQQ4RT.cjs} +78 -73
  37. package/dist/chunk-QNWQQ4RT.cjs.map +1 -0
  38. package/dist/{chunk-BZ2XOEHW.js → chunk-QOH45QSE.js} +3 -3
  39. package/dist/{chunk-DWXWEGGO.cjs → chunk-QTXDXHMD.cjs} +22 -22
  40. package/dist/{chunk-DWXWEGGO.cjs.map → chunk-QTXDXHMD.cjs.map} +1 -1
  41. package/dist/{chunk-B25CF3PO.cjs → chunk-RBCMBRI7.cjs} +30 -30
  42. package/dist/{chunk-B25CF3PO.cjs.map → chunk-RBCMBRI7.cjs.map} +1 -1
  43. package/dist/{chunk-QHLJSB2J.cjs → chunk-RKH7DEDF.cjs} +12 -12
  44. package/dist/{chunk-QHLJSB2J.cjs.map → chunk-RKH7DEDF.cjs.map} +1 -1
  45. package/dist/{chunk-Y7ZYOQNB.js → chunk-RSMAVO2Z.js} +3 -3
  46. package/dist/{chunk-A3CSQB4A.cjs → chunk-SI77HF4U.cjs} +1247 -805
  47. package/dist/chunk-SI77HF4U.cjs.map +1 -0
  48. package/dist/{chunk-GHLGXFNU.js → chunk-U6JC3RS2.js} +3 -3
  49. package/dist/{chunk-B5N3AHPG.cjs → chunk-UBXEETWQ.cjs} +81 -81
  50. package/dist/{chunk-B5N3AHPG.cjs.map → chunk-UBXEETWQ.cjs.map} +1 -1
  51. package/dist/{chunk-N4Z6X5BV.js → chunk-WFH52OVQ.js} +5 -5
  52. package/dist/{chunk-U3IXG7TR.cjs → chunk-WJX3ZSOE.cjs} +33 -33
  53. package/dist/{chunk-U3IXG7TR.cjs.map → chunk-WJX3ZSOE.cjs.map} +1 -1
  54. package/dist/{chunk-XGX6JSEU.js → chunk-YPXXXX6R.js} +2086 -1644
  55. package/dist/chunk-YPXXXX6R.js.map +1 -0
  56. package/dist/{chunk-P2ASEPA2.cjs → chunk-ZS3YCJYY.cjs} +6 -6
  57. package/dist/{chunk-P2ASEPA2.cjs.map → chunk-ZS3YCJYY.cjs.map} +1 -1
  58. package/dist/components/case-studies/index.cjs +16 -16
  59. package/dist/components/case-studies/index.js +4 -4
  60. package/dist/components/chat/ai-assistant-info.d.ts.map +1 -1
  61. package/dist/components/chat/approval-request-message.d.ts.map +1 -1
  62. package/dist/components/chat/chat-appearance-context.d.ts +9 -0
  63. package/dist/components/chat/chat-appearance-context.d.ts.map +1 -0
  64. package/dist/components/chat/chat-input.d.ts.map +1 -1
  65. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  66. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  67. package/dist/components/chat/chat-nav-sidebar.d.ts +50 -0
  68. package/dist/components/chat/chat-nav-sidebar.d.ts.map +1 -0
  69. package/dist/components/chat/chat-top-navigation.d.ts +29 -0
  70. package/dist/components/chat/chat-top-navigation.d.ts.map +1 -0
  71. package/dist/components/chat/error-message-display.d.ts.map +1 -1
  72. package/dist/components/chat/index.cjs +21 -3
  73. package/dist/components/chat/index.cjs.map +1 -1
  74. package/dist/components/chat/index.d.ts +6 -0
  75. package/dist/components/chat/index.d.ts.map +1 -1
  76. package/dist/components/chat/index.js +20 -2
  77. package/dist/components/chat/msp-organization-card.d.ts +7 -1
  78. package/dist/components/chat/msp-organization-card.d.ts.map +1 -1
  79. package/dist/components/chat/onboarding-carousel.d.ts +29 -0
  80. package/dist/components/chat/onboarding-carousel.d.ts.map +1 -0
  81. package/dist/components/chat/system-event-message.d.ts +14 -0
  82. package/dist/components/chat/system-event-message.d.ts.map +1 -0
  83. package/dist/components/chat/types/chat.types.d.ts +13 -0
  84. package/dist/components/chat/types/chat.types.d.ts.map +1 -1
  85. package/dist/components/chat/types/component.types.d.ts +20 -1
  86. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  87. package/dist/components/contact/index.cjs +5 -5
  88. package/dist/components/contact/index.js +4 -4
  89. package/dist/components/docs/index.cjs +8 -8
  90. package/dist/components/docs/index.js +7 -7
  91. package/dist/components/embeds/index.cjs +5 -5
  92. package/dist/components/embeds/index.js +4 -4
  93. package/dist/components/faq/index.cjs +5 -5
  94. package/dist/components/faq/index.js +4 -4
  95. package/dist/components/features/index.cjs +4 -4
  96. package/dist/components/features/index.js +3 -3
  97. package/dist/components/help-center-pages/index.cjs +91 -91
  98. package/dist/components/help-center-pages/index.js +15 -15
  99. package/dist/components/index.cjs +73 -55
  100. package/dist/components/index.cjs.map +1 -1
  101. package/dist/components/index.js +33 -15
  102. package/dist/components/index.js.map +1 -1
  103. package/dist/components/meeting-scheduler/index.cjs +41 -41
  104. package/dist/components/meeting-scheduler/index.js +3 -3
  105. package/dist/components/navigation/index.cjs +7 -7
  106. package/dist/components/navigation/index.js +6 -6
  107. package/dist/components/onboarding-guides/index.cjs +7 -7
  108. package/dist/components/onboarding-guides/index.js +6 -6
  109. package/dist/components/related-content/index.cjs +3 -3
  110. package/dist/components/related-content/index.js +2 -2
  111. package/dist/components/tickets/index.cjs +8 -8
  112. package/dist/components/tickets/index.js +7 -7
  113. package/dist/components/ui/avatar-stack.d.ts +5 -1
  114. package/dist/components/ui/avatar-stack.d.ts.map +1 -1
  115. package/dist/components/ui/index.cjs +22 -4
  116. package/dist/components/ui/index.cjs.map +1 -1
  117. package/dist/components/ui/index.js +21 -3
  118. package/dist/components/ui/reopen-ticket-request-modal.d.ts +4 -1
  119. package/dist/components/ui/reopen-ticket-request-modal.d.ts.map +1 -1
  120. package/dist/components/ui/tab-navigation.d.ts +7 -0
  121. package/dist/components/ui/tab-navigation.d.ts.map +1 -1
  122. package/dist/index.cjs +27 -9
  123. package/dist/index.cjs.map +1 -1
  124. package/dist/index.js +25 -7
  125. package/dist/index.js.map +1 -1
  126. package/package.json +1 -1
  127. package/src/components/chat/__tests__/chat-appearance.test.tsx +79 -0
  128. package/src/components/chat/__tests__/onboarding-carousel.test.tsx +56 -0
  129. package/src/components/chat/ai-assistant-info.tsx +7 -1
  130. package/src/components/chat/approval-request-message.tsx +6 -1
  131. package/src/components/chat/chat-appearance-context.tsx +15 -0
  132. package/src/components/chat/chat-input.tsx +26 -1
  133. package/src/components/chat/chat-message-enhanced.tsx +180 -142
  134. package/src/components/chat/chat-message-list.tsx +169 -164
  135. package/src/components/chat/chat-nav-sidebar.tsx +163 -0
  136. package/src/components/chat/chat-top-navigation.tsx +109 -0
  137. package/src/components/chat/error-message-display.tsx +21 -3
  138. package/src/components/chat/index.ts +6 -0
  139. package/src/components/chat/msp-organization-card.tsx +35 -0
  140. package/src/components/chat/onboarding-carousel.tsx +162 -0
  141. package/src/components/chat/system-event-message.tsx +54 -0
  142. package/src/components/chat/types/chat.types.ts +15 -0
  143. package/src/components/chat/types/component.types.ts +20 -0
  144. package/src/components/ui/__tests__/avatar-stack.test.tsx +25 -0
  145. package/src/components/ui/__tests__/tab-navigation.test.tsx +56 -2
  146. package/src/components/ui/avatar-stack.tsx +15 -5
  147. package/src/components/ui/reopen-ticket-request-modal.tsx +6 -2
  148. package/src/components/ui/tab-navigation.tsx +23 -0
  149. package/src/stories/FaeLayoutV2.stories.tsx +150 -0
  150. package/src/stories/FaeOnboardingV2.stories.tsx +68 -0
  151. package/src/stories/FaeThreadV2.stories.tsx +192 -0
  152. package/src/stories/fae-brand-decorator.tsx +28 -0
  153. package/src/styles/ods-colors.css +10 -0
  154. package/src/styles/ods-effects.css +13 -0
  155. package/dist/chunk-A3CSQB4A.cjs.map +0 -1
  156. package/dist/chunk-DQOQ3KTP.cjs.map +0 -1
  157. package/dist/chunk-M4HYIXP6.cjs.map +0 -1
  158. package/dist/chunk-T6KWNJDW.js.map +0 -1
  159. package/dist/chunk-XGX6JSEU.js.map +0 -1
  160. /package/dist/{chunk-65KTXRBY.js.map → chunk-2G2CB7ZZ.js.map} +0 -0
  161. /package/dist/{chunk-XW7MUIW4.js.map → chunk-3NIZ2VLQ.js.map} +0 -0
  162. /package/dist/{chunk-ZF4Z36G4.js.map → chunk-4RJQMOQW.js.map} +0 -0
  163. /package/dist/{chunk-7XF63THA.js.map → chunk-ASBBGP36.js.map} +0 -0
  164. /package/dist/{chunk-O7LOZW4R.js.map → chunk-C5TFPMOM.js.map} +0 -0
  165. /package/dist/{chunk-TUG7IT6H.js.map → chunk-HLEXYTOE.js.map} +0 -0
  166. /package/dist/{chunk-GXV6UEVY.js.map → chunk-IELOPDTA.js.map} +0 -0
  167. /package/dist/{chunk-B32YLOYQ.js.map → chunk-JBJZL5PI.js.map} +0 -0
  168. /package/dist/{chunk-UT333EOT.js.map → chunk-LCOMNQLX.js.map} +0 -0
  169. /package/dist/{chunk-WJK2EETW.js.map → chunk-NINO3PI7.js.map} +0 -0
  170. /package/dist/{chunk-PSQKD6ZN.js.map → chunk-PG65AE3I.js.map} +0 -0
  171. /package/dist/{chunk-BZ2XOEHW.js.map → chunk-QOH45QSE.js.map} +0 -0
  172. /package/dist/{chunk-Y7ZYOQNB.js.map → chunk-RSMAVO2Z.js.map} +0 -0
  173. /package/dist/{chunk-GHLGXFNU.js.map → chunk-U6JC3RS2.js.map} +0 -0
  174. /package/dist/{chunk-N4Z6X5BV.js.map → chunk-WFH52OVQ.js.map} +0 -0
@@ -14,10 +14,12 @@ import { useStickToBottom } from 'use-stick-to-bottom';
14
14
  import { cn } from '../../utils/cn';
15
15
  import { DotsLoaderIcon, Arrow02DownIcon } from '../icons-v2-generated';
16
16
  import { OverlayOpenRegistryProvider } from '../ui/overlay-open-registry';
17
+ import { ChatAppearanceContext } from './chat-appearance-context';
17
18
  import { ChatMessageEnhanced } from './chat-message-enhanced';
18
19
  import { ChatMessageListSkeleton } from './chat-message-skeleton';
19
20
  import { CyclingPhrase } from './cycling-phrase';
20
21
  import type { ChatMessageListProps } from './types';
22
+ import { CHAT_APPEARANCE } from './types/chat.types';
21
23
  import { SCROLL_ANCHOR } from './types/message.types';
22
24
  import type { MessageContent } from './types/message.types';
23
25
 
@@ -128,6 +130,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
128
130
  contentClassName,
129
131
  assistantType,
130
132
  approvalVariant,
133
+ appearance = CHAT_APPEARANCE.CLASSIC,
131
134
  assistantIcon,
132
135
  pendingApprovals,
133
136
  hasNextPage,
@@ -915,118 +918,119 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
915
918
  const showStreamingLoader = isTyping && !isPausedOnApproval && !(pendingApprovals && pendingApprovals.length > 0);
916
919
 
917
920
  return (
918
- <OverlayOpenRegistryProvider onOpenChange={handleOverlayOpenChange}>
919
- <div className="flex min-h-0 flex-1 flex-col">
920
- {/* Positioning box for the jump-to-bottom button: it wraps ONLY the
921
+ <ChatAppearanceContext.Provider value={appearance}>
922
+ <OverlayOpenRegistryProvider onOpenChange={handleOverlayOpenChange}>
923
+ <div className="flex min-h-0 flex-1 flex-col">
924
+ {/* Positioning box for the jump-to-bottom button: it wraps ONLY the
921
925
  scroller, so `absolute bottom-…` really is the scroller's lower
922
926
  edge. The outer flex column also holds the streaming loader and the
923
927
  approvals bar, and anchoring against THAT box put the button over
924
928
  the loader for the whole of every stream — precisely when it is
925
929
  most likely to be visible. */}
926
- <div className="relative flex min-h-0 flex-1 flex-col">
927
- <div
928
- ref={setScrollRef}
929
- className={cn(
930
- 'flex h-full w-full flex-1 flex-col overflow-y-auto overflow-x-hidden',
931
- // `overscroll-contain` (default ON, opt-out via `overscrollContain={false}`):
932
- // reaching the top/bottom of the thread must NOT chain the wheel/
933
- // touch scroll to the page behind the chat — e.g. the company-hub
934
- // deck (native body scroll + sticky slide panels) would otherwise
935
- // advance slides while you scroll the chat (fixed in #1501; kept as
936
- // the prop default here). Passive in-page DEMO chats disable it so
937
- // the surrounding page keeps scrolling normally when the pointer is
938
- // over the (non-interactive) thread.
939
- overscrollContain && 'overscroll-contain',
940
- 'scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30',
941
- className,
942
- )}
943
- {...props}
944
- >
930
+ <div className="relative flex min-h-0 flex-1 flex-col">
945
931
  <div
946
- ref={setContentRef}
932
+ ref={setScrollRef}
947
933
  className={cn(
948
- // `fullWidth=true` drops the centered-narrow column for
949
- // side-panel hosts (e.g. multi-platform-hub Mingo). Same
950
- // semantics as ChatHeader / ChatInput / ChatFooter.
951
- fullWidth
952
- ? 'flex w-full min-w-0 flex-col pb-[var(--spacing-system-xs)]'
953
- : 'mx-auto flex w-full min-w-0 max-w-ods-content-narrow flex-col pb-[var(--spacing-system-xs)]',
954
- contentClassName ?? 'px-[var(--spacing-system-m)]',
934
+ 'flex h-full w-full flex-1 flex-col overflow-y-auto overflow-x-hidden',
935
+ // `overscroll-contain` (default ON, opt-out via `overscrollContain={false}`):
936
+ // reaching the top/bottom of the thread must NOT chain the wheel/
937
+ // touch scroll to the page behind the chat — e.g. the company-hub
938
+ // deck (native body scroll + sticky slide panels) would otherwise
939
+ // advance slides while you scroll the chat (fixed in #1501; kept as
940
+ // the prop default here). Passive in-page DEMO chats disable it so
941
+ // the surrounding page keeps scrolling normally when the pointer is
942
+ // over the (non-interactive) thread.
943
+ overscrollContain && 'overscroll-contain',
944
+ 'scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30',
945
+ className,
955
946
  )}
956
- style={{ minHeight: '100%' }}
947
+ {...props}
957
948
  >
958
- {hasNextPage && <div ref={sentinelRef} className="h-px" />}
959
- <div className="flex-1" />
960
- {messages.map((message, index) => {
961
- // Hidden messages (synthetic continuation prompts the host
962
- // injects after an approval card) are part of the API
963
- // conversation history but never render. Skipping here
964
- // keeps the visible thread coherent — see
965
- // `Message.hidden` doc-comment in message.types.ts.
966
- if (message.hidden) return null;
967
- // ONE OWNER for the pending turn. Both send paths mint an empty
968
- // assistant placeholder (the accumulation target — it MUST stay
969
- // in state) and flip the phase to 'thinking'. Rendering it here
970
- // too would put an author label with no body in the transcript
971
- // WHILE the footer loader below already represents the same
972
- // pending turn — two owners, and an `aria-live` log that
973
- // announces a bare "Mingo". The footer loader wins (it is
974
- // already `role="status" aria-live="polite"`, sits outside the
975
- // scroller so it can't jitter the thread, and cycles the
976
- // progress phrase), so the placeholder is not rendered AT ALL
977
- // until it carries something visible — removed from the live
978
- // region, not merely hidden. Mirrors `endSseTurn`, which prunes
979
- // an empty trailing assistant AFTER a turn; this closes the same
980
- // window BEFORE the first token. Any non-text segment (tool
981
- // execution, approval card, thinking) counts as visible.
982
- const isEmptyPendingTurn =
983
- index === messages.length - 1 &&
984
- isTyping &&
985
- message.role === 'assistant' &&
986
- !hasNonEmptyContent(message.content) &&
987
- (!Array.isArray(message.content) || message.content.every(s => s.type === 'text'));
988
- if (isEmptyPendingTurn) return null;
989
- const ownedContent = renderAfterMessage?.(message, index);
990
- const row = (
991
- <ChatMessageEnhanced
992
- key={message.id}
993
- ref={getRegisterMessageEl(message.id)}
994
- role={message.role}
995
- name={message.name}
996
- content={message.content}
997
- timestamp={message.timestamp}
998
- isTyping={index === messages.length - 1 && isTyping && message.role === 'assistant'}
999
- avatar={showAvatars ? message.avatar : null}
1000
- showAvatar={showAvatars}
1001
- assistantType={message.assistantType || assistantType}
1002
- approvalVariant={approvalVariant}
1003
- authorType={message.authorType}
1004
- assistantIcon={message.role !== 'user' ? assistantIcon : undefined}
1005
- contextItems={message.contextItems}
1006
- resolveContextIcon={resolveContextIcon}
1007
- renderContextItem={renderContextItem}
1008
- renderMention={renderMention}
1009
- renderEntityCard={renderEntityCard}
1010
- refs={message.refs}
1011
- onAskSelect={index > lastUserMessageIndex ? onAskSelect : undefined}
1012
- NavLinkAnchor={NavLinkAnchor}
1013
- />
1014
- );
1015
- // Only wrap when there IS owned content: the bare row keeps
1016
- // its own key and its place as a direct flex child, so a
1017
- // thread without any stays byte-identical to before.
1018
- if (!ownedContent) return row;
1019
- return (
1020
- <div key={message.id} className="grid w-full min-w-0 grid-cols-1">
1021
- {row}
1022
- {ownedContent}
1023
- </div>
1024
- );
1025
- })}
949
+ <div
950
+ ref={setContentRef}
951
+ className={cn(
952
+ // `fullWidth=true` drops the centered-narrow column for
953
+ // side-panel hosts (e.g. multi-platform-hub Mingo). Same
954
+ // semantics as ChatHeader / ChatInput / ChatFooter.
955
+ fullWidth
956
+ ? 'flex w-full min-w-0 flex-col pb-[var(--spacing-system-xs)]'
957
+ : 'mx-auto flex w-full min-w-0 max-w-ods-content-narrow flex-col pb-[var(--spacing-system-xs)]',
958
+ contentClassName ?? 'px-[var(--spacing-system-m)]',
959
+ )}
960
+ style={{ minHeight: '100%' }}
961
+ >
962
+ {hasNextPage && <div ref={sentinelRef} className="h-px" />}
963
+ <div className="flex-1" />
964
+ {messages.map((message, index) => {
965
+ // Hidden messages (synthetic continuation prompts the host
966
+ // injects after an approval card) are part of the API
967
+ // conversation history but never render. Skipping here
968
+ // keeps the visible thread coherent — see
969
+ // `Message.hidden` doc-comment in message.types.ts.
970
+ if (message.hidden) return null;
971
+ // ONE OWNER for the pending turn. Both send paths mint an empty
972
+ // assistant placeholder (the accumulation target — it MUST stay
973
+ // in state) and flip the phase to 'thinking'. Rendering it here
974
+ // too would put an author label with no body in the transcript
975
+ // WHILE the footer loader below already represents the same
976
+ // pending turn — two owners, and an `aria-live` log that
977
+ // announces a bare "Mingo". The footer loader wins (it is
978
+ // already `role="status" aria-live="polite"`, sits outside the
979
+ // scroller so it can't jitter the thread, and cycles the
980
+ // progress phrase), so the placeholder is not rendered AT ALL
981
+ // until it carries something visible — removed from the live
982
+ // region, not merely hidden. Mirrors `endSseTurn`, which prunes
983
+ // an empty trailing assistant AFTER a turn; this closes the same
984
+ // window BEFORE the first token. Any non-text segment (tool
985
+ // execution, approval card, thinking) counts as visible.
986
+ const isEmptyPendingTurn =
987
+ index === messages.length - 1 &&
988
+ isTyping &&
989
+ message.role === 'assistant' &&
990
+ !hasNonEmptyContent(message.content) &&
991
+ (!Array.isArray(message.content) || message.content.every(s => s.type === 'text'));
992
+ if (isEmptyPendingTurn) return null;
993
+ const ownedContent = renderAfterMessage?.(message, index);
994
+ const row = (
995
+ <ChatMessageEnhanced
996
+ key={message.id}
997
+ ref={getRegisterMessageEl(message.id)}
998
+ role={message.role}
999
+ name={message.name}
1000
+ content={message.content}
1001
+ timestamp={message.timestamp}
1002
+ isTyping={index === messages.length - 1 && isTyping && message.role === 'assistant'}
1003
+ avatar={showAvatars ? message.avatar : null}
1004
+ showAvatar={showAvatars}
1005
+ assistantType={message.assistantType || assistantType}
1006
+ approvalVariant={approvalVariant}
1007
+ authorType={message.authorType}
1008
+ assistantIcon={message.role !== 'user' ? assistantIcon : undefined}
1009
+ contextItems={message.contextItems}
1010
+ resolveContextIcon={resolveContextIcon}
1011
+ renderContextItem={renderContextItem}
1012
+ renderMention={renderMention}
1013
+ renderEntityCard={renderEntityCard}
1014
+ refs={message.refs}
1015
+ onAskSelect={index > lastUserMessageIndex ? onAskSelect : undefined}
1016
+ NavLinkAnchor={NavLinkAnchor}
1017
+ />
1018
+ );
1019
+ // Only wrap when there IS owned content: the bare row keeps
1020
+ // its own key and its place as a direct flex child, so a
1021
+ // thread without any stays byte-identical to before.
1022
+ if (!ownedContent) return row;
1023
+ return (
1024
+ <div key={message.id} className="grid w-full min-w-0 grid-cols-1">
1025
+ {row}
1026
+ {ownedContent}
1027
+ </div>
1028
+ );
1029
+ })}
1030
+ </div>
1026
1031
  </div>
1027
- </div>
1028
1032
 
1029
- {/* Jump to bottom. The manual escape hatch from auto-scroll that
1033
+ {/* Jump to bottom. The manual escape hatch from auto-scroll that
1030
1034
  every 2026 chat surface ships (AI Elements calls it
1031
1035
  `ConversationScrollButton`) and the WCAG 2.2.2 counterpart to
1032
1036
  moving content: the reader can always get back to the live
@@ -1037,79 +1041,80 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
1037
1041
  height either way — a sibling would shrink the scroller and
1038
1042
  move the very bottom it points at. Hidden while at the bottom
1039
1043
  and in passive demo hosts. */}
1040
- {autoScroll && !atBottom && messages.length > 0 && (
1041
- <button
1042
- type="button"
1043
- onClick={() => {
1044
- // Clicking IS the intent to return to the live end, so it
1045
- // re-arms the follow lock a scroll-up had released.
1046
- followBottomRef.current = true;
1047
- void scrollToBottom({ animation: 'smooth' });
1048
- }}
1049
- aria-label="Scroll to latest message"
1050
- className={cn(
1051
- 'absolute bottom-[var(--spacing-system-s)] left-1/2 z-10 -translate-x-1/2',
1052
- 'flex size-9 items-center justify-center rounded-full',
1053
- 'border border-ods-border bg-ods-card text-ods-text-primary shadow-lg',
1054
- 'transition-colors hover:bg-ods-bg-hover',
1055
- )}
1056
- >
1057
- <Arrow02DownIcon size={16} />
1058
- </button>
1059
- )}
1060
- </div>
1044
+ {autoScroll && !atBottom && messages.length > 0 && (
1045
+ <button
1046
+ type="button"
1047
+ onClick={() => {
1048
+ // Clicking IS the intent to return to the live end, so it
1049
+ // re-arms the follow lock a scroll-up had released.
1050
+ followBottomRef.current = true;
1051
+ void scrollToBottom({ animation: 'smooth' });
1052
+ }}
1053
+ aria-label="Scroll to latest message"
1054
+ className={cn(
1055
+ 'absolute bottom-[var(--spacing-system-s)] left-1/2 z-10 -translate-x-1/2',
1056
+ 'flex size-9 items-center justify-center rounded-full',
1057
+ 'border border-ods-border bg-ods-card text-ods-text-primary shadow-lg',
1058
+ 'transition-colors hover:bg-ods-bg-hover',
1059
+ )}
1060
+ >
1061
+ <Arrow02DownIcon size={16} />
1062
+ </button>
1063
+ )}
1064
+ </div>
1061
1065
 
1062
- {/* Footer-pinned streaming loader — outside the scroller so it
1066
+ {/* Footer-pinned streaming loader — outside the scroller so it
1063
1067
  doesn't jitter as the streaming message grows. Color is set
1064
1068
  via inline style (CSS var) rather than a Tailwind class so it
1065
1069
  is JIT-independent when this lib is consumed via yalc. */}
1066
- {showStreamingLoader && (
1067
- <div
1068
- className={cn(
1069
- fullWidth
1070
- ? 'flex w-full items-center gap-[var(--spacing-system-xxs)] py-[var(--spacing-system-xs)]'
1071
- : 'mx-auto flex w-full max-w-ods-content-narrow items-center gap-[var(--spacing-system-xxs)] py-[var(--spacing-system-xs)]',
1072
- contentClassName ?? 'px-[var(--spacing-system-m)]',
1073
- )}
1074
- style={{ color: 'var(--color-text-muted)' }}
1075
- role="status"
1076
- aria-live="polite"
1077
- >
1078
- <DotsLoaderIcon className="h-6 w-6" />
1079
- <CyclingPhrase words={STREAMING_WORDS} className="text-h6" />
1080
- </div>
1081
- )}
1070
+ {showStreamingLoader && (
1071
+ <div
1072
+ className={cn(
1073
+ fullWidth
1074
+ ? 'flex w-full items-center gap-[var(--spacing-system-xxs)] py-[var(--spacing-system-xs)]'
1075
+ : 'mx-auto flex w-full max-w-ods-content-narrow items-center gap-[var(--spacing-system-xxs)] py-[var(--spacing-system-xs)]',
1076
+ contentClassName ?? 'px-[var(--spacing-system-m)]',
1077
+ )}
1078
+ style={{ color: 'var(--color-text-muted)' }}
1079
+ role="status"
1080
+ aria-live="polite"
1081
+ >
1082
+ <DotsLoaderIcon className="h-6 w-6" />
1083
+ <CyclingPhrase words={STREAMING_WORDS} className="text-h6" />
1084
+ </div>
1085
+ )}
1082
1086
 
1083
- {/* Sticky Pending Approvals — outside the scroller; same
1087
+ {/* Sticky Pending Approvals — outside the scroller; same
1084
1088
  structure as the v1 baseline. The library's RO watches
1085
1089
  `contentRef` (INSIDE the scroller), so height changes to
1086
1090
  this sibling don't trigger the library directly. If a
1087
1091
  future UX requires snapping to bottom when approvals
1088
1092
  appear, that hookup goes here. */}
1089
- {pendingApprovals && pendingApprovals.length > 0 && (
1090
- <div
1091
- className={cn(
1092
- 'border-t border-ods-border bg-ods-bg/95 backdrop-blur-sm',
1093
- fullWidth ? 'w-full' : 'mx-auto w-full max-w-ods-content-narrow',
1094
- contentClassName ?? 'px-[var(--spacing-system-m)]',
1095
- )}
1096
- >
1097
- <ChatMessageEnhanced
1098
- role="assistant"
1099
- name={assistantType === 'mingo' ? 'Mingo' : 'Fae'}
1100
- content={pendingApprovals}
1101
- timestamp={new Date()}
1102
- showAvatar={showAvatars}
1103
- assistantType={assistantType}
1104
- approvalVariant={approvalVariant}
1105
- assistantIcon={assistantIcon}
1106
- NavLinkAnchor={NavLinkAnchor}
1107
- className="py-3"
1108
- />
1109
- </div>
1110
- )}
1111
- </div>
1112
- </OverlayOpenRegistryProvider>
1093
+ {pendingApprovals && pendingApprovals.length > 0 && (
1094
+ <div
1095
+ className={cn(
1096
+ 'border-t border-ods-border bg-ods-bg/95 backdrop-blur-sm',
1097
+ fullWidth ? 'w-full' : 'mx-auto w-full max-w-ods-content-narrow',
1098
+ contentClassName ?? 'px-[var(--spacing-system-m)]',
1099
+ )}
1100
+ >
1101
+ <ChatMessageEnhanced
1102
+ role="assistant"
1103
+ name={assistantType === 'mingo' ? 'Mingo' : 'Fae'}
1104
+ content={pendingApprovals}
1105
+ timestamp={new Date()}
1106
+ showAvatar={showAvatars}
1107
+ assistantType={assistantType}
1108
+ approvalVariant={approvalVariant}
1109
+ assistantIcon={assistantIcon}
1110
+ NavLinkAnchor={NavLinkAnchor}
1111
+ className="py-3"
1112
+ />
1113
+ </div>
1114
+ )}
1115
+ </div>
1116
+ </OverlayOpenRegistryProvider>
1117
+ </ChatAppearanceContext.Provider>
1113
1118
  );
1114
1119
  },
1115
1120
  );
@@ -0,0 +1,163 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, type ButtonHTMLAttributes, type HTMLAttributes, type ReactNode } from 'react';
4
+ import { cn } from '../../utils/cn';
5
+ import { ScrollFadeOverlay, useScrollFade } from '../ui/scroll-fade';
6
+
7
+ export interface ChatNavSidebarItem {
8
+ id: string;
9
+ label: string;
10
+ /** 24px glyph before the label. */
11
+ icon: ReactNode;
12
+ /** The view this item opens is the one on screen. */
13
+ active?: boolean;
14
+ onClick?: () => void;
15
+ }
16
+
17
+ export interface ChatNavSidebarProps extends HTMLAttributes<HTMLElement> {
18
+ /** Brand row at the top. */
19
+ logo?: ReactNode;
20
+ /** Primary actions under the logo (new chat, archive, help, ...). */
21
+ items: ChatNavSidebarItem[];
22
+ /** Caption over the list (e.g. "Your Current Chats"). */
23
+ sectionLabel?: string;
24
+ /** The list itself, typically `ChatListItem` rows or an empty state. Scrolls
25
+ * on its own; everything above it stays put. */
26
+ children?: ReactNode;
27
+ /** Pinned under the list, full width, above a divider (e.g. the MSP card). */
28
+ footer?: ReactNode;
29
+ }
30
+
31
+ /**
32
+ * Left rail of the fae chat v2 layout (Figma `chat-sidebar`): brand, a stack of
33
+ * navigation actions, a captioned scrolling list and a pinned footer. It only
34
+ * lays things out; which rows exist, what they show and what selecting them
35
+ * does belong to the host.
36
+ */
37
+ const ChatNavSidebar = forwardRef<HTMLElement, ChatNavSidebarProps>(
38
+ ({ className, logo, items, sectionLabel, children, footer, ...props }, ref) => {
39
+ const { scrollRef, fadeTop, fadeBottom, update: updateFade } = useScrollFade<HTMLDivElement>();
40
+
41
+ return (
42
+ <aside
43
+ ref={ref}
44
+ className={cn('flex h-full w-[296px] shrink-0 flex-col border-r border-ods-border bg-ods-bg', className)}
45
+ {...props}
46
+ >
47
+ <div className="flex min-h-0 flex-1 flex-col gap-[var(--spacing-system-m)] px-[var(--spacing-system-xs)] pt-[var(--spacing-system-mf)]">
48
+ {logo && <div className="flex shrink-0 items-center px-[var(--spacing-system-xs)]">{logo}</div>}
49
+
50
+ <nav className="flex shrink-0 flex-col gap-[var(--spacing-system-xxs)]">
51
+ {items.map(item => (
52
+ <ChatNavSidebarButton key={item.id} icon={item.icon} active={item.active} onClick={item.onClick}>
53
+ {item.label}
54
+ </ChatNavSidebarButton>
55
+ ))}
56
+ </nav>
57
+
58
+ <div className="flex min-h-0 flex-1 flex-col gap-[var(--spacing-system-xxs)]">
59
+ {sectionLabel && (
60
+ <p className="shrink-0 px-[var(--spacing-system-xs)] pt-[var(--spacing-system-xs)] text-ods-text-secondary text-h5">
61
+ {sectionLabel}
62
+ </p>
63
+ )}
64
+ <div className="relative flex min-h-0 flex-1 flex-col">
65
+ <div
66
+ ref={scrollRef}
67
+ onScroll={updateFade}
68
+ className="flex min-h-0 flex-1 flex-col gap-[var(--spacing-system-xxs)] overflow-y-auto overscroll-contain pb-[var(--spacing-system-mf)]"
69
+ >
70
+ {children}
71
+ </div>
72
+ <ScrollFadeOverlay edge="top" visible={fadeTop} />
73
+ <ScrollFadeOverlay edge="bottom" visible={fadeBottom} />
74
+ </div>
75
+ </div>
76
+ </div>
77
+
78
+ {footer && <div className="shrink-0 border-t border-ods-border">{footer}</div>}
79
+ </aside>
80
+ );
81
+ },
82
+ );
83
+
84
+ ChatNavSidebar.displayName = 'ChatNavSidebar';
85
+
86
+ interface ChatNavSidebarButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
87
+ icon: ReactNode;
88
+ active?: boolean;
89
+ }
90
+
91
+ function ChatNavSidebarButton({ icon, active, className, children, ...props }: ChatNavSidebarButtonProps) {
92
+ return (
93
+ <button
94
+ type="button"
95
+ aria-current={active ? 'page' : undefined}
96
+ className={cn(
97
+ 'flex w-full items-center gap-[var(--spacing-system-xs)] rounded-md p-[var(--spacing-system-xs)] text-left text-h4',
98
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus',
99
+ '[&_svg]:size-6 [&_svg]:shrink-0',
100
+ active ? 'text-ods-accent' : 'text-ods-text-primary [&_svg]:text-ods-text-secondary',
101
+ className,
102
+ )}
103
+ {...props}
104
+ >
105
+ {icon}
106
+ <span className="min-w-0 flex-1 truncate">{children}</span>
107
+ </button>
108
+ );
109
+ }
110
+
111
+ export interface ChatListItemProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'title'> {
112
+ title: ReactNode;
113
+ /** Second line (e.g. the ticket number). */
114
+ subtitle?: ReactNode;
115
+ /** 16px mark before the title (e.g. a resolved check). */
116
+ leading?: ReactNode;
117
+ /** 24px status glyph at the row's end. */
118
+ trailing?: ReactNode;
119
+ /** The chat open on screen. */
120
+ active?: boolean;
121
+ }
122
+
123
+ /**
124
+ * One row of the `ChatNavSidebar` list (Figma `chat-list-item`): a one-line
125
+ * title over a caption, an optional mark before the title and a status glyph
126
+ * at the end. The selected row takes the accent's secondary fill, so a tenant
127
+ * accent recolours it with the rest of the brand.
128
+ */
129
+ const ChatListItem = forwardRef<HTMLButtonElement, ChatListItemProps>(
130
+ ({ className, title, subtitle, leading, trailing, active, ...props }, ref) => (
131
+ <button
132
+ ref={ref}
133
+ type="button"
134
+ aria-current={active ? 'true' : undefined}
135
+ className={cn(
136
+ 'flex w-full shrink-0 items-center gap-[var(--spacing-system-xs)] rounded-md p-[var(--spacing-system-xs)] text-left',
137
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ods-focus',
138
+ active && 'bg-ods-accent-secondary',
139
+ className,
140
+ )}
141
+ {...props}
142
+ >
143
+ <span className="flex min-w-0 flex-1 flex-col">
144
+ <span className="flex min-w-0 items-center gap-[var(--spacing-system-xxs)]">
145
+ {leading && <span className="flex shrink-0 items-center [&_svg]:size-4">{leading}</span>}
146
+ <span className={cn('min-w-0 flex-1 truncate text-h4', active ? 'text-ods-accent' : 'text-ods-text-primary')}>
147
+ {title}
148
+ </span>
149
+ </span>
150
+ {subtitle != null && (
151
+ <span className={cn('truncate text-h6', active ? 'text-ods-text-primary' : 'text-ods-text-secondary')}>
152
+ {subtitle}
153
+ </span>
154
+ )}
155
+ </span>
156
+ {trailing && <span className="flex shrink-0 items-center [&_svg]:size-6">{trailing}</span>}
157
+ </button>
158
+ ),
159
+ );
160
+
161
+ ChatListItem.displayName = 'ChatListItem';
162
+
163
+ export { ChatNavSidebar, ChatListItem };