@flamingo-stack/openframe-frontend-core 0.0.488 → 0.0.489-snapshot.20260728173516

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 (153) hide show
  1. package/dist/{chunk-GQX6LFFA.cjs → chunk-2MKUHJ3O.cjs} +42 -42
  2. package/dist/{chunk-GQX6LFFA.cjs.map → chunk-2MKUHJ3O.cjs.map} +1 -1
  3. package/dist/{chunk-DX6AOHNU.cjs → chunk-3IEVA5AO.cjs} +77 -77
  4. package/dist/{chunk-DX6AOHNU.cjs.map → chunk-3IEVA5AO.cjs.map} +1 -1
  5. package/dist/{chunk-PNXNWGBY.js → chunk-6CFFISUC.js} +2 -2
  6. package/dist/{chunk-F6BYUO5U.js → chunk-6CJ2K7BD.js} +3 -3
  7. package/dist/{chunk-KFQXO7QB.js → chunk-6GT557KN.js} +1 -1
  8. package/dist/chunk-6GT557KN.js.map +1 -0
  9. package/dist/{chunk-AJNDDXKC.js → chunk-6QK3ONDD.js} +3 -3
  10. package/dist/{chunk-H7JX2BG7.cjs → chunk-7I42V2OD.cjs} +75 -75
  11. package/dist/{chunk-H7JX2BG7.cjs.map → chunk-7I42V2OD.cjs.map} +1 -1
  12. package/dist/{chunk-AHDJMEEN.js → chunk-AIRNG27G.js} +7 -7
  13. package/dist/{chunk-RZSA6MZL.cjs → chunk-AW63ET2A.cjs} +15 -15
  14. package/dist/{chunk-RZSA6MZL.cjs.map → chunk-AW63ET2A.cjs.map} +1 -1
  15. package/dist/{chunk-A7IPON2V.cjs → chunk-BIKOJ4FH.cjs} +5 -5
  16. package/dist/{chunk-A7IPON2V.cjs.map → chunk-BIKOJ4FH.cjs.map} +1 -1
  17. package/dist/{chunk-S52LJNMU.js → chunk-BZMOEWWS.js} +7 -7
  18. package/dist/{chunk-SIKCXKGB.cjs → chunk-D5LQMRPU.cjs} +7 -7
  19. package/dist/{chunk-SIKCXKGB.cjs.map → chunk-D5LQMRPU.cjs.map} +1 -1
  20. package/dist/{chunk-HQYJINFV.js → chunk-EGXCWZYP.js} +4 -4
  21. package/dist/{chunk-EUYIFALO.cjs → chunk-EJQUYZXF.cjs} +14 -14
  22. package/dist/{chunk-EUYIFALO.cjs.map → chunk-EJQUYZXF.cjs.map} +1 -1
  23. package/dist/{chunk-V3CNNMY3.cjs → chunk-FY5XAW3H.cjs} +18 -18
  24. package/dist/{chunk-V3CNNMY3.cjs.map → chunk-FY5XAW3H.cjs.map} +1 -1
  25. package/dist/{chunk-FOQV6RCO.cjs → chunk-GMPYR3MX.cjs} +29 -29
  26. package/dist/{chunk-FOQV6RCO.cjs.map → chunk-GMPYR3MX.cjs.map} +1 -1
  27. package/dist/{chunk-EYEIVYYF.cjs → chunk-GUTYUMGV.cjs} +7 -7
  28. package/dist/{chunk-EYEIVYYF.cjs.map → chunk-GUTYUMGV.cjs.map} +1 -1
  29. package/dist/{chunk-FZOBQG6M.cjs → chunk-IEZ3YBAP.cjs} +35 -35
  30. package/dist/{chunk-FZOBQG6M.cjs.map → chunk-IEZ3YBAP.cjs.map} +1 -1
  31. package/dist/{chunk-PECGZWQR.cjs → chunk-KP5KRRD6.cjs} +3 -3
  32. package/dist/{chunk-PECGZWQR.cjs.map → chunk-KP5KRRD6.cjs.map} +1 -1
  33. package/dist/{chunk-2B6773QM.cjs → chunk-LILQJ3UZ.cjs} +1 -1
  34. package/dist/chunk-LILQJ3UZ.cjs.map +1 -0
  35. package/dist/{chunk-SCCTJ2UI.js → chunk-MPYDX5WX.js} +4 -4
  36. package/dist/{chunk-YJSKQ2ZV.js → chunk-NGEFYPY7.js} +3 -3
  37. package/dist/{chunk-77DFPD35.js → chunk-OQIMH2WQ.js} +5515 -5377
  38. package/dist/chunk-OQIMH2WQ.js.map +1 -0
  39. package/dist/{chunk-KLYHZRAD.js → chunk-PB3BTZDU.js} +2 -2
  40. package/dist/{chunk-CWQACI4Z.js → chunk-RW2LPL72.js} +2 -2
  41. package/dist/{chunk-YEEHB4T6.cjs → chunk-UZZT65PQ.cjs} +40 -40
  42. package/dist/{chunk-YEEHB4T6.cjs.map → chunk-UZZT65PQ.cjs.map} +1 -1
  43. package/dist/{chunk-LLWBPPZ4.js → chunk-VDZO6IDR.js} +6 -6
  44. package/dist/{chunk-3BFX6HJM.js → chunk-VL2YMDPH.js} +6 -6
  45. package/dist/{chunk-5RXKOGH6.cjs → chunk-VUXNXWOQ.cjs} +496 -358
  46. package/dist/chunk-VUXNXWOQ.cjs.map +1 -0
  47. package/dist/{chunk-PXF5J24T.js → chunk-WLM3F3WU.js} +2 -2
  48. package/dist/components/case-studies/index.cjs +10 -10
  49. package/dist/components/case-studies/index.js +4 -4
  50. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  51. package/dist/components/chat/guide-display.d.ts +14 -0
  52. package/dist/components/chat/guide-display.d.ts.map +1 -0
  53. package/dist/components/chat/hooks/use-chat-history-hydration.d.ts +47 -0
  54. package/dist/components/chat/hooks/use-chat-history-hydration.d.ts.map +1 -0
  55. package/dist/components/chat/hooks/use-chat.d.ts +1 -0
  56. package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
  57. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  58. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
  59. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +4 -0
  60. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  61. package/dist/components/chat/index.cjs +6 -4
  62. package/dist/components/chat/index.cjs.map +1 -1
  63. package/dist/components/chat/index.d.ts +1 -0
  64. package/dist/components/chat/index.d.ts.map +1 -1
  65. package/dist/components/chat/index.js +5 -3
  66. package/dist/components/chat/types/component.types.d.ts +6 -0
  67. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  68. package/dist/components/chat/types/message.types.d.ts +15 -2
  69. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  70. package/dist/components/chat/types/processing.types.d.ts +3 -0
  71. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  72. package/dist/components/chat/types/unified-chat-state.types.d.ts +5 -0
  73. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  74. package/dist/components/chat/utils/chat-conversation-storage.d.ts +34 -0
  75. package/dist/components/chat/utils/chat-conversation-storage.d.ts.map +1 -0
  76. package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -1
  77. package/dist/components/chat/utils/message-segment-accumulator.d.ts +5 -0
  78. package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
  79. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  80. package/dist/components/contact/index.cjs +5 -5
  81. package/dist/components/contact/index.js +4 -4
  82. package/dist/components/docs/index.cjs +7 -7
  83. package/dist/components/docs/index.js +6 -6
  84. package/dist/components/embeds/index.cjs +5 -5
  85. package/dist/components/embeds/index.js +4 -4
  86. package/dist/components/faq/index.cjs +6 -6
  87. package/dist/components/faq/index.js +5 -5
  88. package/dist/components/features/index.cjs +4 -4
  89. package/dist/components/features/index.js +3 -3
  90. package/dist/components/help-center-pages/index.cjs +23 -23
  91. package/dist/components/help-center-pages/index.js +14 -14
  92. package/dist/components/index.cjs +142 -140
  93. package/dist/components/index.cjs.map +1 -1
  94. package/dist/components/index.js +14 -12
  95. package/dist/components/index.js.map +1 -1
  96. package/dist/components/navigation/index.cjs +4 -4
  97. package/dist/components/navigation/index.js +3 -3
  98. package/dist/components/onboarding-guides/index.cjs +8 -8
  99. package/dist/components/onboarding-guides/index.js +7 -7
  100. package/dist/components/related-content/index.cjs +6 -6
  101. package/dist/components/related-content/index.js +5 -5
  102. package/dist/components/tickets/index.cjs +7 -7
  103. package/dist/components/tickets/index.js +6 -6
  104. package/dist/components/ui/index.cjs +6 -4
  105. package/dist/components/ui/index.cjs.map +1 -1
  106. package/dist/components/ui/index.js +5 -3
  107. package/dist/contexts/chat-runtime-context.d.ts +8 -0
  108. package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
  109. package/dist/contexts/index.cjs +2 -2
  110. package/dist/contexts/index.js +1 -1
  111. package/dist/hooks/index.cjs +3 -3
  112. package/dist/hooks/index.js +2 -2
  113. package/dist/index.cjs +6 -4
  114. package/dist/index.cjs.map +1 -1
  115. package/dist/index.js +5 -3
  116. package/dist/utils/index.cjs.map +1 -1
  117. package/dist/utils/index.js.map +1 -1
  118. package/package.json +1 -1
  119. package/src/components/chat/chat-message-enhanced.tsx +63 -52
  120. package/src/components/chat/guide-display.tsx +44 -0
  121. package/src/components/chat/hooks/use-chat-history-hydration.ts +142 -0
  122. package/src/components/chat/hooks/use-chat.ts +12 -0
  123. package/src/components/chat/hooks/use-nats-chat-adapter.ts +2 -0
  124. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +10 -0
  125. package/src/components/chat/hooks/use-sse-chat-adapter.ts +109 -162
  126. package/src/components/chat/index.ts +1 -0
  127. package/src/components/chat/types/component.types.ts +9 -0
  128. package/src/components/chat/types/message.types.ts +17 -1
  129. package/src/components/chat/types/processing.types.ts +1 -0
  130. package/src/components/chat/types/unified-chat-state.types.ts +6 -0
  131. package/src/components/chat/utils/chat-conversation-storage.ts +83 -0
  132. package/src/components/chat/utils/chunk-parser.ts +6 -0
  133. package/src/components/chat/utils/message-segment-accumulator.ts +19 -0
  134. package/src/components/chat/utils/process-historical-messages.ts +6 -0
  135. package/src/contexts/chat-runtime-context.tsx +8 -0
  136. package/src/stories/ChatMessageEnhanced.stories.tsx +48 -1
  137. package/dist/chunk-2B6773QM.cjs.map +0 -1
  138. package/dist/chunk-5RXKOGH6.cjs.map +0 -1
  139. package/dist/chunk-77DFPD35.js.map +0 -1
  140. package/dist/chunk-KFQXO7QB.js.map +0 -1
  141. /package/dist/{chunk-PNXNWGBY.js.map → chunk-6CFFISUC.js.map} +0 -0
  142. /package/dist/{chunk-F6BYUO5U.js.map → chunk-6CJ2K7BD.js.map} +0 -0
  143. /package/dist/{chunk-AJNDDXKC.js.map → chunk-6QK3ONDD.js.map} +0 -0
  144. /package/dist/{chunk-AHDJMEEN.js.map → chunk-AIRNG27G.js.map} +0 -0
  145. /package/dist/{chunk-S52LJNMU.js.map → chunk-BZMOEWWS.js.map} +0 -0
  146. /package/dist/{chunk-HQYJINFV.js.map → chunk-EGXCWZYP.js.map} +0 -0
  147. /package/dist/{chunk-SCCTJ2UI.js.map → chunk-MPYDX5WX.js.map} +0 -0
  148. /package/dist/{chunk-YJSKQ2ZV.js.map → chunk-NGEFYPY7.js.map} +0 -0
  149. /package/dist/{chunk-KLYHZRAD.js.map → chunk-PB3BTZDU.js.map} +0 -0
  150. /package/dist/{chunk-CWQACI4Z.js.map → chunk-RW2LPL72.js.map} +0 -0
  151. /package/dist/{chunk-LLWBPPZ4.js.map → chunk-VDZO6IDR.js.map} +0 -0
  152. /package/dist/{chunk-3BFX6HJM.js.map → chunk-VL2YMDPH.js.map} +0 -0
  153. /package/dist/{chunk-PXF5J24T.js.map → chunk-WLM3F3WU.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.488",
3
+ "version": "0.0.489-snapshot.20260728173516",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -11,6 +11,7 @@ import { ApprovalBatchMessage } from "./approval-batch-message"
11
11
  import { ErrorMessageDisplay } from "./error-message-display"
12
12
  import { ContextCompactionDisplay } from "./context-compaction-display"
13
13
  import { ThinkingDisplay } from "./thinking-display"
14
+ import { GuideDisplay } from "./guide-display"
14
15
  import { SimpleMarkdownRenderer } from "../ui/simple-markdown-renderer"
15
16
  import type { ChatRef } from "./chat-ref.types"
16
17
  import { remarkCardLinks } from "./remark-card-links"
@@ -172,7 +173,12 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
172
173
  // duplicates so two siblings never collide on the same React key.
173
174
  const emittedBlockKeys = new Set<string>()
174
175
  segments.forEach((segment, segIdx) => {
175
- if (segment.type !== 'text') return
176
+ // Both markdown-bearing segment types take part: a `guide` body is
177
+ // authored by the same LLM and carries the same `[card://]` markers,
178
+ // so skipping it here left its markers with no `inlineByKey` entry and
179
+ // no hoisted card — the `<a card://>` override silently degraded them
180
+ // to a bare title / id.
181
+ if (segment.type !== 'text' && segment.type !== 'guide') return
176
182
  const text = segment.text
177
183
  const parts: SegmentPart[] = []
178
184
  let cursor = 0
@@ -384,6 +390,52 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
384
390
  }
385
391
  }, [hasMarkerSupport, hasMentionSupport, renderMention, chatRefs, renderingPlan, NavLinkAnchor])
