@flamingo-stack/openframe-frontend-core 0.0.186 → 0.0.188-snapshot.20260515214641

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 (106) hide show
  1. package/dist/{chunk-FMWHOUFE.js → chunk-6XYXWVYY.js} +2 -2
  2. package/dist/{chunk-ALW3D72O.cjs → chunk-HIMEIPED.cjs} +9 -9
  3. package/dist/{chunk-ALW3D72O.cjs.map → chunk-HIMEIPED.cjs.map} +1 -1
  4. package/dist/{chunk-LI2C4ZCU.js → chunk-HOMIFMOO.js} +17412 -17308
  5. package/dist/chunk-HOMIFMOO.js.map +1 -0
  6. package/dist/{chunk-RZ3HHPQH.js → chunk-J2C2TI5Z.js} +2174 -2119
  7. package/dist/chunk-J2C2TI5Z.js.map +1 -0
  8. package/dist/{chunk-SAWVZ5LA.cjs → chunk-SA7CUXAO.cjs} +1643 -1539
  9. package/dist/chunk-SA7CUXAO.cjs.map +1 -0
  10. package/dist/{chunk-TMD5LDX4.cjs → chunk-VJTFBYVG.cjs} +57 -2
  11. package/dist/{chunk-TMD5LDX4.cjs.map → chunk-VJTFBYVG.cjs.map} +1 -1
  12. package/dist/components/chat/approval-batch-message.d.ts.map +1 -1
  13. package/dist/components/chat/block-card.d.ts +39 -0
  14. package/dist/components/chat/block-card.d.ts.map +1 -0
  15. package/dist/components/chat/chat-message-enhanced.d.ts +2 -1
  16. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  17. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  18. package/dist/components/chat/chat-ref.types.d.ts +6 -0
  19. package/dist/components/chat/chat-ref.types.d.ts.map +1 -1
  20. package/dist/components/chat/chat-video-entity-card.d.ts +30 -0
  21. package/dist/components/chat/chat-video-entity-card.d.ts.map +1 -0
  22. package/dist/components/chat/context-compaction-display.d.ts.map +1 -1
  23. package/dist/components/chat/index.d.ts +2 -0
  24. package/dist/components/chat/index.d.ts.map +1 -1
  25. package/dist/components/chat/tool-execution-display.d.ts.map +1 -1
  26. package/dist/components/chat/types/api.types.d.ts +2 -6
  27. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  28. package/dist/components/chat/types/message.types.d.ts +20 -0
  29. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  30. package/dist/components/chat/types/network.types.d.ts +2 -0
  31. package/dist/components/chat/types/network.types.d.ts.map +1 -1
  32. package/dist/components/chat/types/processing.types.d.ts +2 -6
  33. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  34. package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -1
  35. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +2 -6
  36. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  37. package/dist/components/chat/utils/message-segment-accumulator.d.ts +2 -6
  38. package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
  39. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  40. package/dist/components/chat/utils/tool-call-helpers.d.ts +21 -2
  41. package/dist/components/chat/utils/tool-call-helpers.d.ts.map +1 -1
  42. package/dist/components/features/index.cjs +4 -4
  43. package/dist/components/features/index.js +3 -3
  44. package/dist/components/icons-v2-generated/index.cjs +4 -2
  45. package/dist/components/icons-v2-generated/index.cjs.map +1 -1
  46. package/dist/components/icons-v2-generated/index.d.ts +1 -0
  47. package/dist/components/icons-v2-generated/index.d.ts.map +1 -1
  48. package/dist/components/icons-v2-generated/index.js +3 -1
  49. package/dist/components/icons-v2-generated/loaders/dots-loader-icon.d.ts +8 -0
  50. package/dist/components/icons-v2-generated/loaders/dots-loader-icon.d.ts.map +1 -0
  51. package/dist/components/icons-v2-generated/loaders/index.d.ts +2 -0
  52. package/dist/components/icons-v2-generated/loaders/index.d.ts.map +1 -0
  53. package/dist/components/index.cjs +6 -4
  54. package/dist/components/index.cjs.map +1 -1
  55. package/dist/components/index.js +7 -5
  56. package/dist/components/navigation/index.cjs +4 -4
  57. package/dist/components/navigation/index.js +3 -3
  58. package/dist/components/ui/index.cjs +6 -4
  59. package/dist/components/ui/index.cjs.map +1 -1
  60. package/dist/components/ui/index.d.ts +0 -1
  61. package/dist/components/ui/index.d.ts.map +1 -1
  62. package/dist/components/ui/index.js +7 -5
  63. package/dist/hooks/index.cjs +3 -3
  64. package/dist/hooks/index.js +2 -2
  65. package/dist/index.cjs +6 -4
  66. package/dist/index.cjs.map +1 -1
  67. package/dist/index.js +7 -5
  68. package/dist/types/marketing.d.ts +1 -1
  69. package/dist/types/marketing.d.ts.map +1 -1
  70. package/dist/types/video-processing.d.ts +2 -1
  71. package/dist/types/video-processing.d.ts.map +1 -1
  72. package/package.json +1 -1
  73. package/src/components/chat/approval-batch-message.tsx +4 -5
  74. package/src/components/chat/block-card.tsx +44 -0
  75. package/src/components/chat/chat-message-enhanced.tsx +177 -64
  76. package/src/components/chat/chat-message-list.tsx +57 -0
  77. package/src/components/chat/chat-ref.types.ts +6 -0
  78. package/src/components/chat/chat-video-entity-card.tsx +66 -0
  79. package/src/components/chat/context-compaction-display.tsx +2 -3
  80. package/src/components/chat/index.ts +2 -0
  81. package/src/components/chat/thinking-display.tsx +2 -2
  82. package/src/components/chat/tool-execution-display.tsx +14 -11
  83. package/src/components/chat/types/api.types.ts +2 -2
  84. package/src/components/chat/types/message.types.ts +21 -0
  85. package/src/components/chat/types/network.types.ts +2 -0
  86. package/src/components/chat/types/processing.types.ts +2 -6
  87. package/src/components/chat/utils/chunk-parser.ts +2 -0
  88. package/src/components/chat/utils/extract-incomplete-message-state.ts +4 -2
  89. package/src/components/chat/utils/message-segment-accumulator.ts +11 -2
  90. package/src/components/chat/utils/process-historical-messages.ts +2 -0
  91. package/src/components/chat/utils/tool-call-helpers.ts +97 -14
  92. package/src/components/icons-v2/loaders/dots-loader.svg +1 -0
  93. package/src/components/icons-v2-generated/index.ts +1 -0
  94. package/src/components/icons-v2-generated/loaders/dots-loader-icon.tsx +53 -0
  95. package/src/components/icons-v2-generated/loaders/index.ts +1 -0
  96. package/src/components/ui/index.ts +0 -2
  97. package/src/stories/DotsLoaderIcon.stories.tsx +103 -0
  98. package/src/types/marketing.ts +1 -0
  99. package/src/types/video-processing.ts +3 -0
  100. package/dist/chunk-LI2C4ZCU.js.map +0 -1
  101. package/dist/chunk-RZ3HHPQH.js.map +0 -1
  102. package/dist/chunk-SAWVZ5LA.cjs.map +0 -1
  103. package/dist/components/ui/pulse-dots.d.ts +0 -7
  104. package/dist/components/ui/pulse-dots.d.ts.map +0 -1
  105. package/src/components/ui/pulse-dots.tsx +0 -56
  106. /package/dist/{chunk-FMWHOUFE.js.map → chunk-6XYXWVYY.js.map} +0 -0
