@dxos/react-ui-feed 0.11.1

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 (230) hide show
  1. package/LICENSE +105 -0
  2. package/dist/lib/chunk-src.mjs +2228 -0
  3. package/dist/lib/chunk-src.mjs.map +1 -0
  4. package/dist/lib/index.mjs +2 -0
  5. package/dist/lib/testing.mjs +1923 -0
  6. package/dist/lib/testing.mjs.map +1 -0
  7. package/dist/types/src/components/Block/HtmlBlock.d.ts +14 -0
  8. package/dist/types/src/components/Block/HtmlBlock.d.ts.map +1 -0
  9. package/dist/types/src/components/Block/MarkdownBlock.d.ts +24 -0
  10. package/dist/types/src/components/Block/MarkdownBlock.d.ts.map +1 -0
  11. package/dist/types/src/components/Block/block.stories.d.ts +21 -0
  12. package/dist/types/src/components/Block/block.stories.d.ts.map +1 -0
  13. package/dist/types/src/components/Block/construction.d.ts +36 -0
  14. package/dist/types/src/components/Block/construction.d.ts.map +1 -0
  15. package/dist/types/src/components/Block/extensions.d.ts +23 -0
  16. package/dist/types/src/components/Block/extensions.d.ts.map +1 -0
  17. package/dist/types/src/components/Block/highlight.d.ts +14 -0
  18. package/dist/types/src/components/Block/highlight.d.ts.map +1 -0
  19. package/dist/types/src/components/Block/index.d.ts +6 -0
  20. package/dist/types/src/components/Block/index.d.ts.map +1 -0
  21. package/dist/types/src/components/Block/selection-group.d.ts +21 -0
  22. package/dist/types/src/components/Block/selection-group.d.ts.map +1 -0
  23. package/dist/types/src/components/Block/widget-state.d.ts +33 -0
  24. package/dist/types/src/components/Block/widget-state.d.ts.map +1 -0
  25. package/dist/types/src/components/MessageList/MessageList.d.ts +129 -0
  26. package/dist/types/src/components/MessageList/MessageList.d.ts.map +1 -0
  27. package/dist/types/src/components/MessageList/MessageList.stories.d.ts +47 -0
  28. package/dist/types/src/components/MessageList/MessageList.stories.d.ts.map +1 -0
  29. package/dist/types/src/components/MessageList/index.d.ts +3 -0
  30. package/dist/types/src/components/MessageList/index.d.ts.map +1 -0
  31. package/dist/types/src/components/MessageList/position-log.d.ts +73 -0
  32. package/dist/types/src/components/MessageList/position-log.d.ts.map +1 -0
  33. package/dist/types/src/components/Minimap/Minimap.d.ts +28 -0
  34. package/dist/types/src/components/Minimap/Minimap.d.ts.map +1 -0
  35. package/dist/types/src/components/Minimap/Minimap.stories.d.ts +21 -0
  36. package/dist/types/src/components/Minimap/Minimap.stories.d.ts.map +1 -0
  37. package/dist/types/src/components/Minimap/index.d.ts +2 -0
  38. package/dist/types/src/components/Minimap/index.d.ts.map +1 -0
  39. package/dist/types/src/components/Outline/Outline.d.ts +46 -0
  40. package/dist/types/src/components/Outline/Outline.d.ts.map +1 -0
  41. package/dist/types/src/components/Outline/Outline.stories.d.ts +37 -0
  42. package/dist/types/src/components/Outline/Outline.stories.d.ts.map +1 -0
  43. package/dist/types/src/components/Outline/index.d.ts +2 -0
  44. package/dist/types/src/components/Outline/index.d.ts.map +1 -0
  45. package/dist/types/src/components/index.d.ts +5 -0
  46. package/dist/types/src/components/index.d.ts.map +1 -0
  47. package/dist/types/src/debug/Debug.d.ts +17 -0
  48. package/dist/types/src/debug/Debug.d.ts.map +1 -0
  49. package/dist/types/src/debug/Stats.d.ts +30 -0
  50. package/dist/types/src/debug/Stats.d.ts.map +1 -0
  51. package/dist/types/src/debug/debug-model.d.ts +32 -0
  52. package/dist/types/src/debug/debug-model.d.ts.map +1 -0
  53. package/dist/types/src/debug/debug-model.test.d.ts +2 -0
  54. package/dist/types/src/debug/debug-model.test.d.ts.map +1 -0
  55. package/dist/types/src/debug/frame-meter.d.ts +48 -0
  56. package/dist/types/src/debug/frame-meter.d.ts.map +1 -0
  57. package/dist/types/src/debug/frame-meter.test.d.ts +2 -0
  58. package/dist/types/src/debug/frame-meter.test.d.ts.map +1 -0
  59. package/dist/types/src/debug/index.d.ts +6 -0
  60. package/dist/types/src/debug/index.d.ts.map +1 -0
  61. package/dist/types/src/debug/use-debug.d.ts +19 -0
  62. package/dist/types/src/debug/use-debug.d.ts.map +1 -0
  63. package/dist/types/src/hooks/follow.d.ts +89 -0
  64. package/dist/types/src/hooks/follow.d.ts.map +1 -0
  65. package/dist/types/src/hooks/follow.stories.d.ts +25 -0
  66. package/dist/types/src/hooks/follow.stories.d.ts.map +1 -0
  67. package/dist/types/src/hooks/follow.test.d.ts +2 -0
  68. package/dist/types/src/hooks/follow.test.d.ts.map +1 -0
  69. package/dist/types/src/hooks/index.d.ts +6 -0
  70. package/dist/types/src/hooks/index.d.ts.map +1 -0
  71. package/dist/types/src/hooks/useDecorations.d.ts +31 -0
  72. package/dist/types/src/hooks/useDecorations.d.ts.map +1 -0
  73. package/dist/types/src/hooks/useFeedNavigation.d.ts +44 -0
  74. package/dist/types/src/hooks/useFeedNavigation.d.ts.map +1 -0
  75. package/dist/types/src/hooks/useFollow.d.ts +46 -0
  76. package/dist/types/src/hooks/useFollow.d.ts.map +1 -0
  77. package/dist/types/src/hooks/useItemSelection.d.ts +34 -0
  78. package/dist/types/src/hooks/useItemSelection.d.ts.map +1 -0
  79. package/dist/types/src/index.d.ts +4 -0
  80. package/dist/types/src/index.d.ts.map +1 -0
  81. package/dist/types/src/model/feed-model.d.ts +67 -0
  82. package/dist/types/src/model/feed-model.d.ts.map +1 -0
  83. package/dist/types/src/model/feed-model.test.d.ts +2 -0
  84. package/dist/types/src/model/feed-model.test.d.ts.map +1 -0
  85. package/dist/types/src/model/feed.d.ts +66 -0
  86. package/dist/types/src/model/feed.d.ts.map +1 -0
  87. package/dist/types/src/model/feed.test.d.ts +2 -0
  88. package/dist/types/src/model/feed.test.d.ts.map +1 -0
  89. package/dist/types/src/model/index.d.ts +5 -0
  90. package/dist/types/src/model/index.d.ts.map +1 -0
  91. package/dist/types/src/model/list-model.d.ts +61 -0
  92. package/dist/types/src/model/list-model.d.ts.map +1 -0
  93. package/dist/types/src/model/list-model.test.d.ts +2 -0
  94. package/dist/types/src/model/list-model.test.d.ts.map +1 -0
  95. package/dist/types/src/model/useListModel.d.ts +22 -0
  96. package/dist/types/src/model/useListModel.d.ts.map +1 -0
  97. package/dist/types/src/stories/assistant.stories.d.ts +38 -0
  98. package/dist/types/src/stories/assistant.stories.d.ts.map +1 -0
  99. package/dist/types/src/stories/baseline.stories.d.ts +32 -0
  100. package/dist/types/src/stories/baseline.stories.d.ts.map +1 -0
  101. package/dist/types/src/stories/bridge.stories.d.ts +72 -0
  102. package/dist/types/src/stories/bridge.stories.d.ts.map +1 -0
  103. package/dist/types/src/stories/construction.stories.d.ts +17 -0
  104. package/dist/types/src/stories/construction.stories.d.ts.map +1 -0
  105. package/dist/types/src/stories/fill.stories.d.ts +51 -0
  106. package/dist/types/src/stories/fill.stories.d.ts.map +1 -0
  107. package/dist/types/src/stories/horizontal.stories.d.ts +17 -0
  108. package/dist/types/src/stories/horizontal.stories.d.ts.map +1 -0
  109. package/dist/types/src/stories/mount.stories.d.ts +19 -0
  110. package/dist/types/src/stories/mount.stories.d.ts.map +1 -0
  111. package/dist/types/src/stories/navigation.stories.d.ts +33 -0
  112. package/dist/types/src/stories/navigation.stories.d.ts.map +1 -0
  113. package/dist/types/src/stories/streaming.stories.d.ts +17 -0
  114. package/dist/types/src/stories/streaming.stories.d.ts.map +1 -0
  115. package/dist/types/src/stories/tail.stories.d.ts +47 -0
  116. package/dist/types/src/stories/tail.stories.d.ts.map +1 -0
  117. package/dist/types/src/stories/widget.stories.d.ts +16 -0
  118. package/dist/types/src/stories/widget.stories.d.ts.map +1 -0
  119. package/dist/types/src/testing/FeedStory.d.ts +30 -0
  120. package/dist/types/src/testing/FeedStory.d.ts.map +1 -0
  121. package/dist/types/src/testing/MessageWindow.d.ts +48 -0
  122. package/dist/types/src/testing/MessageWindow.d.ts.map +1 -0
  123. package/dist/types/src/testing/controls.d.ts +46 -0
  124. package/dist/types/src/testing/controls.d.ts.map +1 -0
  125. package/dist/types/src/testing/debug/FeedStats.d.ts +18 -0
  126. package/dist/types/src/testing/debug/FeedStats.d.ts.map +1 -0
  127. package/dist/types/src/testing/debug/index.d.ts +4 -0
  128. package/dist/types/src/testing/debug/index.d.ts.map +1 -0
  129. package/dist/types/src/testing/debug/sweep.d.ts +18 -0
  130. package/dist/types/src/testing/debug/sweep.d.ts.map +1 -0
  131. package/dist/types/src/testing/debug/use-feed-debug.d.ts +33 -0
  132. package/dist/types/src/testing/debug/use-feed-debug.d.ts.map +1 -0
  133. package/dist/types/src/testing/generator.d.ts +16 -0
  134. package/dist/types/src/testing/generator.d.ts.map +1 -0
  135. package/dist/types/src/testing/index.d.ts +10 -0
  136. package/dist/types/src/testing/index.d.ts.map +1 -0
  137. package/dist/types/src/testing/scenarios.d.ts +58 -0
  138. package/dist/types/src/testing/scenarios.d.ts.map +1 -0
  139. package/dist/types/src/testing/stream.d.ts +22 -0
  140. package/dist/types/src/testing/stream.d.ts.map +1 -0
  141. package/dist/types/src/testing/turn.d.ts +21 -0
  142. package/dist/types/src/testing/turn.d.ts.map +1 -0
  143. package/dist/types/src/testing/widgets.d.ts +3 -0
  144. package/dist/types/src/testing/widgets.d.ts.map +1 -0
  145. package/dist/types/src/virtualizer/Window.d.ts +137 -0
  146. package/dist/types/src/virtualizer/Window.d.ts.map +1 -0
  147. package/dist/types/src/virtualizer/Window.stories.d.ts +112 -0
  148. package/dist/types/src/virtualizer/Window.stories.d.ts.map +1 -0
  149. package/dist/types/src/virtualizer/index.d.ts +3 -0
  150. package/dist/types/src/virtualizer/index.d.ts.map +1 -0
  151. package/dist/types/src/virtualizer/placement.d.ts +188 -0
  152. package/dist/types/src/virtualizer/placement.d.ts.map +1 -0
  153. package/dist/types/src/virtualizer/placement.test.d.ts +2 -0
  154. package/dist/types/src/virtualizer/placement.test.d.ts.map +1 -0
  155. package/dist/types/tsconfig.tsbuildinfo +1 -0
  156. package/package.json +72 -0
  157. package/src/components/Block/HtmlBlock.tsx +26 -0
  158. package/src/components/Block/MarkdownBlock.tsx +157 -0
  159. package/src/components/Block/block.stories.tsx +140 -0
  160. package/src/components/Block/construction.ts +77 -0
  161. package/src/components/Block/extensions.ts +95 -0
  162. package/src/components/Block/highlight.ts +53 -0
  163. package/src/components/Block/index.ts +10 -0
  164. package/src/components/Block/selection-group.ts +53 -0
  165. package/src/components/Block/widget-state.tsx +75 -0
  166. package/src/components/MessageList/MessageList.stories.tsx +111 -0
  167. package/src/components/MessageList/MessageList.tsx +647 -0
  168. package/src/components/MessageList/index.ts +6 -0
  169. package/src/components/MessageList/position-log.ts +167 -0
  170. package/src/components/Minimap/Minimap.stories.tsx +129 -0
  171. package/src/components/Minimap/Minimap.tsx +84 -0
  172. package/src/components/Minimap/index.ts +5 -0
  173. package/src/components/Outline/Outline.stories.tsx +196 -0
  174. package/src/components/Outline/Outline.tsx +383 -0
  175. package/src/components/Outline/index.ts +5 -0
  176. package/src/components/index.ts +8 -0
  177. package/src/debug/Debug.tsx +90 -0
  178. package/src/debug/Stats.tsx +57 -0
  179. package/src/debug/debug-model.test.ts +36 -0
  180. package/src/debug/debug-model.ts +66 -0
  181. package/src/debug/frame-meter.test.ts +49 -0
  182. package/src/debug/frame-meter.ts +188 -0
  183. package/src/debug/index.ts +9 -0
  184. package/src/debug/use-debug.tsx +41 -0
  185. package/src/hooks/follow.stories.tsx +200 -0
  186. package/src/hooks/follow.test.ts +105 -0
  187. package/src/hooks/follow.ts +241 -0
  188. package/src/hooks/index.ts +9 -0
  189. package/src/hooks/useDecorations.tsx +54 -0
  190. package/src/hooks/useFeedNavigation.ts +90 -0
  191. package/src/hooks/useFollow.ts +258 -0
  192. package/src/hooks/useItemSelection.tsx +53 -0
  193. package/src/index.ts +7 -0
  194. package/src/model/feed-model.test.ts +84 -0
  195. package/src/model/feed-model.ts +214 -0
  196. package/src/model/feed.test.ts +65 -0
  197. package/src/model/feed.ts +157 -0
  198. package/src/model/index.ts +8 -0
  199. package/src/model/list-model.test.ts +71 -0
  200. package/src/model/list-model.ts +152 -0
  201. package/src/model/useListModel.ts +53 -0
  202. package/src/stories/assistant.stories.tsx +425 -0
  203. package/src/stories/baseline.stories.tsx +78 -0
  204. package/src/stories/bridge.stories.tsx +376 -0
  205. package/src/stories/construction.stories.tsx +147 -0
  206. package/src/stories/fill.stories.tsx +271 -0
  207. package/src/stories/horizontal.stories.tsx +129 -0
  208. package/src/stories/mount.stories.tsx +233 -0
  209. package/src/stories/navigation.stories.tsx +145 -0
  210. package/src/stories/streaming.stories.tsx +132 -0
  211. package/src/stories/tail.stories.tsx +149 -0
  212. package/src/stories/widget.stories.tsx +107 -0
  213. package/src/testing/FeedStory.tsx +452 -0
  214. package/src/testing/MessageWindow.tsx +139 -0
  215. package/src/testing/controls.tsx +96 -0
  216. package/src/testing/debug/FeedStats.tsx +182 -0
  217. package/src/testing/debug/index.ts +16 -0
  218. package/src/testing/debug/sweep.ts +85 -0
  219. package/src/testing/debug/use-feed-debug.ts +91 -0
  220. package/src/testing/generator.ts +90 -0
  221. package/src/testing/index.ts +13 -0
  222. package/src/testing/scenarios.tsx +552 -0
  223. package/src/testing/stream.ts +70 -0
  224. package/src/testing/turn.ts +91 -0
  225. package/src/testing/widgets.tsx +183 -0
  226. package/src/virtualizer/Window.stories.tsx +683 -0
  227. package/src/virtualizer/Window.tsx +435 -0
  228. package/src/virtualizer/index.ts +6 -0
  229. package/src/virtualizer/placement.test.ts +312 -0
  230. package/src/virtualizer/placement.ts +403 -0
