@flamingo-stack/openframe-frontend-core 0.0.181 → 0.0.182

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 (39) hide show
  1. package/dist/{chunk-VEOBMVF5.js → chunk-CLZ3QQMJ.js} +4211 -4123
  2. package/dist/chunk-CLZ3QQMJ.js.map +1 -0
  3. package/dist/{chunk-L5AAJ3QN.cjs → chunk-IWMK4MH4.cjs} +615 -527
  4. package/dist/chunk-IWMK4MH4.cjs.map +1 -0
  5. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  6. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  7. package/dist/components/chat/cycling-phrase.d.ts +30 -0
  8. package/dist/components/chat/cycling-phrase.d.ts.map +1 -0
  9. package/dist/components/chat/hooks/index.d.ts +0 -1
  10. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  11. package/dist/components/chat/index.d.ts +0 -1
  12. package/dist/components/chat/index.d.ts.map +1 -1
  13. package/dist/components/features/index.cjs +2 -2
  14. package/dist/components/features/index.js +1 -1
  15. package/dist/components/index.cjs +2 -6
  16. package/dist/components/index.cjs.map +1 -1
  17. package/dist/components/index.js +1 -5
  18. package/dist/components/navigation/index.cjs +2 -2
  19. package/dist/components/navigation/index.js +1 -1
  20. package/dist/components/ui/index.cjs +2 -6
  21. package/dist/components/ui/index.cjs.map +1 -1
  22. package/dist/components/ui/index.js +1 -5
  23. package/dist/index.cjs +2 -6
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.js +1 -5
  26. package/package.json +1 -1
  27. package/src/components/chat/chat-message-enhanced.tsx +37 -5
  28. package/src/components/chat/chat-message-list.tsx +51 -179
  29. package/src/components/chat/cycling-phrase.tsx +129 -0
  30. package/src/components/chat/hooks/index.ts +0 -1
  31. package/src/components/chat/index.ts +0 -1
  32. package/dist/chunk-L5AAJ3QN.cjs.map +0 -1
  33. package/dist/chunk-VEOBMVF5.js.map +0 -1
  34. package/dist/components/chat/chat-message-loader.d.ts +0 -23
  35. package/dist/components/chat/chat-message-loader.d.ts.map +0 -1
  36. package/dist/components/chat/hooks/use-delayed-flag.d.ts +0 -25
  37. package/dist/components/chat/hooks/use-delayed-flag.d.ts.map +0 -1
  38. package/src/components/chat/chat-message-loader.tsx +0 -67
  39. package/src/components/chat/hooks/use-delayed-flag.ts +0 -56
@@ -1,12 +1,10 @@
1
1
  "use client"
2
2
 
3
- import { useRef, useState, useEffect, useLayoutEffect, useCallback, useImperativeHandle, forwardRef } from "react"
3
+ import { useRef, useEffect, useLayoutEffect, useImperativeHandle, forwardRef } from "react"
4
4
  import { useStickToBottom } from "use-stick-to-bottom"
5
5
  import { cn } from "../../utils/cn"
6
6
  import { ChatMessageEnhanced } from "./chat-message-enhanced"
7
- import { ChatMessageListLoader } from "./chat-message-loader"
8
- import { useDelayedFlag } from "./hooks/use-delayed-flag"
9
- import { PulseDots } from "../ui/pulse-dots"
7
+ import { ChatMessageListSkeleton } from "./chat-message-skeleton"
10
8
  import type { ChatMessageListProps } from "./types"
11
9
 
12
10
  /*
@@ -40,7 +38,6 @@ import type { ChatMessageListProps } from "./types"
40
38
  * — pass them directly as `ref={scrollRef}` / `ref={contentRef}`.
41
39
  */
42
40
 
43
-
44
41
  const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