@@ -0,0 +1,44 @@
1
+ "use client"
2
+
3
+ import React from 'react'
4
+
5
+ export interface BlockCardProps {
6
+ /** The inline pill rendered AT the marker position inside the
7
+ * paragraph. Typically the existing compact entity card (e.g. the
8
+ * 56×56 thumbnail card for webinars / customer interviews). When
9
+ * omitted, the marker is replaced by the ref title in a plain
10
+ * inline `<span>`. */
11
+ inline?: React.ReactNode
12
+ /** The block-level content rendered as a SIBLING below the markdown
13
+ * paragraph. Use for elements that cannot legally nest inside
14
+ * `<p>` — `<div>`, `<header>`, `<EntityVideoSection>`, `<Video>`,
15
+ * etc. The pre-scan in `chat-message-enhanced` extracts this and
16
+ * appends it after the `<SimpleMarkdownRenderer>` output for the
17
+ * containing segment, where block content is HTML-valid. */
18
+ children: React.ReactNode
19
+ }
20
+
21
+ /**
22
+ * Sentinel wrapper for `renderEntityCard` returns whose payload is
23
+ * block-level (divs / header / `<EntityVideoSection>` / `<Video>`) and
24
+ * therefore cannot legally render inside the assistant message's
25
+ * markdown paragraph.
26
+ *
27
+ * The chat renderer (`chat-message-enhanced.tsx`) does a pre-scan of
28
+ * each `text` segment's `[card://type:id]` markers, calls
29
+ * `renderEntityCard(ref)` for each, and detects `<BlockCard>` by React
30
+ * element type identity. When detected:
31
+ * 1. `inline` is placed AT the marker position inside the `<p>` —
32
+ * keeps the existing compact-card UX intact.
33
+ * 2. `children` is appended as a sibling BELOW the paragraph — where
34
+ * block content is HTML-valid and the browser doesn't eject it
35
+ * with a hydration mismatch.
36
+ *
37
+ * The component itself never renders — it's a tag, not a wrapper. The
38
+ * `return null` is unreachable in practice (the pre-scan replaces every
39
+ * `<BlockCard>` before render), but kept as a defensive no-op so a
40
+ * mis-routed `<BlockCard>` outside the chat path doesn't crash.
41
+ */
42
+ export function BlockCard(_props: BlockCardProps): React.ReactElement | null {
43
+ return null
44
+ }
@@ -1,10 +1,8 @@
1
1
  "use client"
2
2
 
3
- import { forwardRef, memo, useMemo } from "react"
3
+ import React, { forwardRef, memo, useMemo } from "react"
4
4
  import { cn } from "../../utils/cn"
5
5
  import { SquareAvatar } from "../ui/square-avatar"