@@ -0,0 +1,91 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { random } from '@dxos/random';
6
+ import { type ContentBlock } from '@dxos/types';
7
+
8
+ import { createAnswer, textStream } from './stream';
9
+
10
+ export type TurnOptions = {
11
+ chunkDelay?: number;
12
+ wordsPerChunk?: number;
13
+ };
14
+
15
+ /**
16
+ * One assistant turn as the blocks a model actually emits, arriving in order.
17
+ *
18
+ * The text-only stream exercises the easy half of reconciliation: a tail that only ever grows, which
19
+ * the item appends. A real turn is harder in three ways, and each is a separate way for an item to
20
+ * be rebuilt instead of extended —
21
+ *
22
+ * 1. a **status** block appears first and is removed once work begins, so the document shrinks;
23
+ * 2. **reasoning** streams inside an unclosed tag and is closed when it completes, which rewrites
24
+ * the end of the document rather than appending to it;
25
+ * 3. a **tool call** lands whole, then its **result** arrives as a second block after it.
26
+ *
27
+ * Yields the complete block array at each step; the caller replaces the message's blocks with it.
28
+ */
29
+ export async function* streamTurn({ chunkDelay = 120, wordsPerChunk = 4 }: TurnOptions = {}): AsyncGenerator<
30
+ ContentBlock.Any[],
31
+ void
32
+ > {
33
+ const pause = () => new Promise((resolve) => setTimeout(resolve, chunkDelay));
34
+
35
+ // A status the reader sees before anything else exists, and which is gone by the answer.
36
+ const status: ContentBlock.Any = { _tag: 'status', statusText: 'Thinking…', pending: true };
37
+ yield [status];
38
+ await pause();
39
+
40
+ let reasoning = '';
41
+ for await (const chunk of textStream(random.lorem.paragraph(), { chunkDelay, wordsPerChunk })) {
42
+ reasoning += chunk;
43
+ yield [status, { _tag: 'reasoning', reasoningText: reasoning, pending: true }];
44
+ }
45
+
46
+ // Closing the tag and dropping the status in one step: the document does not grow here, and an
47
+ // item that only knows how to append would replace itself instead.
48
+ const reasoned: ContentBlock.Any = { _tag: 'reasoning', reasoningText: reasoning };
49
+ yield [reasoned];
50
+ await pause();
51
+
52
+ const toolCallId = `tool-${random.number.int({ min: 1000, max: 9999 })}`;
53
+ const call: ContentBlock.Any = {
54
+ _tag: 'toolCall',
55
+ toolCallId,
56
+ name: 'search',
57
+ input: '{}',
58
+ providerExecuted: false,
59
+ pending: true,
60
+ };
61
+ yield [reasoned, call];
62
+ await pause();
63
+
64
+ const called: ContentBlock.Any = { ...call, pending: false };
65
+ const result: ContentBlock.Any = {
66
+ _tag: 'toolResult',
67
+ toolCallId,
68
+ name: 'search',
69
+ result: random.lorem.sentence(12),
70
+ providerExecuted: false,
71
+ };
72
+ yield [reasoned, called, result];
73
+ await pause();
74
+
75
+ let text = '';
76
+ for await (const chunk of textStream(createAnswer(), { chunkDelay, wordsPerChunk })) {
77
+ text += chunk;
78
+ yield [reasoned, called, result, { _tag: 'text', text, pending: true }];
79
+ }
80
+
81
+ // Suggestions arrive only once the answer is complete, so the last step both closes the text and
82
+ // extends the message — the two kinds of change at once.
83
+ yield [
84
+ reasoned,
85
+ called,
86
+ result,
87
+ { _tag: 'text', text },
88
+ { _tag: 'suggestion', text: 'Tell me more' },
89
+ { _tag: 'suggestion', text: 'Show the sources' },
90
+ ];
91
+ }
@@ -0,0 +1,183 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { Icon, IconBlock } from '@dxos/react-ui';
8
+ import { TogglePanel } from '@dxos/react-ui-components';
9
+ import { type XmlWidgetProps, type XmlWidgetRegistry, getXmlTextChild } from '@dxos/ui-editor';
10
+ import { mx } from '@dxos/ui-theme';
11
+
12
+ import { useWidgetState } from '../components';
13
+
14
+ /**
15
+ * Widgets for the block kinds an assistant turn contains.
16
+ *
17
+ * Approximations of `plugin-assistant`'s, deliberately: what the engine has to prove is that a
18
+ * per-message document can carry block widgets at all — that they mount, measure and survive
19
+ * streaming — not that these are the widgets the plugin will ship. The real ones move down when the
20
+ * plugin does.
21
+ *
22
+ * Every widget renders from the tag alone. The plugin keeps tool state beside the document and
23
+ * re-applies it after a remount; an item here is rebuilt from its message, so state that is not in
24
+ * the document has nowhere to survive.
25
+ */
26
+
27
+ /**
28
+ * A collapsible block: the header is what the reader sees while scrolling past, the body is what
29
+ * they open when they care. Reasoning and tool traces are long, incidental and frequent — rendering
30
+ * them expanded turns a chat into a log.
31
+ *
32
+ * The disclosure animates, and every frame of it is measured twice — by CodeMirror, to re-place the
33
+ * lines under the widget, and by the virtualizer, to re-place the rows under the item. That is what
34
+ * it costs for the content below to travel with the panel instead of jumping to its new place; the
35
+ * geometry stays consistent because `MarkdownBlock` re-measures the document as the widget resizes.
36
+ */
37
+ const Panel = ({
38
+ stateKey,
39
+ icon,
40
+ title,
41
+ children,
42
+ classNames,
43
+ }: {
44
+ /** Identifies this panel within its message, so its open flag can outlive the row. */
45
+ stateKey: string;
46
+ icon: string;
47
+ title: string;
48
+ children: React.ReactNode;
49
+ classNames?: string;
50
+ }) => {
51
+ const [open, setOpen] = useWidgetState(stateKey, false);
52
+
53
+ return (
54
+ // The open flag is published to the DOM so a test can ask whether it survived the row being
55
+ // unmounted — which is the whole question a virtualized feed raises about widget state.
56
+ <div data-testid='feed.widget' data-open={open}>
57
+ <TogglePanel.Root open={open} onChangeOpen={setOpen}>
58
+ {/* A minimum height, not a fixed one: the widget's content is portaled and paints after
59
+ CodeMirror has placed the box, so without a floor the row is measured empty and grows a
60
+ frame later — a row jumping under the reader. `estimatedHeight` in the registry looks like
61
+ the answer and is not: it sets `height` and `overflow: hidden` on the widget root, which
62
+ pins the panel shut. */}
63
+ <TogglePanel.Content classNames={mx('min-h-[2.125rem] rounded border border-subdued-separator', classNames)}>
64
+ <TogglePanel.Header classNames='flex items-center gap-2 px-2 py-1 text-sm'>
65
+ <span className='grow text-description truncate'>{title}</span>
66
+ <IconBlock>
67
+ <Icon icon={icon} size={4} />
68
+ </IconBlock>
69
+ </TogglePanel.Header>
70
+ <TogglePanel.Body>
71
+ <TogglePanel.Viewport classNames='px-2 pb-1 text-sm'>{children}</TogglePanel.Viewport>
72
+ </TogglePanel.Body>
73
+ </TogglePanel.Content>
74
+ </TogglePanel.Root>
75
+ </div>
76
+ );
77
+ };
78
+
79
+ const Frame = ({ icon, title, children, classNames }: XmlWidgetProps<any> & { classNames?: string }) => (
80
+ <div className={mx('flex gap-2 px-2 py-1 rounded border border-subdued-separator text-sm', classNames)}>
81
+ {icon && <Icon icon={icon} size={4} classNames='mt-1 shrink-0 text-description' />}
82
+ <div className='min-w-0'>
83
+ {title && <p className='text-xs text-description'>{title}</p>}
84
+ {children}
85
+ </div>
86
+ </div>
87
+ );
88
+
89
+ const Reasoning = ({ children, range }: XmlWidgetProps) => {
90
+ const text = getXmlTextChild(children ?? []) ?? '';
91
+ return (
92
+ <Panel
93
+ stateKey={`reasoning:${range?.from}`}
94
+ icon='ph--brain--regular'
95
+ title={text.slice(0, 80)}
96
+ classNames='opacity-70'
97
+ >
98
+ <p>{text}</p>
99
+ </Panel>
100
+ );
101
+ };
102
+
103
+ const Status = ({ children }: XmlWidgetProps) => (
104
+ <p className='px-2 py-1 text-sm text-description animate-pulse'>{getXmlTextChild(children ?? [])}</p>
105
+ );
106
+
107
+ const ToolCall = ({ name, pending, range }: XmlWidgetProps<{ name?: string; pending?: string }>) => (
108
+ <Panel
109
+ stateKey={`toolCall:${range?.from}`}
110
+ icon={pending ? 'ph--circle-notch--regular' : 'ph--wrench--regular'}
111
+ title={`Calling ${name}`}
112
+ >
113
+ <p className='font-mono text-xs'>{name}</p>
114
+ </Panel>
115
+ );
116
+
117
+ const ToolResult = ({ children, for: tool, range }: XmlWidgetProps<{ for?: string }>) => (
118
+ <Panel stateKey={`toolResult:${range?.from}`} icon='ph--check--regular' title={`${tool} returned`}>
119
+ <p className='font-mono text-xs'>{getXmlTextChild(children ?? [])}</p>
120
+ </Panel>
121
+ );
122
+
123
+ // Inline, so a run of them wraps as chips rather than stacking one per line.
124
+ const Suggestion = ({ children }: XmlWidgetProps) => (
125
+ <button type='button' className='me-1 px-2 py-0.5 rounded-full border border-separator text-sm'>
126
+ {getXmlTextChild(children ?? [])}
127
+ </button>
128
+ );
129
+
130
+ const Select = ({ children }: XmlWidgetProps) => {
131
+ const options = (children ?? [])
132
+ .map((option: any) => option?._tag === 'option' && getXmlTextChild(option.children))
133
+ .filter(Boolean) as string[];
134
+
135
+ return (
136
+ <div className='flex flex-col gap-1 py-1'>
137
+ {options.map((option) => (
138
+ <button key={option} type='button' className='px-2 py-1 text-left rounded border border-separator text-sm'>
139
+ {option}
140
+ </button>
141
+ ))}
142
+ </div>
143
+ );
144
+ };
145
+
146
+ const Json = ({ children }: XmlWidgetProps) => (
147
+ <pre className='px-2 py-1 overflow-x-auto rounded bg-input-surface text-xs'>{getXmlTextChild(children ?? [])}</pre>
148
+ );
149
+
150
+ /**
151
+ * Floor for a panel's collapsed height, reserved before its portaled content paints.
152
+ *
153
+ * `heightMode: 'min'` is what makes this a floor rather than a pin — the default pins the box and
154
+ * clips it, so an opened panel stays shut. Without any reservation the row is measured at the height
155
+ * of an empty box and grows a frame later, which is a row moving under a reader scrolling up.
156
+ */
157
+ const COLLAPSED_HEIGHT = 42;
158
+
159
+ export const chatRegistry: XmlWidgetRegistry = {
160
+ // No widget: the reader's own words stay in the document, where they can be selected and searched
161
+ // like any other text. Registered so the markdown parser keeps the tag as one block — an
162
+ // unregistered tag opens a paragraph that swallows the lines after it.
163
+ prompt: { block: true },
164
+ reasoning: {
165
+ block: true,
166
+ streaming: true,
167
+ estimatedHeight: () => COLLAPSED_HEIGHT,
168
+ heightMode: 'min',
169
+ Component: Reasoning,
170
+ },
171
+ status: {
172
+ block: true,
173
+ streaming: true,
174
+ estimatedHeight: () => COLLAPSED_HEIGHT,
175
+ heightMode: 'min',
176
+ Component: Status,
177
+ },
178
+ toolCall: { block: true, estimatedHeight: () => COLLAPSED_HEIGHT, heightMode: 'min', Component: ToolCall },
179
+ toolResult: { block: true, estimatedHeight: () => COLLAPSED_HEIGHT, heightMode: 'min', Component: ToolResult },
180
+ suggestion: { block: false, Component: Suggestion },
181
+ select: { block: true, Component: Select },
182
+ json: { block: true, Component: Json },
183
+ };