45
42
  (
46
43
  {
@@ -67,33 +64,17 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
67
64
  // `resize: 'smooth'` — during streaming, content grows token-by-
68
65
  // token; library uses spring physics to follow the bottom for a
69
66
  // ChatGPT/Claude.ai-like feel (vs jarring instant snaps).
70
- // `initial: 'instant'` — snap to bottom on first mount BEFORE
71
- // browser paint. Without this, a page reload that lands on a long
72
- // history would briefly paint the top of the list (oldest msgs)
73
- // before any post-paint effect could run `scrollTop = bottom`. The
74
- // library writes scrollTop in the commit phase, so the first paint
75
- // is already at the bottom.
76
- //
77
- // `escapedFromLock` is exposed so the stream-tail branch below can
78
- // honor user intent: if they scroll UP during streaming, the lib
79
- // flips it true via the wheel/touch handlers, and we stop forcing
80
- // the viewport to follow. Scrolling back DOWN to near-bottom
81
- // resets it to false and tailing resumes.
82
- const { scrollRef, contentRef, scrollToBottom, escapedFromLock } = useStickToBottom({
67
+ // `initial: false` — DON'T auto-scroll on mount; our dialog-change
68
+ // effect below owns first-paint positioning so re-opening a chat
69
+ // with prior history lands at the bottom without a smooth-scroll
70
+ // animation playing over the cold-paint.
71
+ const { scrollRef, contentRef, scrollToBottom } = useStickToBottom({
83
72
  resize: 'smooth',
84
- initial: 'instant',
73
+ initial: false,
85
74
  })
86
75
 
87
76
  // ---- Prepend / load-more state (NOT owned by the library) --------
88
- // `scrollEl` and `sentinelEl` are STATE, not refs, so the load-more
89
- // effect re-runs when either element mounts/unmounts. This is the
90
- // critical fix for the reload pagination bug: when the loader is
91
- // showing, the scroll container is unmounted (`scrollRef.current ==
92
- // null`); when the loader disappears later, neither `hasNextPage`
93
- // nor `messages.length` changes — but `scrollEl` flips from null to
94
- // the new element, which re-runs the effect.
95
- const [scrollEl, setScrollEl] = useState<HTMLDivElement | null>(null)
96
- const [sentinelEl, setSentinelEl] = useState<HTMLDivElement | null>(null)
77
+ const sentinelRef = useRef<HTMLDivElement>(null)
97
78
  const onLoadMoreRef = useRef(onLoadMore)
98
79
  onLoadMoreRef.current = onLoadMore
99
80
  const isFetchingRef = useRef(isFetchingNextPage)
@@ -118,36 +99,9 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
118
99
  scrollHeight: number
119
100
  }>({ firstMessageId: undefined, firstMessageContent: undefined, scrollHeight: 0 })
120
101
 
121
- // ---- Force-stick: dialog change / first-load / new user message / stream tail --
122
- // `useLayoutEffect` so the scrollTop write lands BEFORE browser
123
- // paint — otherwise a dialog switch with prior history flashes the
124
- // top of the list (oldest msgs) for one frame before snapping down.
125
- //
126
- // Critical: gate on `scrollEl` (state, not ref). When `useDelayedFlag`
127
- // is showing the loader, the scroll container is UNMOUNTED. If we
128
- // ran the snap-logic during the loader phase, `scrollToBottom` would
129
- // no-op AND `prevRenderRef` would record the new dialog/count — so
130
- // when the loader finally hides and the container remounts, we'd
131
- // think "no dialog change" and never snap. By depending on
132
- // `scrollEl`, this effect re-runs the moment the container mounts
133
- // and catches up on any transition that happened during the loader.
134
- //
135
- // Streaming tail (added 2026-05): the library's RO-driven follow
136
- // depends on its internal `state.isAtBottom`, which starts FALSE
137
- // and only flips true on an explicit `scrollToBottom` call
138
- // (without `preserveScrollPosition`). For a flow where the
139
- // consumer wraps `contentRef` with a `<div className="flex-1" />`
140
- // spacer + `minHeight: 100%` (so messages visually bottom-align
141
- // when content is short), the library's initial-snap RO callback
142
- // runs with `preserveScrollPosition: true` → bails on
143
- // `!state.isAtBottom`, never flips it true. Result: streaming
144
- // assistant content grows but viewport stays at scrollTop=0.
145
- // We explicitly tail on every messages update when the user is
146
- // within `STREAM_TAIL_THRESHOLD_PX` of the bottom — independent
147
- // of the library's internal state. This restores 2026-grade
148
- // "follow during stream, release when user scrolls up" UX.
149
- useLayoutEffect(() => {
150
- if (!autoScroll || !scrollEl) return
102
+ // ---- Force-stick: dialog change / first-load / new user message --
103
+ useEffect(() => {
104
+ if (!autoScroll) return
151
105
 
152
106
  const dialogChanged = dialogId !== prevRenderRef.current.dialogId
153
107
  const prevCount = prevRenderRef.current.messageCount
@@ -166,18 +120,6 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
166
120
  return
167
121
  }
168
122
  if (newCount > prevCount) {
169
- // Load-older PREPEND: new content arrived at the START of the
170
- // array (firstMessageId changed). DON'T snap — the prepend-
171
- // anchor effect below preserves the user's reading position.
172
- // Without this guard, `messages.slice(prevCount)` would pick
173
- // up old user messages now sitting at the tail and falsely
174
- // trigger a scrollToBottom.
175
- const isPrepend =
176
- prependRef.current.firstMessageId !== undefined &&
177
- messages[0]?.id !== prependRef.current.firstMessageId
178
-
179
- if (isPrepend) return
180
-
181
123
  // Scan the new tail for any user-role message. Handles the
182
124
  // coalesced-render case where optimistic-user + server-
183
125
  // assistant land in the same diff (last is assistant, but a
@@ -188,34 +130,12 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
188
130
  const hasNewUser = newSlice.some((m) => m.role === 'user')
189
131
  if (hasNewUser) {
190
132
  void scrollToBottom({ animation: 'instant', ignoreEscapes: true })
191
- return
192
133
  }
193
- // Assistant placeholder appended (no user in the slice)
194
- // instant-snap so the bubble lands pinned to the bottom and
195
- // subsequent token streams keep the user oriented. Without
196
- // this snap, the library's RO can fail to follow if its
197
- // internal `isAtBottom` was never set true (see top comment).
198
- void scrollToBottom({ animation: 'instant', ignoreEscapes: true })
199
- return
200
- }
201
-
202
- // Stream tail: same count, last id unchanged → assistant content
203
- // is streaming in. Follow the bottom unless the user has
204
- // explicitly escaped the lock (scrolled up). We can't gate on
205
- // current `distToBottom` because the server may emit ~100KB
206
- // of sources/refs metadata in ONE chunk before the text stream
207
- // begins — by the time this useLayoutEffect runs after that
208
- // render, distToBottom is already in the thousands of pixels
209
- // and any threshold check would mis-fire as "user is far away".
210
- // `escapedFromLock` is the canonical "did the user intentionally
211
- // step out of follow-mode" signal — driven by the lib's wheel
212
- // + touch + scroll handlers, not by post-render geometry.
213
- // `smooth` animation matches the library's `resize: 'smooth'`
214
- // option — spring-physics glide instead of jarring snap.
215
- if (!escapedFromLock) {
216
- void scrollToBottom({ animation: 'smooth' })
134
+ // Assistant-only new messages the library's resize-watch
135
+ // already keeps the bottom locked when the user hasn't
136
+ // escaped. No explicit call needed; spring animation runs.
217
137
  }
218
- }, [autoScroll, messages, dialogId, scrollToBottom, scrollEl, escapedFromLock])
138
+ }, [autoScroll, messages, dialogId, scrollToBottom])
219
139
 
220
140
  // ---- Prepend anchoring (load-older) ------------------------------
221
141
  // The library doesn't preserve user position when content prepends
@@ -224,7 +144,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
224
144
  // user on the same message they were reading. `useLayoutEffect`
225
145
  // so the scrollTop write lands before paint (no visible jump).
226
146
  useLayoutEffect(() => {
227
- const el = scrollEl
147
+ const el = scrollRef.current
228
148
  if (!el) {
229
149
  prependRef.current = { firstMessageId: undefined, firstMessageContent: undefined, scrollHeight: 0 }
230
150
  return
@@ -260,63 +180,46 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
260
180
  if (currentFirstContent !== prependRef.current.firstMessageContent) {
261
181
  prependRef.current.firstMessageContent = currentFirstContent
262
182
  }
263
- }, [messages, scrollEl])
183
+ }, [messages, scrollRef])
264
184
 
265
185
  // ---- Load-more (infinite-scroll UP) ------------------------------
266
- // Distinct from stick-to-bottom. Two mechanisms in parallel:
267
- // 1. IntersectionObserver on the top sentinel — cheap, fires when
268
- // the sentinel actually enters the viewport (± rootMargin).
269
- // 2. Scroll-listener fallback (200px from top) — covers the race
270
- // where IO mounts before the sentinel/scroll container has its
271
- // final geometry on page reload, and the case where the user
272
- // reaches the top on a chat that initially had `hasNextPage`
273
- // undefined (cache cold). Both call `onLoadMoreRef.current` and
274
- // guard with `isFetchingRef` so a double-fire is a single fetch.
275
- // Deps include `messages.length` so the effect re-binds once content
276
- // actually renders (fixes the case where the first commit has
277
- // hasNextPage=true but sentinel ref isn't yet attached).
186
+ // Distinct from stick-to-bottom; uses its own IntersectionObserver
187
+ // on the top sentinel.
278
188
  useEffect(() => {
279
- const scrollContainer = scrollEl
280
- const sentinelElement = sentinelEl
281
- if (!scrollContainer || !hasNextPage) return
282
-
283
- const tryLoad = () => {
284
- if (isFetchingRef.current) return
285
- onLoadMoreRef.current?.()
286
- }
287
-
288
- let observer: IntersectionObserver | undefined
289
- if (sentinelElement) {
290
- observer = new IntersectionObserver(
291
- (entries) => {
292
- const entry = entries[0]
293
- if (entry?.isIntersecting) tryLoad()
294
- },
295
- { root: scrollContainer, rootMargin: '200px', threshold: 0.1 },
296
- )
297
- observer.observe(sentinelElement)
298
- }
299
-
300
- const onScroll = () => {
301
- if (scrollContainer.scrollTop <= 200) tryLoad()
302
- }
303
- scrollContainer.addEventListener('scroll', onScroll, { passive: true })
189
+ const scrollContainer = scrollRef.current
190
+ const sentinelElement = sentinelRef.current
191
+ if (!scrollContainer || !sentinelElement || !hasNextPage) return
304
192
 
305
- return () => {
306
- observer?.disconnect()
307
- scrollContainer.removeEventListener('scroll', onScroll)
308
- }
309
- }, [hasNextPage, scrollEl, sentinelEl, messages.length])
193
+ const observer = new IntersectionObserver(
194
+ (entries) => {
195
+ const entry = entries[0]
196
+ if (!entry) return
197
+ if (entry.isIntersecting && !isFetchingRef.current) {
198
+ onLoadMoreRef.current?.()
199
+ }
200
+ },
201
+ { root: scrollContainer, rootMargin: '200px', threshold: 0.1 },
202
+ )
203
+ observer.observe(sentinelElement)
204
+ return () => observer.disconnect()
205
+ }, [hasNextPage, scrollRef])
310
206
 