6
- import { ChatTypingIndicator } from "./chat-typing-indicator"
7
- import { CyclingPhrase } from "./cycling-phrase"
8
6
  import { ToolExecutionDisplay } from "./tool-execution-display"
9
7
  import { ApprovalRequestMessage } from "./approval-request-message"
10
8
  import { ApprovalBatchMessage } from "./approval-batch-message"
@@ -14,8 +12,17 @@ import { ThinkingDisplay } from "./thinking-display"
14
12
  import { SimpleMarkdownRenderer } from "../ui/simple-markdown-renderer"
15
13
  import type { ChatRef } from "./chat-ref.types"
16
14
  import { remarkCardLinks } from "./remark-card-links"
15
+ import { BlockCard, type BlockCardProps } from "./block-card"
17
16
  import type { MessageSegment, MessageContent, ChatMessageEnhancedProps } from "./types"
18
17
 
18
+ /**
19
+ * Same regex shape as `remarkCardLinks` — kept in lockstep so the
20
+ * pre-scan and the remark plugin see the SAME set of markers. If the
21
+ * grammar widens (today: snake_case OR kebab-case; closer `]` OR `)`),
22
+ * both files must update.
23
+ */
24
+ const CARD_MARKER_REGEX = /\[card:\/\/([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)[\])]/g
25
+
19
26
  function normalizeContent(content: MessageContent): MessageSegment[] {
20
27
  if (typeof content === 'string') {
21
28
  return content ? [{ type: 'text', text: content }] : []
@@ -23,22 +30,6 @@ function normalizeContent(content: MessageContent): MessageSegment[] {
23
30
  return content
24
31
  }
25
32
 
26
- // Flamingo-themed cycling words shown next to the streaming indicator
27
- // (Claude-Code-style activity hint). Mix of classic AI verbs, flamingo
28
- // behaviours (strut/wade/preen/flock), and one Mingo neologism.
29
- const STREAMING_WORDS = [
30
- 'Thinking',
31
- 'Vibing',
32
- 'Mingoing',
33
- 'Strutting',
34
- 'Pondering',
35
- 'Wading',
36
- 'Hatching',
37
- 'Preening',
38
- 'Conjuring',
39
- 'Riffing',
40
- ] as const
41
-
42
33
  const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>(
43
34
  ({ className, role, content, name, avatar, isTyping = false, timestamp, showAvatar = true, assistantType, authorType: authorTypeProp, assistantIcon, chatRefs, renderEntityCard, ...props }, ref) => {
44
35
  const isUser = role === 'user'
@@ -63,15 +54,113 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
63
54
  () => (hasMarkerSupport ? [remarkCardLinks] : []),
64
55
  [hasMarkerSupport],
65
56
  )
57
+
58
+ const segments = useMemo(() => normalizeContent(content), [content])
59
+
60
+ /**
61
+ * Per-message rendering plan for `[card://type:id]` markers.
62
+ *
63
+ * Block-bearing markers SPLIT their containing text segment so the
64
+ * block payload (e.g. video player) renders AT THE MARKER POSITION
65
+ * in the text flow — not at the end of the segment, which causes
66
+ * the block to "appear high and drift down" while text streams in.
67
+ *
68
+ * Per-segment output is an array of parts:
69
+ * - `{ kind: 'text' }` — substring of the segment text, rendered
70
+ * through `<SimpleMarkdownRenderer>`. Ends
71
+ * with the block marker so the inline pill
72
+ * lands at the right spot via the `<a>`
73
+ * override.
74
+ * - `{ kind: 'block' }` — block payload, rendered as a sibling
75
+ * BELOW the preceding text chunk and above
76
+ * the next one.
77
+ *
78
+ * Inline-only markers (no `<BlockCard>` wrapper) do NOT split the
79
+ * segment; they're handled by the override at marker position via
80
+ * the shared `inlineByKey` map.
81
+ *
82
+ * Streaming behaviour: as a marker becomes complete in the streamed
83
+ * text, the regex matches, the segment splits at that point, and
84
+ * the block card lands right after the inline pill. Subsequent
85
+ * tokens render in the trailing chunk — block stays in position.
86
+ */
87
+ const renderingPlan = useMemo(() => {
88
+ if (!hasMarkerSupport) return null
89
+ const refs = chatRefs ?? {}
90
+ const render = renderEntityCard
91
+ const inlineByKey = new Map<string, React.ReactNode>()
92
+ type SegmentPart =
93
+ | { kind: 'text'; text: string }
94
+ | { kind: 'block'; key: string; node: React.ReactNode }
95
+ const partsBySegment = new Map<number, SegmentPart[]>()
96
+ if (!render) return { inlineByKey, partsBySegment }
97
+ const seenRendered = new Map<string, React.ReactNode>()
98
+ segments.forEach((segment, segIdx) => {
99
+ if (segment.type !== 'text') return
100
+ const text = segment.text
101
+ const parts: SegmentPart[] = []
102
+ let cursor = 0
103
+ CARD_MARKER_REGEX.lastIndex = 0
104
+ let match: RegExpExecArray | null
105
+ while ((match = CARD_MARKER_REGEX.exec(text)) !== null) {
106
+ const cardType = match[1]
107
+ const cardId = match[2]
108
+ const key = `${cardType}:${cardId}`
109
+ // Dedup renderEntityCard calls per unique key across the
110
+ // whole message — same key emitted twice (rare but possible)
111
+ // returns the cached node so React reuses the same instance.
112
+ let rendered = seenRendered.get(key)
113
+ if (!seenRendered.has(key)) {
114
+ const refMatch = refs[key]
115
+ rendered = refMatch ? render(refMatch) : undefined
116
+ seenRendered.set(key, rendered)
117
+ }
118
+ if (React.isValidElement(rendered) && rendered.type === BlockCard) {
119
+ const props = rendered.props as BlockCardProps
120
+ const markerEnd = match.index + match[0].length
121
+ // Text chunk INCLUDING the marker — the inline pill renders
122
+ // at the marker position via the `<a>` override.
123
+ parts.push({ kind: 'text', text: text.slice(cursor, markerEnd) })
124
+ parts.push({ kind: 'block', key, node: props.children })
125
+ cursor = markerEnd
126
+ const refMatch = refs[key]
127
+ inlineByKey.set(
128
+ key,
129
+ props.inline != null
130
+ ? props.inline
131
+ : <span className="text-ods-text-primary font-medium">{refMatch?.title ?? cardId}</span>,
132
+ )
133
+ } else if (rendered != null) {
134
+ // Inline-only — no split; remembered for the override.
135
+ inlineByKey.set(key, rendered)
136
+ }
137
+ }
138
+ // Trailing text after the last block marker (or the entire
139
+ // segment when no block markers fired).
140
+ if (cursor < text.length) {
141
+ parts.push({ kind: 'text', text: text.slice(cursor) })
142
+ }
143
+ // Only register the split plan when at least one block marker
144
+ // fired — otherwise the segment renders as one SimpleMarkdown-
145
+ // Renderer call (existing behaviour preserved for the
146
+ // overwhelming majority of segments that have no block cards).
147
+ if (parts.some((p) => p.kind === 'block')) {
148
+ partsBySegment.set(segIdx, parts)
149
+ }
150
+ })
151
+ return { inlineByKey, partsBySegment }
152
+ }, [hasMarkerSupport, chatRefs, renderEntityCard, segments])
153
+
66
154
  const cardComponentOverrides = useMemo(() => {
67
155
  if (!hasMarkerSupport) return undefined
68
156
  const refs = chatRefs ?? {}
69
- const render = renderEntityCard
157
+ const inlineByKey = renderingPlan?.inlineByKey
70
158
  return {
71
- // Override `<a>` to detect `card://` URLs emitted by `remarkCardLinks`
72
- // and delegate rendering to the host. Other href schemes pass
73
- // through unchanged — react-markdown's default `<a>` handler covers
74
- // them.
159
+ // Override `<a>` to detect `card://` URLs emitted by `remarkCardLinks`.
160
+ // The render result was pre-computed in `renderingPlan` so block-level
161
+ // payloads (e.g. video player cards) can be hoisted out of the
162
+ // paragraph as siblings — the inline pill stays at the marker
163
+ // position. Other href schemes pass through unchanged.
75
164
  a: ({ href, children, className: linkClassName, ...rest }: any) => {
76
165
  if (typeof href === 'string' && href.startsWith('card://')) {
77
166
  const stripped = href.slice('card://'.length)
@@ -80,27 +169,32 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
80
169
  const cardType = stripped.slice(0, sepIdx)
81
170
  const cardId = stripped.slice(sepIdx + 1)
82
171
  const key = `${cardType}:${cardId}`
83
- const refMatch: ChatRef | undefined = refs[key]
84
- if (refMatch && render) {
85
- const rendered = render(refMatch)
86
- if (rendered != null) return rendered
87
- }
172
+ const inline = inlineByKey?.get(key)
173
+ if (inline != null) return inline
88
174
  // No renderer, no ref, OR renderer returned null — fall back
89
175
  // to plain text title-only. Use any same-type ref's title if
90
176
  // available; otherwise the bare cardId. Never render the
91
177
  // literal `card://` URL.
178
+ const refMatch: ChatRef | undefined = refs[key]
92
179
  const fallbackTitle = (refMatch?.title)
93
180
  ?? Object.values(refs).find((r) => r.type === cardType)?.title
94
181
  ?? cardId
95
182
  return <span className="text-ods-text-primary">{fallbackTitle}</span>
96
183
  }
97
184
  }
185
+ // Chat-context links ALWAYS open in a new tab — same rule
186
+ // every chat-inline entity card uses via `navLinkProps({
187
+ // alwaysNewTab: true })`. The user's CURRENT context is the
188
+ // conversation; navigating away in-place would unmount the
189
+ // chat and lose the thread. Applies to absolute (http) AND
190
+ // relative (`/foo/bar`) hrefs so the rule is one line, not
191
+ // a branching check.
98
192
  return (
99
193
  <a
100
194
  href={href}
101
195
  className={linkClassName}
102
- target={typeof href === 'string' && href.startsWith('http') ? '_blank' : undefined}
103
- rel={typeof href === 'string' && href.startsWith('http') ? 'noopener noreferrer' : undefined}
196
+ target={typeof href === 'string' && href.length > 0 ? '_blank' : undefined}
197
+ rel={typeof href === 'string' && href.length > 0 ? 'noopener noreferrer' : undefined}
104
198
  {...rest}
105
199
  >
106
200
  {children}
@@ -108,7 +202,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
108
202
  )
109
203
  },
110
204
  }
111
- }, [hasMarkerSupport, chatRefs, renderEntityCard])
205
+ }, [hasMarkerSupport, chatRefs, renderingPlan])
112
206
 
