@flamingo-stack/openframe-frontend-core 0.0.186 → 0.0.188

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 (100) 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-RZ3HHPQH.js → chunk-J2C2TI5Z.js} +2174 -2119
  5. package/dist/chunk-J2C2TI5Z.js.map +1 -0
  6. package/dist/{chunk-TMD5LDX4.cjs → chunk-VJTFBYVG.cjs} +57 -2
  7. package/dist/{chunk-TMD5LDX4.cjs.map → chunk-VJTFBYVG.cjs.map} +1 -1
  8. package/dist/{chunk-SAWVZ5LA.cjs → chunk-W5AWCFKE.cjs} +1641 -1537
  9. package/dist/chunk-W5AWCFKE.cjs.map +1 -0
  10. package/dist/{chunk-LI2C4ZCU.js → chunk-YOMHP4V3.js} +17410 -17306
  11. package/dist/chunk-YOMHP4V3.js.map +1 -0
  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/package.json +1 -1
  69. package/src/components/chat/approval-batch-message.tsx +4 -5
  70. package/src/components/chat/block-card.tsx +44 -0
  71. package/src/components/chat/chat-message-enhanced.tsx +168 -62
  72. package/src/components/chat/chat-message-list.tsx +57 -0
  73. package/src/components/chat/chat-ref.types.ts +6 -0
  74. package/src/components/chat/chat-video-entity-card.tsx +66 -0
  75. package/src/components/chat/context-compaction-display.tsx +2 -3
  76. package/src/components/chat/index.ts +2 -0
  77. package/src/components/chat/thinking-display.tsx +2 -2
  78. package/src/components/chat/tool-execution-display.tsx +14 -11
  79. package/src/components/chat/types/api.types.ts +2 -2
  80. package/src/components/chat/types/message.types.ts +21 -0
  81. package/src/components/chat/types/network.types.ts +2 -0
  82. package/src/components/chat/types/processing.types.ts +2 -6
  83. package/src/components/chat/utils/chunk-parser.ts +2 -0
  84. package/src/components/chat/utils/extract-incomplete-message-state.ts +4 -2
  85. package/src/components/chat/utils/message-segment-accumulator.ts +11 -2
  86. package/src/components/chat/utils/process-historical-messages.ts +2 -0
  87. package/src/components/chat/utils/tool-call-helpers.ts +97 -14
  88. package/src/components/icons-v2/loaders/dots-loader.svg +1 -0
  89. package/src/components/icons-v2-generated/index.ts +1 -0
  90. package/src/components/icons-v2-generated/loaders/dots-loader-icon.tsx +53 -0
  91. package/src/components/icons-v2-generated/loaders/index.ts +1 -0
  92. package/src/components/ui/index.ts +0 -2
  93. package/src/stories/DotsLoaderIcon.stories.tsx +103 -0
  94. package/dist/chunk-LI2C4ZCU.js.map +0 -1
  95. package/dist/chunk-RZ3HHPQH.js.map +0 -1
  96. package/dist/chunk-SAWVZ5LA.cjs.map +0 -1
  97. package/dist/components/ui/pulse-dots.d.ts +0 -7
  98. package/dist/components/ui/pulse-dots.d.ts.map +0 -1
  99. package/src/components/ui/pulse-dots.tsx +0 -56
  100. /package/dist/{chunk-FMWHOUFE.js.map → chunk-6XYXWVYY.js.map} +0 -0
@@ -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,15 +169,13 @@ 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
@@ -108,7 +195,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
108
195
  )
109
196
  },
110
197
  }
111
- }, [hasMarkerSupport, chatRefs, renderEntityCard])
198
+ }, [hasMarkerSupport, chatRefs, renderingPlan])
112
199
 
113
200
  const getAvatarProps = () => {
114
201
  const displayName = name || (isUser ? "User" : assistantType === 'mingo' ? "Mingo" : "Fae")
@@ -133,20 +220,9 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
133
220
  }
134
221
 
135
222
  const avatarProps = getAvatarProps()
136
- const segments = normalizeContent(content)
137
223
 
138
224
  const isSystem = authorType === 'system'
