@flamingo-stack/openframe-frontend-core 0.0.223 → 0.0.224

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 (55) hide show
  1. package/dist/{chunk-4WBVMIKQ.cjs → chunk-7JGR67H2.cjs} +24 -24
  2. package/dist/{chunk-4WBVMIKQ.cjs.map → chunk-7JGR67H2.cjs.map} +1 -1
  3. package/dist/{chunk-ENDUMC7Z.cjs → chunk-AENTBOSN.cjs} +118 -70
  4. package/dist/chunk-AENTBOSN.cjs.map +1 -0
  5. package/dist/{chunk-4VHWSAQR.js → chunk-BNYPQCKL.js} +304 -256
  6. package/dist/chunk-BNYPQCKL.js.map +1 -0
  7. package/dist/{chunk-GYYGA24C.js → chunk-LEHEHEVW.js} +2 -2
  8. package/dist/{chunk-TA6Z2AKD.js → chunk-OAOLVEYW.js} +2 -2
  9. package/dist/{chunk-7TKYFZ3L.cjs → chunk-SZCTY3YF.cjs} +7 -7
  10. package/dist/{chunk-7TKYFZ3L.cjs.map → chunk-SZCTY3YF.cjs.map} +1 -1
  11. package/dist/{chunk-ISWWP2BH.js → chunk-V6ME2VMU.js} +2 -2
  12. package/dist/{chunk-ABBU7QOM.cjs → chunk-WIAQRCJ5.cjs} +10 -10
  13. package/dist/{chunk-ABBU7QOM.cjs.map → chunk-WIAQRCJ5.cjs.map} +1 -1
  14. package/dist/{chunk-WOKB6RJ7.js → chunk-YEOM52ZR.js} +2 -2
  15. package/dist/{chunk-KXLTWANQ.cjs → chunk-ZCHTRQBO.cjs} +9 -9
  16. package/dist/{chunk-KXLTWANQ.cjs.map → chunk-ZCHTRQBO.cjs.map} +1 -1
  17. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  18. package/dist/components/chat/hooks/use-chat-card-item.d.ts.map +1 -1
  19. package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -1
  20. package/dist/components/chat/index.cjs +2 -2
  21. package/dist/components/chat/index.js +1 -1
  22. package/dist/components/chat/types/unified-chat-state.types.d.ts +10 -0
  23. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  24. package/dist/components/contact/index.cjs +3 -3
  25. package/dist/components/contact/index.js +2 -2
  26. package/dist/components/features/index.cjs +2 -2
  27. package/dist/components/features/index.js +1 -1
  28. package/dist/components/index.cjs +77 -77
  29. package/dist/components/index.js +4 -4
  30. package/dist/components/navigation/index.cjs +2 -2
  31. package/dist/components/navigation/index.js +1 -1
  32. package/dist/components/onboarding-guides/index.cjs +25 -25
  33. package/dist/components/onboarding-guides/index.js +3 -3
  34. package/dist/components/tickets/index.cjs +64 -64
  35. package/dist/components/tickets/index.js +4 -4
  36. package/dist/components/ui/index.cjs +2 -2
  37. package/dist/components/ui/index.js +1 -1
  38. package/dist/components/ui/simple-markdown-renderer.d.ts +9 -1
  39. package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
  40. package/dist/index.cjs +2 -2
  41. package/dist/index.js +1 -1
  42. package/package.json +1 -1
  43. package/src/components/chat/chat-message-enhanced.tsx +67 -14
  44. package/src/components/chat/embeddable-chat.tsx +51 -13
  45. package/src/components/chat/hooks/use-chat-card-item.ts +17 -1
  46. package/src/components/chat/hooks/use-unified-chat.ts +41 -29
  47. package/src/components/chat/types/unified-chat-state.types.ts +11 -0
  48. package/src/components/navigation/app-layout-drawer.tsx +4 -4
  49. package/src/components/ui/simple-markdown-renderer.tsx +12 -2
  50. package/dist/chunk-4VHWSAQR.js.map +0 -1
  51. package/dist/chunk-ENDUMC7Z.cjs.map +0 -1
  52. /package/dist/{chunk-GYYGA24C.js.map → chunk-LEHEHEVW.js.map} +0 -0
  53. /package/dist/{chunk-TA6Z2AKD.js.map → chunk-OAOLVEYW.js.map} +0 -0
  54. /package/dist/{chunk-ISWWP2BH.js.map → chunk-V6ME2VMU.js.map} +0 -0
  55. /package/dist/{chunk-WOKB6RJ7.js.map → chunk-YEOM52ZR.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import React, { forwardRef, memo, useMemo } from "react"
3
+ import React, { forwardRef, memo, useMemo, useRef } from "react"
4
4
  import { cn } from "../../utils/cn"
5
5
  import { SquareAvatar } from "../ui/square-avatar"
6
6
  import { ToolExecutionDisplay } from "./tool-execution-display"
@@ -57,6 +57,19 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
57
57
 
58
58
  const segments = useMemo(() => normalizeContent(content), [content])
59
59
 
60
+ // Cross-render cache of rendered inline-card nodes, keyed by `type:id`.
61
+ // A fetch-mode card lives inside the assistant message, which re-renders on
62
+ // every stream chunk (and the whole list re-renders when a new message
63
+ // arrives). `renderEntityCard` produces a FRESH element each time, so React
64
+ // re-mounts the card — closing any open menu/popover and re-triggering its
65
+ // fetch. Caching the produced node by key and returning the SAME element
66
+ // reference lets React bail out of re-rendering that subtree, so the card
67
+ // (and its open menu) survives across chunks. Invalidated per key when the
68
+ // backing ref or the render fn identity changes.
69
+ const renderedCardNodeCache = useRef(
70
+ new Map<string, { refMatch: ChatRef | undefined; render: ((ref: ChatRef) => React.ReactNode) | undefined; node: React.ReactNode }>(),
71
+ )
72
+
60
73
  /**
61
74
  * Per-message rendering plan for `[card://type:id]` markers.
62
75
  *
@@ -94,7 +107,13 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
94
107
  | { kind: 'block'; key: string; node: React.ReactNode }
95
108
  const partsBySegment = new Map<number, SegmentPart[]>()
96
109
  if (!render) return { inlineByKey, partsBySegment }
97
- const seenRendered = new Map<string, React.ReactNode>()
110
+ const cache = renderedCardNodeCache.current
111
+ const usedKeys = new Set<string>()
112
+ // Card keys already emitted as a hoisted block (`b-<key>`). The same
113
+ // marker can legitimately appear twice in one message (LLM references
114
+ // the same entity twice); we hoist the FIRST occurrence and skip the
115
+ // duplicates so two siblings never collide on the same React key.
116
+ const emittedBlockKeys = new Set<string>()
98
117
  segments.forEach((segment, segIdx) => {
99
118
  if (segment.type !== 'text') return
100
119
  const text = segment.text
@@ -106,11 +125,15 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
106
125
  const cardType = match[1]
107
126
  const cardId = match[2]
108
127
  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)) {
128
+ usedKeys.add(key)
129
+ // Reuse the cached node when neither the backing ref nor the render
130
+ // fn changed returning the SAME element reference across renders is
131
+ // what stops React from re-mounting the card (and closing its open
132
+ // menu / re-fetching) on every stream chunk. Also dedups the same key
133
+ // emitted twice within one message.
134
+ const refMatch = refs[key]
135
+ let entry = cache.get(key)
136
+ if (!entry || entry.refMatch !== refMatch || entry.render !== render) {
114
137
  // Always invoke render() — even when the metadata map has
115
138
  // no entry for this marker. Fetch-mode card types
116
139
  // (delivery_item, roadmap_item, internal_task, etc.) don't
@@ -129,25 +152,30 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
129
152
  // `ref.title` shows the id rather than `undefined`) and
130
153
  // `url` to null (matches the no-link semantics fetch-mode
131
154
  // cards rely on — they resolve their own URL after fetch).
132
- const refMatch = refs[key]
133
155
  const refForRender: ChatRef = refMatch ?? {
134
156
  type: cardType,
135
157
  id: cardId,
136
158
  title: cardId,
137
159
  url: null,
138
160
  }
139
- rendered = render(refForRender)
140
- seenRendered.set(key, rendered)
161
+ entry = { refMatch, render, node: render(refForRender) }
162
+ cache.set(key, entry)
141
163
  }
164
+ const rendered = entry.node
142
165
  if (React.isValidElement(rendered) && rendered.type === BlockCard) {
143
166
  const props = rendered.props as BlockCardProps
144
167
  const markerEnd = match.index + match[0].length
145
168
  // Text chunk INCLUDING the marker — the inline pill renders
146
169
  // at the marker position via the `<a>` override.
147
170
  parts.push({ kind: 'text', text: text.slice(cursor, markerEnd) })
148
- parts.push({ kind: 'block', key, node: props.children })
171
+ // Hoist the block payload only on the FIRST occurrence of this key
172
+ // — a repeated marker still gets its inline pill (text + override
173
+ // above) but must not push a second `b-<key>` sibling.
174
+ if (!emittedBlockKeys.has(key)) {
175
+ emittedBlockKeys.add(key)
176
+ parts.push({ kind: 'block', key, node: props.children })
177
+ }
149
178
  cursor = markerEnd
150
- const refMatch = refs[key]
151
179
  inlineByKey.set(
152
180
  key,
153
181
  props.inline != null
@@ -155,8 +183,26 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
155
183
  : <span className="text-ods-text-primary font-medium">{refMatch?.title ?? cardId}</span>,
156
184
  )
157
185
  } else if (rendered != null) {
158
- // Inline-only no split; remembered for the override.
159
- inlineByKey.set(key, rendered)
186
+ // Hoist fetch-mode entity cards (roadmap/blog/case-study/release/…)
187
+ // OUT of the markdown as stable-keyed block siblings — exactly like
188
+ // BlockCard. Rendered INSIDE `<SimpleMarkdownRenderer>` they remount
189
+ // on every streaming re-parse (react-markdown rebuilds its subtree
190
+ // token-by-token), which closes any open menu/popover and re-fires
191
+ // the card's fetch. As a sibling keyed by the card key (`b-<key>`)
192
+ // the card survives streaming: appending more text/markers only adds
193
+ // NEW siblings, existing cards keep their React instance.
194
+ //
195
+ // We EXCLUDE the marker from the surrounding text (cursor jumps past
196
+ // it) so no inline pill renders — the full card IS the content.
197
+ // Hoist only the FIRST occurrence; a duplicate marker is dropped
198
+ // entirely (no inline pill for hoisted cards) so we never push a
199
+ // second sibling colliding on the same `b-<key>` React key.
200
+ parts.push({ kind: 'text', text: text.slice(cursor, match.index) })
201
+ if (!emittedBlockKeys.has(key)) {
202
+ emittedBlockKeys.add(key)
203
+ parts.push({ kind: 'block', key, node: rendered })
204
+ }
205
+ cursor = match.index + match[0].length
160
206
  }
161
207
  }
162
208
  // Trailing text after the last block marker (or the entire
@@ -172,6 +218,13 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
172
218
  partsBySegment.set(segIdx, parts)
173
219
  }
174
220
  })
221
+ // Drop cached nodes for markers no longer present so the cache can't
222
+ // grow unbounded as a long message's markers change.
223
+ if (cache.size > usedKeys.size) {
224
+ for (const k of cache.keys()) {
225
+ if (!usedKeys.has(k)) cache.delete(k)
226
+ }
227
+ }
175
228
  return { inlineByKey, partsBySegment }
176
229
  }, [hasMarkerSupport, chatRefs, renderEntityCard, segments])
177
230
 
@@ -934,10 +934,43 @@ function EmbeddableChatInner({
934
934
  [discussRef, displayRef, resolvedBaseRoute, chipBasePlatform, extras],
935
935
  )
936
936
 
937
+ // Stable assistant-icon element. `<ChatMessageList>` forwards this prop to
938
+ // every assistant `<ChatMessageEnhanced>`, whose memo compares it BY
939
+ // REFERENCE. Created inline in JSX it was a fresh element on every render,
940
+ // defeating the memo for all assistant messages (re-rendering completed
941
+ // bubbles — and re-mounting their inline cards — on every realtime chunk).
942
+ const mingoAssistantIcon = useMemo(
943
+ () => <MingoIcon className="h-6 w-6" cornerColor="var(--ods-flamingo-cyan-base)" />,
944
+ [],
945
+ )
946
+
947
+ // Stable per-message timestamps. The memoized `<ChatMessageEnhanced>`
948
+ // compares `timestamp.getTime()`, so we must NOT stamp `new Date()` fresh
949
+ // on each render: a moving clock makes every message's timestamp differ
950
+ // between renders, defeating memoization and forcing the WHOLE list (and
951
+ // every open menu/card inside it) to re-render on every realtime chunk.
952
+ // Prefer the host's real timestamp; otherwise stamp once per id and reuse
953
+ // it (keeps the displayed time stable and lets memoization hold even when
954
+ // the host omits a timestamp).
955
+ const timestampCacheRef = useRef<Map<string, Date>>(new Map())
956
+
937
957
  // Map docMessages → lib's Message type, forwarding chatRefs + scrollAnchor.
938
- const messages: Message[] = useMemo(
939
- () =>
940
- rawMessages.map((m) => ({
958
+ const messages: Message[] = useMemo(() => {
959
+ const cache = timestampCacheRef.current
960
+ const seenIds = new Set<string>()
961
+
962
+ const mapped = rawMessages.map((m) => {
963
+ seenIds.add(m.id)
964
+ let timestamp: Date
965
+ if (m.timestamp != null) {
966
+ timestamp = new Date(m.timestamp)
967
+ } else {
968
+ const cached = cache.get(m.id)
969
+ timestamp = cached ?? new Date()
970
+ if (!cached) cache.set(m.id, timestamp)
971
+ }
972
+
973
+ return {
941
974
  id: m.id,
942
975
  role: m.role as 'user' | 'assistant',
943
976
  // Host-supplied per-message name/avatar win (e.g. the signed-in user's
@@ -949,13 +982,23 @@ function EmbeddableChatInner({
949
982
  // name color as the standalone /mingo page (user → 'admin').
950
983
  ...(m.authorType ? { authorType: m.authorType } : {}),
951
984
  content: m.segments && m.segments.length > 0 ? m.segments : m.content,
952
- timestamp: new Date(),
985
+ timestamp,
953
986
  assistantType: m.role === 'assistant' ? ('mingo' as const) : undefined,
954
987
  ...(m.chatRefs ? { chatRefs: m.chatRefs } : {}),
955
988
  ...(m.scrollAnchor ? { scrollAnchor: m.scrollAnchor } : {}),
956
- })),
957
- [rawMessages],
958
- )
989
+ }
990
+ })
991
+
992
+ // Drop cached fallbacks for messages no longer present so the map can't
993
+ // grow unbounded across a long-lived session.
994
+ if (cache.size > seenIds.size) {
995
+ for (const id of cache.keys()) {
996
+ if (!seenIds.has(id)) cache.delete(id)
997
+ }
998
+ }
999
+
1000
+ return mapped
1001
+ }, [rawMessages])
959
1002
 
960
1003
  const handleSend = useCallback(
961
1004
  (text: string) => {
@@ -1254,12 +1297,7 @@ function EmbeddableChatInner({
1254
1297
  isTyping={chatLoading}
1255
1298
  autoScroll={true}
1256
1299
  assistantType="mingo"
1257
- assistantIcon={
1258
- <MingoIcon
1259
- className="h-6 w-6"
1260
- cornerColor="var(--ods-flamingo-cyan-base)"
1261
- />
1262
- }
1300
+ assistantIcon={mingoAssistantIcon}
1263
1301
  renderEntityCard={renderEntityCard}
1264
1302
  NavLinkAnchor={NavLinkAnchorViaRuntime}
1265
1303
  className="flex-1"
@@ -21,6 +21,7 @@
21
21
 
22
22
  import { useQuery } from '@tanstack/react-query'
23
23
  import { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'
24
+ import { embedAuthedFetch } from '../../../utils/embed-authed-fetch'
24
25
 
25
26
  export interface UseChatCardItemResult<T = unknown> {
26
27
  item: T | undefined
@@ -80,7 +81,13 @@ export function useChatCardItem<T = unknown>(
80
81
  queryKey: ['chat-card-item', type, id],
81
82
  queryFn: async (): Promise<T | null> => {
82
83
  if (!url) return null
83
- const res = await fetch(url)
84
+ // Go through `embedAuthedFetch` (NOT bare `fetch`) so the request
85
+ // rides the same auth path as the chat stream/commands: it consults
86
+ // the host-registered `EmbedAuthAdapter` (cookie `credentials:'include'`
87
+ // cross-origin, dev-ticket Bearer, 401 refresh-and-retry). Bare `fetch`
88
+ // sent no credentials, so list endpoints behind the gateway returned
89
+ // 401 and the card rendered blank.
90
+ const res = await embedAuthedFetch(url)
84
91
  if (!res.ok) return null
85
92
  const data = await res.json()
86
93
  const items = extractItems(data)
@@ -93,6 +100,15 @@ export function useChatCardItem<T = unknown>(
93
100
  // user toggles back to the tab is wasteful (the row's id-keyed
94
101
  // payload is effectively immutable).
95
102
  refetchOnWindowFocus: false,
103
+ // The id-keyed payload is effectively immutable, and a fetch-mode card
104
+ // inside a STREAMING assistant message re-mounts on every chunk as the
105
+ // surrounding markdown re-parses. Without a staleTime that meant a fresh
106
+ // network fetch + loading-skeleton flash on every single chunk. Treat the
107
+ // row as fresh for 5 min (matches the public pages) and keep it cached
108
+ // long after unmount so remounts resolve synchronously from cache — no
109
+ // refetch, no skeleton flash.
110
+ staleTime: 5 * 60 * 1000,
111
+ gcTime: 30 * 60 * 1000,
96
112
  })
97
113
  return {
98
114
  item: (query.data ?? undefined) as T | undefined,
@@ -134,27 +134,39 @@ export function useUnifiedChat(
134
134
  ? sseState
135
135
  : (mingoStateOverride ?? natsState)
136
136
 
137
+ // Live ref to the active state. The injected `mingoState` (and the SSE/NATS
138
+ // adapters) hand back a NEW state object on EVERY streaming chunk
139
+ // (`messages`/`streamingPhase`/`tokenUsage` change), so binding the forwards
140
+ // below to `[activeState]` would recreate them each chunk. That churn
141
+ // cascades into consumers' memo deps — most visibly `renderEntityCard` in
142
+ // `embeddable-chat`, whose new identity defeats `ChatMessageEnhanced`'s memo
143
+ // and re-renders (re-mounts inline cards on) every message each chunk.
144
+ // Reading through the ref keeps the forwards referentially STABLE while
145
+ // still calling the latest active state.
146
+ const activeStateRef = useRef(activeState)
147
+ activeStateRef.current = activeState
148
+
137
149
  // Re-wrap so the returned identity is stable for the active state.
138
150
  // Consumers shouldn't see the inactive adapter's state at all.
139
- const stopMessage = useCallback(() => activeState.stopMessage(), [activeState])
151
+ const stopMessage = useCallback(() => activeStateRef.current.stopMessage(), [])
140
152
  const clearMessages = useCallback(
141
- () => activeState.clearMessages(),
142
- [activeState],
153
+ () => activeStateRef.current.clearMessages(),
154
+ [],
143
155
  )
144
156
  const sendMessage = useCallback(
145
157
  (text: string, opts?: Parameters<UnifiedChatState['sendMessage']>[1]) =>
146
- activeState.sendMessage(text, opts),
147
- [activeState],
158
+ activeStateRef.current.sendMessage(text, opts),
159
+ [],
148
160
  )
149
161
  const discussRef = useCallback(
150
162
  (ref: Parameters<UnifiedChatState['discussRef']>[0]) =>
151
- activeState.discussRef(ref),
152
- [activeState],
163
+ activeStateRef.current.discussRef(ref),
164
+ [],
153
165
  )
154
166
  const displayRef = useCallback(
155
167
  (ref: Parameters<UnifiedChatState['displayRef']>[0]) =>
156
- activeState.displayRef(ref),
157
- [activeState],
168
+ activeStateRef.current.displayRef(ref),
169
+ [],
158
170
  )
159
171
 
160
172
  // Dialog-management forwards — one thin wrapper per action so the
@@ -162,45 +174,45 @@ export function useUnifiedChat(
162
174
  // identity does. We don't recreate per-call to avoid spurious child
163
175
  // re-renders downstream.
164
176
  const selectDialog = useCallback(
165
- (id: string | null) => activeState.selectDialog(id),
166
- [activeState],
177
+ (id: string | null) => activeStateRef.current.selectDialog(id),
178
+ [],
167
179
  )
168
180
  const startNewDialog = useCallback(
169
- () => activeState.startNewDialog(),
170
- [activeState],
181
+ () => activeStateRef.current.startNewDialog(),
182
+ [],
171
183
  )
172
184
  const deleteDialog = useCallback(
173
- (id: string) => activeState.deleteDialog(id),
174
- [activeState],
185
+ (id: string) => activeStateRef.current.deleteDialog(id),
186
+ [],
175
187
  )
176
188
  const renameDialog = useCallback(
177
- (id: string, title: string) => activeState.renameDialog(id, title),
178
- [activeState],
189
+ (id: string, title: string) => activeStateRef.current.renameDialog(id, title),
190
+ [],
179
191
  )
180
192
  const archiveDialog = useCallback(
181
- (id: string) => activeState.archiveDialog(id),
182
- [activeState],
193
+ (id: string) => activeStateRef.current.archiveDialog(id),
194
+ [],
183
195
  )
184
196
  const loadMoreDialogs = useCallback(
185
- () => activeState.loadMoreDialogs(),
186
- [activeState],
197
+ () => activeStateRef.current.loadMoreDialogs(),
198
+ [],
187
199
  )
188
200
  const reloadDialogs = useCallback(
189
- () => activeState.reloadDialogs(),
190
- [activeState],
201
+ () => activeStateRef.current.reloadDialogs(),
202
+ [],
191
203
  )
192
204
  const loadMoreMessages = useCallback(
193
- () => activeState.loadMoreMessages(),
194
- [activeState],
205
+ () => activeStateRef.current.loadMoreMessages(),
206
+ [],
195
207
  )
196
208
  const approveRequest = useCallback(
197
- (requestId: string) => activeState.approveRequest(requestId),
198
- [activeState],
209
+ (requestId: string) => activeStateRef.current.approveRequest(requestId),
210
+ [],
199
211
  )
200
212
  const rejectRequest = useCallback(
201
213
  (requestId: string, reason?: string) =>
202
- activeState.rejectRequest(requestId, reason),
203
- [activeState],
214
+ activeStateRef.current.rejectRequest(requestId, reason),
215
+ [],
204
216
  )
205
217
 
206
218
  return useMemo<UnifiedChatState>(
@@ -143,6 +143,17 @@ export interface UnifiedChatMessage {
143
143
  content: string
144
144
  segments?: MessageSegment[]
145
145
 
146
+ /**
147
+ * Optional host-supplied message timestamp (creation/send time). Rendered
148
+ * as the message-row time and, critically, fed into the memoized message's
149
+ * equality check. Hosts SHOULD pass a stable value (the message's real
150
+ * `createdAt`) — when omitted the panel stamps a stable per-id fallback so
151
+ * memoization still holds. Never re-derive this as "now" per render: a
152
+ * moving clock defeats memoization and re-renders every message (collapsing
153
+ * open menus/cards) on each realtime chunk.
154
+ */
155
+ timestamp?: Date | string | number
156
+
146
157
  /** Guide/SSE-only: document citations. Undefined in Mingo mode. */
147
158
  sources?: ChatSource[]
148
159
 
@@ -339,12 +339,12 @@ function AppLayoutDrawerResizeHandle({
339
339
 
340
340
  const trackPosition =
341
341
  side === "right"
342
- ? "right-full top-4 bottom-4 w-12 items-center justify-end pr-1"
342
+ ? "right-full top-4 bottom-4 w-6 items-center justify-end pr-1"
343
343
  : side === "left"
344
- ? "left-full top-4 bottom-4 w-12 items-center justify-start pl-1"
344
+ ? "left-full top-4 bottom-4 w-6 items-center justify-start pl-1"
345
345
  : side === "bottom"
346
- ? "bottom-full left-4 right-4 h-12 justify-center items-end pb-1"
347
- : "top-full left-4 right-4 h-12 justify-center items-start pt-1"
346
+ ? "bottom-full left-4 right-4 h-6 justify-center items-end pb-1"
347
+ : "top-full left-4 right-4 h-6 justify-center items-start pt-1"
348
348
 
349
349
  const cursorClass = isHorizontal ? "cursor-col-resize" : "cursor-row-resize"
350
350
  const gripClass = isHorizontal ? "h-10 w-1" : "w-10 h-1"
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react';
3
+ import React, { useEffect, useState, useRef, useMemo, useCallback, memo } from 'react';
4
4
  import ReactMarkdown, { type Components, defaultUrlTransform } from 'react-markdown';
5
5
  import type { PluggableList } from 'unified';
6
6
  import remarkGfm from 'remark-gfm';
@@ -583,7 +583,7 @@ export interface SimpleMarkdownRendererProps {
583
583
  // ---------------------------------------------------------------------------
584
584
  // Component
585
585
  // ---------------------------------------------------------------------------
586
- export const SimpleMarkdownRenderer: React.FC<SimpleMarkdownRendererProps> = ({
586
+ const SimpleMarkdownRendererImpl: React.FC<SimpleMarkdownRendererProps> = ({
587
587
  content,
588
588
  className = "",
589
589
  sectionIds,
@@ -947,3 +947,13 @@ export const SimpleMarkdownRenderer: React.FC<SimpleMarkdownRendererProps> = ({
947
947
  </div>
948
948
  );
949
949
  };
950
+
951
+ /**
952
+ * Memoized so a parent re-render with UNCHANGED props (same `content` string,
953
+ * same memoized `componentOverrides`/plugins) does NOT re-parse the markdown.
954
+ * Re-parsing rebuilds the entire react-markdown subtree, which RE-MOUNTS any
955
+ * embedded inline entity cards — closing their open menus and re-triggering
956
+ * their fetch on every chat re-render (streaming chunk AND scroll). With
957
+ * stable props the renderer bails, so completed messages' cards stay mounted.
958
+ */
959
+ export const SimpleMarkdownRenderer = memo(SimpleMarkdownRendererImpl);