386
392
 
393
+ /**
394
+ * Body of a markdown-bearing segment (`text` / `guide`).
395
+ *
396
+ * No block markers in this segment → a single SimpleMarkdownRenderer call
397
+ * (the vast majority of messages). Otherwise the text is split at each
398
+ * marker and block payloads are interleaved: each text chunk includes its
399
+ * trailing marker so the inline pill renders at the right position via the
400
+ * `<a>` override, and block payloads land AS SIBLINGS between text chunks —
401
+ * HTML-valid (block DOM never nests inside `<p>`) AND positionally correct
402
+ * (the block appears where the marker is in the flow, not at the segment's
403
+ * end). Stable React keys come from the card key (block) and chunk position
404
+ * (text); streaming token-by-token reuses the same React instances so
405
+ * `<Video>` doesn't remount mid-play.
406
+ */
407
+ const renderSegmentBody = (segIndex: number, text: string) => {
408
+ const parts = renderingPlan?.partsBySegment.get(segIndex)
409
+ if (!parts || parts.length === 0) {
410
+ return (
411
+ <SimpleMarkdownRenderer
412
+ content={text}
413
+ textSize="compact"
414
+ additionalRemarkPlugins={cardRemarkPlugins}
415
+ componentOverrides={cardComponentOverrides}
416
+ />
417
+ )
418
+ }
419
+ return parts.map((part, pIdx) => {
420
+ if (part.kind === 'text') {
421
+ return (
422
+ <SimpleMarkdownRenderer
423
+ key={`t-${pIdx}`}
424
+ content={part.text}
425
+ textSize="compact"
426
+ additionalRemarkPlugins={cardRemarkPlugins}
427
+ componentOverrides={cardComponentOverrides}
428
+ />
429
+ )
430
+ }
431
+ return (
432
+ <div key={`b-${part.key}`} className="my-3">
433
+ {part.node}
434
+ </div>
435
+ )
436
+ })
437
+ }
438
+
387
439
  const getAvatarProps = () => {
388
440
  const displayName = name || (isUser ? "User" : assistantType === 'mingo' ? "Mingo" : "Fae")
389
441
  const isMingo = assistantType === 'mingo'
@@ -488,60 +540,19 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
488
540
  {(!isSystem || segments.length > 0) && <div className="flex flex-col gap-2">
489
541
  {segments.map((segment, index) => {
490
542
  if (segment.type === 'text') {
491
- const parts = renderingPlan?.partsBySegment.get(index)
492
- const wrapperClass = cn(
493
- "min-w-0 w-full break-words text-h4",
494
- isError ? "text-ods-error" : "text-ods-text-primary",
495
- )
496
- // No block markers in this segment → single
497
- // SimpleMarkdownRenderer call (existing behaviour
498
- // preserved for the vast majority of messages).
499
- if (!parts || parts.length === 0) {
500
- return (
501
- <div key={index} className={wrapperClass}>
502
- <SimpleMarkdownRenderer
503
- content={segment.text}
504
- textSize="compact"
505
- additionalRemarkPlugins={cardRemarkPlugins}
506
- componentOverrides={cardComponentOverrides}
507
- />
508
- </div>
509
- )
510
- }
511
- // Block markers present → split text at each marker
512
- // and interleave block payloads. Each text chunk
513
- // includes its trailing marker so the inline pill
514
- // renders at the right position via the `<a>`
515
- // override. Block payloads land AS SIBLINGS between
516
- // text chunks — HTML-valid (block DOM never nests
517
- // inside `<p>`) AND positionally correct (block
518
- // appears where the marker is in the flow, not at
519
- // the segment's end). Stable React keys come from
520
- // the card key (block) and chunk position (text);
521
- // streaming token-by-token reuses the same React
522
- // instances so `<Video>` doesn't remount mid-play.
523
543
  return (
524
- <div key={index} className={wrapperClass}>
525
- {parts.map((part, pIdx) => {
526
- if (part.kind === 'text') {
527
- return (
528
- <SimpleMarkdownRenderer
529
- key={`t-${pIdx}`}
530
- content={part.text}
531
- textSize="compact"
532
- additionalRemarkPlugins={cardRemarkPlugins}
533
- componentOverrides={cardComponentOverrides}
534
- />
535
- )
536
- }
537
- return (
538
- <div key={`b-${part.key}`} className="my-3">
539
- {part.node}
540
- </div>
541
- )
542
- })}
544
+ <div
545
+ key={index}
546
+ className={cn(
547
+ "min-w-0 w-full break-words text-h4",
548
+ isError ? "text-ods-error" : "text-ods-text-primary",
549
+ )}
550
+ >
551
+ {renderSegmentBody(index, segment.text)}
543
552
  </div>
544
553
  )
554
+ } else if (segment.type === 'guide') {
555
+ return <GuideDisplay key={index}>{renderSegmentBody(index, segment.text)}</GuideDisplay>
545
556
  } else if (segment.type === 'tool_execution') {
546
557
  return (
547
558
  <ToolExecutionDisplay
@@ -0,0 +1,44 @@
1
+ "use client"
2
+
3
+ import { forwardRef } from "react"
4
+
5
+ import { cn } from "../../utils/cn"
6
+ import { CompassIcon } from "../icons-v2-generated"
7
+ import type { GuideDisplayProps } from "./types"
8
+
9
+ const GUIDE_LABEL = "OpenFrame Guide"
10
+
11
+ /**
12
+ * `GUIDE` segment — the assistant's how-to/documentation answer, framed as a
13
+ * titled card instead of a bare paragraph.
14
+ *
15
+ * Chrome only: the caller renders the markdown body and passes it as children,
16
+ * so a guide goes through the SAME body pipeline as a text segment
17
+ * (`[card://]` splitting + hoisted entity cards + mention chips). Rendering the
18
+ * raw markdown here instead would bypass that plan and degrade every card
19
+ * marker inside a guide to a bare title.
20
+ */
21
+ const GuideDisplay = forwardRef<HTMLDivElement, GuideDisplayProps>(
22
+ ({ className, children, ...props }, ref) => {
23
+ return (
24
+ <div
25
+ ref={ref}
26
+ className={cn(
27
+ "flex flex-col gap-[var(--spacing-system-m)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-m)]",
28
+ className
29
+ )}
30
+ {...props}
31
+ >
32
+ <div className="flex items-center gap-[var(--spacing-system-xs)]">
33
+ <CompassIcon size={16} className="shrink-0 text-ods-open-yellow" />
34
+ <span className="text-h5 text-ods-open-yellow">{GUIDE_LABEL}</span>
35
+ </div>
36
+ <div className="min-w-0 w-full break-words text-h4 text-ods-text-primary">{children}</div>
37
+ </div>
38
+ )
39
+ }
40
+ )
41
+
42
+ GuideDisplay.displayName = "GuideDisplay"
43
+
44
+ export { GuideDisplay }
@@ -0,0 +1,142 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * useChatHistoryHydration — mount-time rebuild of the chat message list from
5
+ * the SERVER-side conversation store (the `chat_conversations` /
6
+ * `chat_messages` SSOT behind `GET <chatStreamUrl>/history`).
7
+ *
8
+ * The client persists ONLY the server-issued conversation id (see
9
+ * `chat-conversation-storage.ts`); this hook turns that id back into rendered
10
+ * history: message bubbles, inline entity-card refs (per send index, same
11
+ * `sendIdx` scheme as live turns), and the send counter.
12
+ *
13
+ * Failure semantics: a fetch/parse failure simply leaves the UI empty — the
14
+ * SERVER still resolves full history for the next turn (it re-reads
15
+ * `chat_messages` by conversation id on every send), so conversation context
16
+ * is never lost when hydration misses.
17
+ */
18
+
19
+ import { useEffect, useRef, useState, type MutableRefObject } from 'react'
20
+ import type { ChatRef } from '../chat-ref.types'
21
+ import type { Message } from './use-chat'
22
+ import { chatAuthedFetch } from '../utils/chat-authed-fetch'
23
+ import { AUTO_CONTINUATION_DIRECTIVE_PREFIX } from '../utils/auto-continuation-directive'
24
+
25
+ export interface UseChatHistoryHydrationArgs {
26
+ /** Mirrors the adapter's `active` gate — an idle (Mingo-mode) mount never fetches. */
27
+ active: boolean
28
+ /** Chat source (= platform); part of the once-per-conversation guard key. */
29
+ source: string
30
+ /** Resolved history endpoint (`<chatStreamUrl>/history` by default). */
31
+ historyUrl: string
32
+ /** The server-issued conversation id (null = nothing to hydrate). */
33
+ conversationIdRef: MutableRefObject<string | null>
34
+ /** Per-send inline entity-card refs — repopulated from `chat_refs`. */
35
+ refsMapRef: MutableRefObject<Map<number, Record<string, ChatRef>>>
36
+ /** User-send counter — set to the hydrated user-turn count so the next
37
+ * live send lands on the following `sendIdx`. */
38
+ sendCountRef: MutableRefObject<number>
39
+ /** `useChat`'s injection primitive (replace-or-prepend). */
40
+ hydrateMessages: (history: Message[]) => void
41
+ /** Invalidates the adapter's `latestMeta` memo after refs repopulate. */
42
+ bumpMetaTick: () => void
43
+ }
44
+
45
+ export interface UseChatHistoryHydrationResult {
46
+ /** True while the rebuild request is in flight. */
47
+ isHydratingHistory: boolean
48
+ /** Once-per-`source:conversationId` guard. The adapter's `clearMessages`
49
+ * resets it to null alongside the stored id (a fresh conversation has no
50
+ * server history to fetch). */
51
+ hydratedKeyRef: MutableRefObject<string | null>
52
+ }
53
+
54
+ export function useChatHistoryHydration({
55
+ active,
56
+ source,
57
+ historyUrl,
58
+ conversationIdRef,
59
+ refsMapRef,
60
+ sendCountRef,
61
+ hydrateMessages,
62
+ bumpMetaTick,
63
+ }: UseChatHistoryHydrationArgs): UseChatHistoryHydrationResult {
64
+ const [isHydratingHistory, setIsHydratingHistory] = useState(false)
65
+ const hydratedKeyRef = useRef<string | null>(null)
66
+
67
+ useEffect(() => {
68
+ if (!active) return
69
+ // No stored conversation id → nothing to hydrate (fresh visitor / after
70
+ // "new chat"); the first send establishes the conversation server-side.
71
+ const conversationId = conversationIdRef.current
72
+ if (!conversationId) return
73
+ const key = `${source}:${conversationId}`
74
+ if (hydratedKeyRef.current === key) return
75
+ hydratedKeyRef.current = key
76
+ let cancelled = false
77
+ setIsHydratingHistory(true)
78
+ ;(async () => {
79
+ try {
80
+ const res = await chatAuthedFetch(
81
+ `${historyUrl}?conversationId=${encodeURIComponent(conversationId)}`,
82
+ { method: 'GET' },
83
+ )
84
+ if (!res.ok) return
85
+ const payload = await res.json().catch(() => null)
86
+ // route-base successResponse envelope ({ data }) with a raw-body fallback.
87
+ const body = (payload?.data ?? payload) as
88
+ | { messages?: Array<Record<string, unknown>> }
89
+ | null
90
+ const rows = Array.isArray(body?.messages) ? body!.messages! : []
91
+ if (cancelled || rows.length === 0) return
92
+ const hydrated: Message[] = []
93
+ let userTurns = 0
94
+ for (const row of rows) {
95
+ const role =
96
+ row.role === 'assistant' ? 'assistant' : row.role === 'user' ? 'user' : null
97
+ if (!role) continue
98
+ const content = typeof row.content === 'string' ? row.content : ''
99
+ // The approval placeholder ('') and the server-built auto-continuation
100
+ // directive are part of the LLM history but never rendered.
101
+ const hidden =
102
+ role === 'user' &&
103
+ (content === '' || content.startsWith(AUTO_CONTINUATION_DIRECTIVE_PREFIX))
104
+ if (role === 'user') {
105
+ userTurns += 1
106
+ } else if (row.chat_refs && typeof row.chat_refs === 'object') {
107
+ // Re-attach inline entity-card refs at the send index this
108
+ // assistant row belongs to (same sendIdx scheme as live turns).
109
+ refsMapRef.current.set(
110
+ Math.max(0, userTurns - 1),
111
+ row.chat_refs as Record<string, ChatRef>,
112
+ )
113
+ }
114
+ hydrated.push({
115
+ id: `hydrated-${String(row.seq ?? hydrated.length)}`,
116
+ role,
117
+ content,
118
+ ...(typeof row.created_at === 'string'
119
+ ? { timestamp: new Date(row.created_at) }
120
+ : {}),
121
+ ...(hidden ? { hidden: true } : {}),
122
+ } as Message)
123
+ }
124
+ if (cancelled || hydrated.length === 0) return
125
+ sendCountRef.current = userTurns
126
+ hydrateMessages(hydrated)
127
+ bumpMetaTick()
128
+ } catch {
129
+ // Fetch failed — start empty; the server still owns history (above).
130
+ } finally {
131
+ if (!cancelled) setIsHydratingHistory(false)
132
+ }
133
+ })()
134
+ return () => {
135
+ cancelled = true
136
+ }
137
+ // Refs are stable across renders — the effect keys on the identity-ish deps only.
138
+ // eslint-disable-next-line react-hooks/exhaustive-deps
139
+ }, [active, source, historyUrl, hydrateMessages, bumpMetaTick])
140
+
141
+ return { isHydratingHistory, hydratedKeyRef }
142
+ }
@@ -476,6 +476,17 @@ export function useChat({
476
476
  reset()
477
477
  }, [reset])
478
478
 
479
+ /**
480
+ * Replace-or-prepend server-hydrated history. Called once by the SSE
481
+ * adapter after `GET /api/docs/chat/history` resolves. If the user already
482
+ * sent a message before hydration landed, the fetched history is PREPENDED
483
+ * so nothing typed is lost — correctness is unaffected either way because
484
+ * the server resolves LLM history from its own store, not from this state.
485
+ */
486
+ const hydrateMessages = useCallback((history: Message[]) => {
487
+ setMessages((prev) => (prev.length === 0 ? history : [...history, ...prev]))
488
+ }, [])
489
+
479
490
  /**
480
491
  * Abort the in-flight streamed message. The fetch's AbortSignal terminates
481
492
  * the upstream Anthropic request (so billing stops); the `for await` loop
@@ -496,6 +507,7 @@ export function useChat({
496
507
  stopMessage,
497
508
  handleQuickAction,
498
509
  clearMessages,
510
+ hydrateMessages,
499
511
  hasMessages: messages.length > 0,
500
512
  }
501
513
  }
@@ -442,6 +442,8 @@ export function appendToTrailingAssistant(
442
442
  merged[merged.length - 1] = { type: 'text', text: tail.text + seg.text }
443
443
  } else if (seg.type === 'thinking' && tail?.type === 'thinking') {
444
444
  merged[merged.length - 1] = { type: 'thinking', text: tail.text + seg.text }
445
+ } else if (seg.type === 'guide' && tail?.type === 'guide') {
446
+ merged[merged.length - 1] = { type: 'guide', text: tail.text + seg.text }
445
447
  } else if (seg.type === 'approval_batch') {
446
448
  // Approval deltas must be IDEMPOTENT: the escalated-result emit can be
447
449
  // seen twice (live + catch-up replay over hydrated history), so upsert
@@ -173,6 +173,16 @@ export function useRealtimeChunkProcessor(
173
173
  break
174
174
  }
175
175
 
176
+ case 'guide': {
177
+ const segments = accumulator.appendGuide(action.text)
178
+ if (isInStreamRef.current || !hasEverStreamedRef.current) {
179
+ emitSegments(segments)
180
+ } else {
181
+ emitSegments([{ type: 'guide', text: action.text }], { append: true })
182
+ }
183
+ break
184
+ }
185
+
176
186
  case 'tool_execution': {
177
187
  // A starting tool run means the agent's turn is in progress even
178
188
  // when this lands after MESSAGE_END (approved commands execute