139
225
 
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
226
  return (
151
227
  <div
152
228
  ref={ref}
@@ -196,19 +272,58 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
196
272
  {(!isSystem || segments.length > 0) && <div className="flex flex-col gap-2">
197
273
  {segments.map((segment, index) => {
198
274
  if (segment.type === 'text') {
275
+ const parts = renderingPlan?.partsBySegment.get(index)
276
+ const wrapperClass = cn(
277
+ "min-w-0 w-full break-words text-h4",
278
+ isError ? "text-ods-error" : "text-ods-text-primary",
279
+ )
280
+ // No block markers in this segment → single
281
+ // SimpleMarkdownRenderer call (existing behaviour
282
+ // preserved for the vast majority of messages).
283
+ if (!parts || parts.length === 0) {
284
+ return (
285
+ <div key={index} className={wrapperClass}>
286
+ <SimpleMarkdownRenderer
287
+ content={segment.text}
288
+ textSize="compact"
289
+ additionalRemarkPlugins={cardRemarkPlugins}
290
+ componentOverrides={cardComponentOverrides}
291
+ />
292
+ </div>
293
+ )
294
+ }
295
+ // Block markers present → split text at each marker
296
+ // and interleave block payloads. Each text chunk
297
+ // includes its trailing marker so the inline pill
298
+ // renders at the right position via the `<a>`
299
+ // override. Block payloads land AS SIBLINGS between
300
+ // text chunks — HTML-valid (block DOM never nests
301
+ // inside `<p>`) AND positionally correct (block
302
+ // appears where the marker is in the flow, not at
303
+ // the segment's end). Stable React keys come from
304
+ // the card key (block) and chunk position (text);
305
+ // streaming token-by-token reuses the same React
306
+ // instances so `<Video>` doesn't remount mid-play.
199
307
  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
- />
308
+ <div key={index} className={wrapperClass}>
309
+ {parts.map((part, pIdx) => {
310
+ if (part.kind === 'text') {
311
+ return (
312
+ <SimpleMarkdownRenderer
313
+ key={`t-${pIdx}`}
314
+ content={part.text}
315
+ textSize="compact"
316
+ additionalRemarkPlugins={cardRemarkPlugins}
317
+ componentOverrides={cardComponentOverrides}
318
+ />
319
+ )
320
+ }
321
+ return (
322
+ <div key={`b-${part.key}`} className="my-3">
323
+ {part.node}
324
+ </div>
325
+ )
326
+ })}
212
327
  </div>
213
328
  )
214
329
  } else if (segment.type === 'tool_execution') {
@@ -265,15 +380,6 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
265
380
  }
266
381
  return null
267
382
  })}
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
383
  </div>}
278
384
  </div>
279
385
  </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,
@@ -35,6 +35,8 @@ export interface ToolExecutionData {
35
35
  type: 'EXECUTING_TOOL' | 'EXECUTED_TOOL'
36
36
  integratedToolType: string
37
37
  toolFunction: string
38
+ /** Backend-issued human-readable title (mirrors `PendingToolCallData.toolTitle`). */
39
+ toolTitle?: string
38
40
  parameters?: Record<string, any>
39
41
  result?: string
40
42
  success?: boolean
@@ -46,6 +48,21 @@ export interface ToolExecutionData {
46
48
  toolExecutionRequestId?: string
47
49
  }
48
50
 
51
+ /**
52
+ * Snapshot of an in-flight tool kept between the `EXECUTING_TOOL` and
53
+ * `EXECUTED_TOOL` events. The backend only sends `toolTitle` on
54
+ * `EXECUTING_TOOL`; carrying this state lets the accumulator restore it onto
55
+ * the merged `EXECUTED_TOOL` segment instead of falling back to the raw
56
+ * `toolFunction`.
57
+ */
58
+ export interface ExecutingToolState {
59
+ integratedToolType: string
60
+ toolFunction: string
61
+ /** Mirrors {@link ToolExecutionData.toolTitle}; absent on `EXECUTED_TOOL`. */
62
+ toolTitle?: string
63
+ parameters?: Record<string, any>
64
+ }
65
+
49
66
  // ========== Approval Request Types ==========
50
67
 
51
68
  export interface ApprovalRequestData {
@@ -171,6 +188,8 @@ export interface ExecutingToolMessageData extends MessageDataBase {
171
188
  type: 'EXECUTING_TOOL'
172
189
  integratedToolType?: string
173
190
  toolFunction?: string
191
+ /** Backend-issued human-readable title (wire field, mirrors `ChunkData.title`). */
192
+ title?: string
174
193
  parameters?: Record<string, any>
175
194
  toolExecutionRequestId?: string
176
195
  }
@@ -179,6 +198,8 @@ export interface ExecutedToolMessageData extends MessageDataBase {
179
198
  type: 'EXECUTED_TOOL'
180
199
  integratedToolType?: string
181
200
  toolFunction?: string
201
+ /** Backend-issued human-readable title (wire field, mirrors `ChunkData.title`). */
202
+ title?: string
182
203
  parameters?: Record<string, any>
183
204
  result?: string
184
205
  success?: boolean
@@ -38,6 +38,8 @@ export interface ChunkData {
38
38
  text?: string
39
39
  integratedToolType?: string
40
40
  toolFunction?: string
41
+ /** Execution chunks carry the human-readable title as `title`. */
42
+ title?: string
41
43
  parameters?: Record<string, any>
42
44
  result?: string
43
45
  success?: boolean