311
207
  // Expose the scroll container ref to parents that need it (rare,
312
208
  // but the existing public contract). Library's `scrollRef` is a
313
209
  // MutableRefObject<HTMLElement> so we cast to the public type.
314
210
  useImperativeHandle(ref, () => scrollRef.current as HTMLDivElement, [scrollRef])
315
211
 
316
- // Gate the loader: only show after 200ms (fast loads never flicker),
317
- // and once shown, hold for at least 400ms (no sub-frame flash if data
318
- // arrives a moment later).
319
- const showLoader = useDelayedFlag(isLoading, { delay: 200, minDuration: 400 })
212
+ if (isLoading) {
213
+ return (
214
+ <ChatMessageListSkeleton
215
+ className={className}
216
+ showAvatars={showAvatars}
217
+ assistantType={assistantType}
218
+ contentClassName={contentClassName}
219
+ messageCount={6}
220
+ />
221
+ )
222
+ }
320
223
 
321
224
  // Adapt the library's refs to React's `Ref<HTMLDivElement>` JSX
322
225
  // slot. The library types its refs against `HTMLElement` (broader
@@ -333,31 +236,11 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
333
236
  // cleanup contract). The hub's React types reject that union in a
334
237
  // JSX `Ref<T>` slot. Force `void` here — the library doesn't use