113
207
  const getAvatarProps = () => {
114
208
  const displayName = name || (isUser ? "User" : assistantType === 'mingo' ? "Mingo" : "Fae")
@@ -133,20 +227,9 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
133
227
  }
134
228
 
135
229
  const avatarProps = getAvatarProps()
136
- const segments = normalizeContent(content)
137
230
 
138
231
  const isSystem = authorType === 'system'
139
232
 
140
- // While `isTyping` says streaming is active, the agent is actually
141
- // PAUSED whenever the last segment is an approval awaiting user
142
- // input — pending status (or undefined, which is the initial state
143
- // before backend confirms). Showing the typing indicator in this
144
- // state would lie to the user about what's happening.
145
- const lastSegment = segments[segments.length - 1]
146
- const isPausedOnApproval = !!lastSegment
147
- && (lastSegment.type === 'approval_request' || lastSegment.type === 'approval_batch')
148
- && (lastSegment.status === undefined || lastSegment.status === 'pending')
149
-
150
233
  return (
151
234
  <div
152
235
  ref={ref}
@@ -196,19 +279,58 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
196
279
  {(!isSystem || segments.length > 0) && <div className="flex flex-col gap-2">
197
280
  {segments.map((segment, index) => {
198
281
  if (segment.type === 'text') {
282
+ const parts = renderingPlan?.partsBySegment.get(index)
283
+ const wrapperClass = cn(
284
+ "min-w-0 w-full break-words text-h4",
285
+ isError ? "text-ods-error" : "text-ods-text-primary",
286
+ )
287
+ // No block markers in this segment → single
288
+ // SimpleMarkdownRenderer call (existing behaviour
289
+ // preserved for the vast majority of messages).
290
+ if (!parts || parts.length === 0) {
291
+ return (
292
+ <div key={index} className={wrapperClass}>
293
+ <SimpleMarkdownRenderer
294
+ content={segment.text}
295
+ textSize="compact"
296
+ additionalRemarkPlugins={cardRemarkPlugins}
297
+ componentOverrides={cardComponentOverrides}
298
+ />
299
+ </div>
300
+ )
301
+ }
302
+ // Block markers present → split text at each marker
303
+ // and interleave block payloads. Each text chunk
304
+ // includes its trailing marker so the inline pill
305
+ // renders at the right position via the `<a>`
306
+ // override. Block payloads land AS SIBLINGS between
307
+ // text chunks — HTML-valid (block DOM never nests
308
+ // inside `<p>`) AND positionally correct (block
309
+ // appears where the marker is in the flow, not at
310
+ // the segment's end). Stable React keys come from
311
+ // the card key (block) and chunk position (text);
312
+ // streaming token-by-token reuses the same React
313
+ // instances so `<Video>` doesn't remount mid-play.
199
314
  return (
200
- <div key={index} className={cn(
201
- "min-w-0 w-full break-words text-h4",
202
- isError
203
- ? "text-ods-error"
204
- : "text-ods-text-primary"
205
- )}>
206
- <SimpleMarkdownRenderer
207
- content={segment.text}
208
- textSize="compact"
209
- additionalRemarkPlugins={cardRemarkPlugins}
210
- componentOverrides={cardComponentOverrides}
211
- />
315
+ <div key={index} className={wrapperClass}>
316
+ {parts.map((part, pIdx) => {
317
+ if (part.kind === 'text') {
318
+ return (
319
+ <SimpleMarkdownRenderer
320
+ key={`t-${pIdx}`}
321
+ content={part.text}
322
+ textSize="compact"
323
+ additionalRemarkPlugins={cardRemarkPlugins}
324
+ componentOverrides={cardComponentOverrides}
325
+ />
326
+ )
327
+ }
328
+ return (
329
+ <div key={`b-${part.key}`} className="my-3">
330
+ {part.node}
331
+ </div>
332
+ )
333
+ })}
212
334
  </div>
213
335
  )
214
336
  } else if (segment.type === 'tool_execution') {
@@ -265,15 +387,6 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
265
387
  }
266
388
  return null
267
389
  })}
268
- {isTyping && !isPausedOnApproval && (
269
- <div className="flex items-center gap-3">
270
- <ChatTypingIndicator />
271
- <CyclingPhrase
272
- words={STREAMING_WORDS}
273
- className="text-ods-text-secondary text-body-sm"
274
- />
275
- </div>
276
- )}
277
390
  </div>}
278
391
  </div>
279
392
  </div>
@@ -5,8 +5,26 @@ import { useStickToBottom } from "use-stick-to-bottom"
5
5
  import { cn } from "../../utils/cn"
6
6
  import { ChatMessageEnhanced } from "./chat-message-enhanced"
7
7
  import { ChatMessageListSkeleton } from "./chat-message-skeleton"
8
+ import { DotsLoaderIcon } from "../icons-v2-generated"
9
+ import { CyclingPhrase } from "./cycling-phrase"
8
10
  import type { ChatMessageListProps } from "./types"
9
11
 
12
+ // Flamingo-themed cycling words shown next to the streaming indicator
13
+ // (Claude-Code-style activity hint). Mix of classic AI verbs, flamingo
14
+ // behaviours (strut/wade/preen/flock), and one Mingo neologism.
15
+ const STREAMING_WORDS = [
16
+ 'Thinking',
17
+ 'Vibing',
18
+ 'Mingoing',
19
+ 'Strutting',
20
+ 'Pondering',
21
+ 'Wading',
22
+ 'Hatching',
23
+ 'Preening',
24
+ 'Conjuring',
25
+ 'Riffing',
26
+ ] as const
27
+
10
28
  /*
11
29
  * Stick-to-bottom: `use-stick-to-bottom` (stackblitz-labs)
12
30
  *
@@ -243,6 +261,26 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
243
261
  contentRef(el)
244
262
  }
245
263
 
264
+ // Footer-pinned streaming loader. Rendered OUTSIDE the scroller (a
265
+ // sibling of the scroll div, like the sticky approvals below) so it
266
+ // stays put while messages stream and grow — no jitter from the
267
+ // last-message height changing under it.
268
+ //
269
+ // Hidden when the agent is actually PAUSED waiting on the user: the
270
+ // last message's last segment is a pending approval (inline), or
271
+ // there are escalated pending approvals shown in the sticky bar.
272
+ const lastMessage = messages[messages.length - 1]
273
+ const lastSegments = Array.isArray(lastMessage?.content) ? lastMessage.content : null
274
+ const lastSegment = lastSegments?.[lastSegments.length - 1]
275
+ const isPausedOnApproval =
276
+ !!lastSegment &&
277
+ (lastSegment.type === 'approval_request' || lastSegment.type === 'approval_batch') &&
278
+ (lastSegment.status === undefined || lastSegment.status === 'pending')
279
+ const showStreamingLoader =
280
+ isTyping &&
281
+ !isPausedOnApproval &&
282
+ !(pendingApprovals && pendingApprovals.length > 0)
283
+
246
284
  return (
247
285
  <div className="relative flex-1 min-h-0 flex flex-col">
248
286
  <div
@@ -286,6 +324,25 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
286
324
  </div>
287
325
  </div>
288
326
 
327
+ {/* Footer-pinned streaming loader — outside the scroller so it
328
+ doesn't jitter as the streaming message grows. Color is set
329
+ via inline style (CSS var) rather than a Tailwind class so it
330
+ is JIT-independent when this lib is consumed via yalc. */}
331
+ {showStreamingLoader && (
332
+ <div
333
+ className={cn(
334
+ "mx-auto w-full max-w-3xl flex items-center gap-1 py-2",
335
+ contentClassName || "px-4",
336
+ )}
337
+ style={{ color: 'var(--color-text-muted)' }}
338
+ role="status"
339
+ aria-live="polite"
340
+ >
341
+ <DotsLoaderIcon className="w-6 h-6" />
342
+ <CyclingPhrase words={STREAMING_WORDS} className="text-sm" />
343
+ </div>
344
+ )}
345
+
289
346
  {/* Sticky Pending Approvals — outside the scroller; same
290
347
  structure as the v1 baseline. The library's RO watches
291
348
  `contentRef` (INSIDE the scroller), so height changes to
@@ -14,6 +14,12 @@ export interface ChatRef {
14
14
  /** documentType from the host's RAG config (e.g. 'webinar', 'customer_interview').
15
15
  * Treated as opaque by the OSS-lib — the host owns the type vocabulary. */
16
16
  type: string
17
+ /** RagTableConfig.id (e.g. 'data-room-docs', 'webinars'). Drives the
18
+ * icon + label lookup in the host's `RAG_SOURCE_DISPLAY` registry —
19
+ * same direct-keyed lookup the chips + search results use. Optional
20
+ * for backward-compat with older wire payloads; when absent the host
21
+ * resolver falls back to reverse-mapping `type → sourceRepo`. */
22
+ sourceRepo?: string
17
23
  /** Primary-key value. Opaque string downstream. */
18
24
  id: string
19
25
  /** Display title — used for fallback rendering when the host's renderer
@@ -0,0 +1,66 @@
1
+ "use client"
2
+
3
+ import React from 'react'
4
+ import { EntityVideoSection } from '../features/entity-video-section'
5
+ import type { ChatRef } from './chat-ref.types'
6
+
7
+ /**
8
+ * <ChatVideoEntityCard> — chat-side inline render for video-bearing
9
+ * entity refs. Reuses `<EntityVideoSection>` (the SAME component the
10
+ * detail pages use for customer interviews, investor updates, product
11
+ * releases, and case studies) so the chat surface matches the public
12
+ * pages 1:1: Full Video / Highlights tabs, YouTube facade vs Mux HLS
13
+ * routing, native fullscreen, captions, posters.
14
+ *
15
+ * Stripped-chrome rendering: just the player + tabs, no title, no
16
+ * date, no Ask button. The inline pill at the marker position (in the
17
+ * paragraph above) already shows the title, date, and Ask affordance
18
+ * via the host's existing compact entity card. Duplicating those
19
+ * here doubles the UX and steals vertical space inside the chat
20
+ * panel. v6.1 §B.2.7.
21
+ *
22
+ * Activation contract: the host's dispatch routes here only when
23
+ * `chatRef.metadata.videoUrl`, `youtubeUrl`, or `highlightVideoUrl`
24
+ * is a non-empty string. URL safety is handled inside `<Video>` (the
25
+ * single source of truth for player URL routing) — no duplicate
26
+ * sanitization at the card level.
27
+ */
28
+ export interface ChatVideoEntityCardProps {
29
+ /** Required. `metadata.videoUrl` / `youtubeUrl` / `highlightVideoUrl`
30
+ * control routing inside `<EntityVideoSection>`. */
31
+ chatRef: ChatRef
32
+ }
33
+
34
+ function readString(value: unknown): string | null {
35
+ return typeof value === 'string' && value.length > 0 ? value : null
36
+ }
37
+
38
+ export function ChatVideoEntityCard({
39
+ chatRef,
40
+ }: ChatVideoEntityCardProps): React.ReactElement {
41
+ const m = chatRef.metadata ?? {}
42
+ const videoUrl = readString(m.videoUrl)
43
+ const youtubeUrl = readString(m.youtubeUrl)
44
+ const poster = readString(m.videoPoster)
45
+ const highlightUrl = readString(m.highlightVideoUrl)
46
+ const highlightPoster = readString(m.highlightVideoPoster)
47
+
48
+ // No wrapping Card / header — the player is the whole card. The
49
+ // 16:9 aspect / rounded-corners / border come from <Video>'s own
50
+ // `layout="centered"` styling inside <EntityVideoSection>.
51
+ return (
52
+ <EntityVideoSection
53
+ mainVideoUrl={videoUrl}
54
+ youtubeUrl={youtubeUrl}
55
+ highlightVideoUrl={highlightUrl}
56
+ highlightVideoThumbnail={highlightPoster}
57
+ mainVideoPoster={poster}
58
+ title={chatRef.title}
59
+ // Intentionally omitted for chat density:
60
+ // videoSummary — assistant text above already covers it.
61
+ // MarkdownRenderer — unused when videoSummary is omitted.
62
+ // videoBites — separate ask; chat doesn't surface bites
63
+ // today (planned follow-up).
64
+ />
65
+ )
66
+ }
@@ -3,8 +3,7 @@
3
3
  import { forwardRef } from "react"
4
4
 
5
5
  import { cn } from "../../utils/cn"
6
- import { CheckCircleIcon } from "../icons-v2-generated"
7
- import { PulseDots } from "../ui/pulse-dots"
6
+ import { CheckCircleIcon, DotsLoaderIcon } from "../icons-v2-generated"
8
7
  import type { ContextCompactionDisplayProps } from "./types"
9
8
 
10
9
 
@@ -28,7 +27,7 @@ const ContextCompactionDisplay = forwardRef<HTMLDivElement, ContextCompactionDis
28
27
  {label}
29
28
  </span>
30
29
  {isStarted ? (
31
- <PulseDots size="sm" />
30
+ <DotsLoaderIcon size={16} className="text-ods-text-secondary" />
32
31
  ) : (
33
32
  <CheckCircleIcon className="w-4 h-4 text-ods-success shrink-0" />
34
33
  )}
@@ -12,6 +12,8 @@ export * from './chat-input'
12
12
  export * from './slash-command-suggestions'
13
13
  export * from './chat-message-enhanced'
14
14
  export * from './chat-message-list'
15
+ export * from './chat-video-entity-card'
16
+ export * from './block-card'
15
17
  export * from './chat-quick-action'
16
18
  export * from './chat-ticket-item'
17
19
  export * from './chat-ticket-list'
@@ -3,7 +3,7 @@
3
3
  import { forwardRef, useState } from "react"
4
4
 
5
5
  import { cn } from "../../utils/cn"
6
- import { PulseDots } from "../ui/pulse-dots"
6
+ import { DotsLoaderIcon } from "../icons-v2-generated"
7
7
  import { SimpleMarkdownRenderer } from "../ui/simple-markdown-renderer"
8
8
  import { ExpandChevron } from "./expand-chevron"
9
9
  import { useCollapsible } from "./hooks/use-collapsible"
@@ -97,7 +97,7 @@ const ThinkingDisplay = forwardRef<HTMLDivElement, ThinkingDisplayProps>(
97
97
  <span className="text-sm font-medium leading-snug text-ods-text-secondary">
98
98
  {label}:
99
99
  </span>
100
- {isStreaming && <PulseDots size="sm" />}
100
+ {isStreaming && <DotsLoaderIcon size={16} className="text-ods-text-secondary" />}
101
101
  </div>
102
102
 
103
103
  <div className="flex-1 min-w-0" style={containerStyle}>
@@ -3,14 +3,13 @@
3
3
  import { forwardRef, useMemo, useState } from "react"
4
4
 
5
5
  import { cn } from "../../utils/cn"
6
- import { CheckCircleIcon, XmarkCircleIcon } from "../icons-v2-generated"
6
+ import { CheckCircleIcon, DotsLoaderIcon, XmarkCircleIcon } from "../icons-v2-generated"
7
7
  import { ToolType } from "../platform"
8
8
  import { ToolIcon } from "../tool-icon"
9
- import { PulseDots } from "../ui/pulse-dots"
10
9
  import { ExpandChevron } from "./expand-chevron"
11
10
  import { useCollapsible } from "./hooks/use-collapsible"
12
11
  import { ArgRow, ResultBlock } from "./tool-call-blocks"
13
- import { COMMAND_BODY_ARG_KEYS } from "./utils/tool-call-helpers"
12
+ import { COMMAND_BODY_ARG_KEYS, getToolCallTitle } from "./utils/tool-call-helpers"
14
13
  import type { ToolExecutionDisplayProps } from "./types"
15
14
 
16
15
  const COMMAND_BODY_KEYS = new Set<string>(COMMAND_BODY_ARG_KEYS)
@@ -24,11 +23,15 @@ const ToolExecutionDisplay = forwardRef<HTMLDivElement, ToolExecutionDisplayProp
24
23
  const isExecuted = message.type === "EXECUTED_TOOL"
25
24
  const integratedToolType = (message.integratedToolType as ToolType) || ("OPENFRAME" as ToolType)
26
25
 
27
- const previewText = useMemo(() => {
28
- const command = message.parameters?.command ?? message.parameters?.query
29
- if (command) return String(command)
30
- return message.toolFunction ?? ""
31
- }, [message.toolFunction, message.parameters])
26
+ const previewText = useMemo(
27
+ () =>
28
+ getToolCallTitle({
29
+ args: message.parameters,
30
+ title: message.toolTitle,
31
+ name: message.toolFunction,
32
+ }),
33
+ [message.parameters, message.toolTitle, message.toolFunction],
34
+ )
32
35
 
33
36
  const argEntries = useMemo<Array<[string, unknown]>>(() => {
34
37
  if (!message.parameters || typeof message.parameters !== "object") return []
@@ -41,7 +44,7 @@ const ToolExecutionDisplay = forwardRef<HTMLDivElement, ToolExecutionDisplayProp
41
44
  const hasBody = argEntries.length > 0 || hasResult || isExecuting
42
45
 
43
46
  const renderStatusIcon = () => {
44
- if (isExecuting) return <PulseDots size="sm" />
47
+ if (isExecuting) return <DotsLoaderIcon size={16} className="text-ods-text-secondary" />
45
48
  if (isExecuted && message.success === true) return <CheckCircleIcon className="w-4 h-4 text-ods-success" />
46
49
  if (isExecuted && message.success === false) return <XmarkCircleIcon className="w-4 h-4 text-ods-error" />
47
50
  return null
@@ -72,7 +75,7 @@ const ToolExecutionDisplay = forwardRef<HTMLDivElement, ToolExecutionDisplayProp
72
75
  : "text-ods-text-secondary line-clamp-2 max-h-10 break-all",
73
76
  )}
74
77
  >
75
- {expanded ? message.toolFunction || previewText : previewText}
78
+ {previewText}
76
79
  </div>
77
80
  <div className="flex items-center justify-center shrink-0 w-5 h-5">{renderStatusIcon()}</div>
78
81
  <div className="flex items-center justify-center shrink-0 w-5 h-5">
@@ -91,7 +94,7 @@ const ToolExecutionDisplay = forwardRef<HTMLDivElement, ToolExecutionDisplayProp
91
94
  {isExecuting && (
92
95
  <div className="flex flex-col gap-1 items-start w-full">
93
96
  <span className="text-ods-text-secondary">Result:</span>
94
- <PulseDots size="sm" />
97
+ <DotsLoaderIcon size={16} className="text-ods-text-secondary" />
95
98
  </div>
96
99
  )}
97
100
  </div>
@@ -5,7 +5,7 @@
5
5
 
6
6
  import type { ChunkData, NatsMessageType, FetchChunksFunction } from './network.types'
7
7
  import type { ChatType, ChatApprovalStatus } from './chat.types'
8
- import type { MessageSegment, PendingToolCallData, TokenUsageData } from './message.types'
8
+ import type { MessageSegment, PendingToolCallData, TokenUsageData, ExecutingToolState } from './message.types'
9
9
 
10
10
  // ========== Hook Options ==========
11
11
 
@@ -150,7 +150,7 @@ export interface UseRealtimeChunkProcessorOptions {
150
150
  /** Pending approvals that haven't been resolved */
151
151
  pendingApprovals?: Map<string, { command: string; explanation?: string; approvalType: string }>
152
152
  /** Executing tools waiting for completion */
153
- executingTools?: Map<string, { integratedToolType: string; toolFunction: string; parameters?: Record<string, any> }>
153
+ executingTools?: Map<string, ExecutingToolState>
154
154
  /** Escalated approvals */
155
155
  escalatedApprovals?: Map<
156
156
  string,