335
238
  // the cleanup return path in any meaningful way for the public hook.
336
- // CRITICAL: memoize these ref callbacks. Inline refs (new function
337
- // each render) cause React to call cleanup(null) then setup(el) on
338
- // EVERY render — which would flip `scrollEl` state null→el every
339
- // render, churning the prepend-anchor `useLayoutEffect` and
340
- // wiping `prependRef.current.scrollHeight` to 0 on each null pass
341
- // (see the `if (!el)` branch in the prepend effect). The net
342
- // effect is that load-older never has a valid prev-height snapshot
343
- // and the user's scroll position isn't preserved after pagination.
344
- // Must live ABOVE the `showLoader` early return — Rules of Hooks.
345
- const setScrollRef = useCallback((el: HTMLDivElement | null): void => {
239
+ const setScrollRef = (el: HTMLDivElement | null): void => {
346
240
  scrollRef(el)
347
- setScrollEl(el)
348
- }, [scrollRef])
349
- const setContentRef = useCallback((el: HTMLDivElement | null): void => {
241
+ }
242
+ const setContentRef = (el: HTMLDivElement | null): void => {
350
243
  contentRef(el)
351
- }, [contentRef])
352
-
353
- if (showLoader) {
354
- return (
355
- <ChatMessageListLoader
356
- className={className}
357
- assistantIcon={assistantIcon}
358
- assistantType={assistantType}
359
- />
360
- )
361
244
  }
362
245
 
363
246
  return (
@@ -379,19 +262,8 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
379
262
  )}
380
263
  style={{ minHeight: '100%' }}
381
264
  >
382
- {/* Infinite scroll sentinel + loader for older pages */}
383
265
  {hasNextPage && (
384
- <div ref={setSentinelEl} className="h-px" />
385
- )}
386
- {isFetchingNextPage && (
387
- <div
388
- className="flex justify-center py-3 animate-in fade-in duration-200"
389
- role="status"
390
- aria-live="polite"
391
- aria-busy="true"
392
- >
393
- <PulseDots size="sm" />
394
- </div>
266
+ <div ref={sentinelRef} className="h-px" />
395
267
  )}
396
268
  <div className="flex-1" />
397
269
  {messages.map((message, index) => (
@@ -0,0 +1,129 @@
1
+ "use client"
2
+
3
+ import { useEffect, useMemo, useState } from "react"
4
+ import { cn } from "../../utils/cn"
5
+
6
+ interface CyclingPhraseProps {
7
+ /** Words to cycle through. Cycle wraps around indefinitely. */
8
+ words: readonly string[]
9
+ /** Optional className applied to the outer span. */
10
+ className?: string
11
+ /** Milliseconds per character during the morph step. */
12
+ charMs?: number
13
+ /** Milliseconds to hold a fully-typed word before starting next morph. */
14
+ holdMs?: number
15
+ }
16
+
17
+ // Idempotent keyframe — multiple component instances share the same
18
+ // CSS rule, no duplication issues.
19
+ const BLINK_KEYFRAMES = `
20
+ @keyframes cyclingCursorBlink {
21
+ 50% { opacity: 0; }
22
+ }
23
+ `
24
+
25
+ /**
26
+ * Terminal-style cycling word: a fixed-size block cursor walks left-
27
+ * to-right through the word, overwriting the previous word's char at
28
+ * each position with the new word's char (or trimming trailing chars
29
+ * if the new word is shorter). On hold the cursor is hidden — the
30
+ * morph is the only time it appears. A static "..." suffix follows
31
+ * the word at all times.
32
+ *
33
+ * Visual sequence (Thinking → Vibing):
34
+ * Thinking... (hold, no cursor)
35
+ * V█hinking... → Vi█inking... → ... → Vibing█...
36
+ * Vibing... (hold, no cursor)
37
+ * ...
38
+ *
39
+ * Width is pinned to the longest word in the list (rendered invisibly
40
+ * underneath) so cycling doesn't shift the dots or surrounding layout.
41
+ */
42
+ export function CyclingPhrase({
43
+ words,
44
+ className,
45
+ charMs = 60,
46
+ holdMs = 4500,
47
+ }: CyclingPhraseProps) {
48
+ const [wordIndex, setWordIndex] = useState(0)
49
+ const [text, setText] = useState('')
50
+ const [cursor, setCursor] = useState(0)
51
+ const [holding, setHolding] = useState(false)
52
+
53
+ // Width-reservation: longest word + "..." sets the outer min-width.
54
+ const placeholder = useMemo(
55
+ () => words.reduce((longest, w) => (w.length > longest.length ? w : longest), ''),
56
+ [words],
57
+ )
58
+
59
+ useEffect(() => {
60
+ if (words.length === 0) return
61
+ const target = words[wordIndex]
62
+ let timeoutId: ReturnType<typeof setTimeout>
63
+
64
+ if (holding) {
65
+ timeoutId = setTimeout(() => {
66
+ setWordIndex((i) => (i + 1) % words.length)
67
+ setCursor(0)
68
+ setHolding(false)
69
+ }, holdMs)
70
+ return () => clearTimeout(timeoutId)
71
+ }
72
+
73
+ const maxLen = Math.max(text.length, target.length)
74
+ if (cursor >= maxLen) {
75
+ // Morph complete — pin to target and enter hold (cursor hides).
76
+ if (text !== target) setText(target)
77
+ setHolding(true)
78
+ return
79
+ }
80
+
81
+ timeoutId = setTimeout(() => {
82
+ setText((prev) => {
83
+ if (cursor < target.length) {
84
+ return target.slice(0, cursor + 1) + prev.slice(cursor + 1)
85
+ }
86
+ return prev.slice(0, cursor)
87
+ })
88
+ setCursor((c) => c + 1)
89
+ }, charMs)
90
+ return () => clearTimeout(timeoutId)
91
+ }, [wordIndex, cursor, text, holding, words, charMs, holdMs])
92
+
93
+ if (words.length === 0) return null
94
+
95
+ const before = text.slice(0, cursor)
96
+ const after = text.slice(cursor)
97
+
98
+ // Block cursor — consistent size regardless of phase. Same style
99
+ // every time it appears (no scaling/jitter). Blinks while visible.
100
+ const cursorBlock = (
101
+ <span
102
+ aria-hidden
103
+ className="inline-block bg-current align-baseline"
104
+ style={{
105
+ width: '0.6em',
106
+ height: '1em',
107
+ verticalAlign: '-0.1em',
108
+ marginLeft: '1px',
109
+ marginRight: '1px',
110
+ animation: 'cyclingCursorBlink 1s steps(1) infinite',
111
+ }}
112
+ />
113
+ )
114
+
115
+ return (
116
+ <span className={cn("relative inline-block whitespace-nowrap", className)}>
117
+ <style dangerouslySetInnerHTML={{ __html: BLINK_KEYFRAMES }} />
118
+ {/* Invisible placeholder — pins width to longest word + "..." */}
119
+ <span aria-hidden className="invisible">{placeholder}...</span>
120
+ {/* Live layer */}
121
+ <span className="absolute inset-0 inline-flex items-baseline" aria-live="polite">
122
+ <span>{before}</span>
123
+ {!holding && cursorBlock}
124
+ <span>{after}</span>
125
+ <span>...</span>
126
+ </span>
127
+ </span>
128
+ )
129
+ }
@@ -2,6 +2,5 @@
2
2
 
3
3
  export * from './use-chunk-catchup'
4
4
  export * from './use-collapsible'
5
- export * from './use-delayed-flag'
6
5
  export * from './use-nats-dialog-subscription'
7
6
  export * from './use-realtime-chunk-processor'
@@ -12,7 +12,6 @@ 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-message-loader'
16
15
  export * from './chat-quick-action'
17
16
  export * from './chat-ticket-item'
18
17
  export * from './chat-ticket-list'