@dxos/react-ui-feed 0.11.1 → 0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264

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 (111) hide show
  1. package/dist/lib/chunk-src.mjs +270 -1020
  2. package/dist/lib/chunk-src.mjs.map +1 -1
  3. package/dist/lib/debug.mjs +296 -0
  4. package/dist/lib/debug.mjs.map +1 -0
  5. package/dist/lib/index.mjs +2 -2
  6. package/dist/lib/testing.mjs +60 -266
  7. package/dist/lib/testing.mjs.map +1 -1
  8. package/dist/types/src/components/Block/MarkdownBlock.d.ts +8 -1
  9. package/dist/types/src/components/Block/MarkdownBlock.d.ts.map +1 -1
  10. package/dist/types/src/components/Block/block.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/MessageList/MessageList.d.ts +40 -25
  12. package/dist/types/src/components/MessageList/MessageList.d.ts.map +1 -1
  13. package/dist/types/src/components/MessageList/MessageList.stories.d.ts.map +1 -1
  14. package/dist/types/src/components/Minimap/Minimap.d.ts +1 -1
  15. package/dist/types/src/components/Minimap/Minimap.d.ts.map +1 -1
  16. package/dist/types/src/components/Minimap/Minimap.stories.d.ts.map +1 -1
  17. package/dist/types/src/components/Outline/Outline.stories.d.ts +4 -3
  18. package/dist/types/src/components/Outline/Outline.stories.d.ts.map +1 -1
  19. package/dist/types/src/debug/index.d.ts +1 -1
  20. package/dist/types/src/debug/index.d.ts.map +1 -1
  21. package/dist/types/src/debug/{use-debug.d.ts → useDebug.d.ts} +1 -1
  22. package/dist/types/src/debug/useDebug.d.ts.map +1 -0
  23. package/dist/types/src/hooks/index.d.ts +0 -2
  24. package/dist/types/src/hooks/index.d.ts.map +1 -1
  25. package/dist/types/src/hooks/useFeedNavigation.d.ts +2 -4
  26. package/dist/types/src/hooks/useFeedNavigation.d.ts.map +1 -1
  27. package/dist/types/src/model/feed-model.d.ts +6 -0
  28. package/dist/types/src/model/feed-model.d.ts.map +1 -1
  29. package/dist/types/src/model/feed.d.ts +1 -1
  30. package/dist/types/src/model/feed.d.ts.map +1 -1
  31. package/dist/types/src/model/index.d.ts +0 -1
  32. package/dist/types/src/model/index.d.ts.map +1 -1
  33. package/dist/types/src/model/useListModel.d.ts +1 -6
  34. package/dist/types/src/model/useListModel.d.ts.map +1 -1
  35. package/dist/types/src/stories/bridge.stories.d.ts +5 -10
  36. package/dist/types/src/stories/bridge.stories.d.ts.map +1 -1
  37. package/dist/types/src/stories/horizontal.stories.d.ts.map +1 -1
  38. package/dist/types/src/stories/navigation.stories.d.ts +9 -0
  39. package/dist/types/src/stories/navigation.stories.d.ts.map +1 -1
  40. package/dist/types/src/stories/tail.stories.d.ts.map +1 -1
  41. package/dist/types/src/stories/widget.stories.d.ts +10 -0
  42. package/dist/types/src/stories/widget.stories.d.ts.map +1 -1
  43. package/dist/types/src/testing/FeedStory.d.ts +3 -3
  44. package/dist/types/src/testing/FeedStory.d.ts.map +1 -1
  45. package/dist/types/src/testing/MessageWindow.d.ts +1 -1
  46. package/dist/types/src/testing/MessageWindow.d.ts.map +1 -1
  47. package/dist/types/tsconfig.tsbuildinfo +1 -1
  48. package/package.json +11 -16
  49. package/src/components/Block/MarkdownBlock.tsx +195 -117
  50. package/src/components/Block/block.stories.tsx +11 -11
  51. package/src/components/MessageList/MessageList.stories.tsx +6 -3
  52. package/src/components/MessageList/MessageList.tsx +217 -50
  53. package/src/components/Minimap/Minimap.stories.tsx +1 -2
  54. package/src/components/Minimap/Minimap.tsx +1 -2
  55. package/src/debug/Debug.tsx +2 -2
  56. package/src/debug/index.ts +1 -1
  57. package/src/hooks/index.ts +0 -2
  58. package/src/hooks/useFeedNavigation.ts +36 -11
  59. package/src/model/feed-model.ts +9 -0
  60. package/src/model/feed.ts +6 -8
  61. package/src/model/index.ts +0 -1
  62. package/src/model/useListModel.ts +1 -17
  63. package/src/stories/baseline.stories.tsx +2 -2
  64. package/src/stories/bridge.stories.tsx +18 -34
  65. package/src/stories/fill.stories.tsx +2 -2
  66. package/src/stories/horizontal.stories.tsx +1 -2
  67. package/src/stories/navigation.stories.tsx +41 -0
  68. package/src/stories/streaming.stories.tsx +1 -1
  69. package/src/stories/tail.stories.tsx +18 -6
  70. package/src/stories/widget.stories.tsx +34 -0
  71. package/src/testing/FeedStory.tsx +6 -58
  72. package/src/testing/MessageWindow.tsx +1 -2
  73. package/src/testing/scenarios.tsx +40 -17
  74. package/dist/types/src/debug/use-debug.d.ts.map +0 -1
  75. package/dist/types/src/hooks/follow.d.ts +0 -89
  76. package/dist/types/src/hooks/follow.d.ts.map +0 -1
  77. package/dist/types/src/hooks/follow.stories.d.ts +0 -25
  78. package/dist/types/src/hooks/follow.stories.d.ts.map +0 -1
  79. package/dist/types/src/hooks/follow.test.d.ts +0 -2
  80. package/dist/types/src/hooks/follow.test.d.ts.map +0 -1
  81. package/dist/types/src/hooks/useFollow.d.ts +0 -46
  82. package/dist/types/src/hooks/useFollow.d.ts.map +0 -1
  83. package/dist/types/src/model/list-model.d.ts +0 -61
  84. package/dist/types/src/model/list-model.d.ts.map +0 -1
  85. package/dist/types/src/model/list-model.test.d.ts +0 -2
  86. package/dist/types/src/model/list-model.test.d.ts.map +0 -1
  87. package/dist/types/src/stories/assistant.stories.d.ts +0 -38
  88. package/dist/types/src/stories/assistant.stories.d.ts.map +0 -1
  89. package/dist/types/src/virtualizer/Window.d.ts +0 -137
  90. package/dist/types/src/virtualizer/Window.d.ts.map +0 -1
  91. package/dist/types/src/virtualizer/Window.stories.d.ts +0 -112
  92. package/dist/types/src/virtualizer/Window.stories.d.ts.map +0 -1
  93. package/dist/types/src/virtualizer/index.d.ts +0 -3
  94. package/dist/types/src/virtualizer/index.d.ts.map +0 -1
  95. package/dist/types/src/virtualizer/placement.d.ts +0 -188
  96. package/dist/types/src/virtualizer/placement.d.ts.map +0 -1
  97. package/dist/types/src/virtualizer/placement.test.d.ts +0 -2
  98. package/dist/types/src/virtualizer/placement.test.d.ts.map +0 -1
  99. package/src/hooks/follow.stories.tsx +0 -200
  100. package/src/hooks/follow.test.ts +0 -105
  101. package/src/hooks/follow.ts +0 -241
  102. package/src/hooks/useFollow.ts +0 -258
  103. package/src/model/list-model.test.ts +0 -71
  104. package/src/model/list-model.ts +0 -152
  105. package/src/stories/assistant.stories.tsx +0 -425
  106. package/src/virtualizer/Window.stories.tsx +0 -683
  107. package/src/virtualizer/Window.tsx +0 -435
  108. package/src/virtualizer/index.ts +0 -6
  109. package/src/virtualizer/placement.test.ts +0 -312
  110. package/src/virtualizer/placement.ts +0 -403
  111. /package/src/debug/{use-debug.tsx → useDebug.tsx} +0 -0
@@ -1,178 +1,14 @@
1
- import { createContext, memo, useCallback, useContext, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
1
+ import { createContext, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { EditorState, StateEffect, StateField } from "@codemirror/state";
4
4
  import { Decoration, EditorView } from "@codemirror/view";
5
5
  import DOMPurify from "dompurify";
6
6
  import { createPortal } from "react-dom";
7
- import { Column, Popover, ScrollArea, composable, composableProps, setRef, useThemeContext } from "@dxos/react-ui";
7
+ import { Column, IconButton, Popover, ScrollArea, composable, composableProps, setRef, useThemeContext } from "@dxos/react-ui";
8
8
  import { createBasicExtensions, createMarkdownExtensions, createThemeExtensions, decorateMarkdown, extendedMarkdown, xmlBlockDecoration, xmlFormatting, xmlTags } from "@dxos/ui-editor";
9
9
  import { createContext as createContext$1 } from "@radix-ui/react-context";
10
- import * as Atom from "effect/unstable/reactivity/Atom";
11
- import * as Registry from "effect/unstable/reactivity/AtomRegistry";
10
+ import { ListModel, useFollow, useWindow } from "@dxos/react-ui-virtual";
12
11
  import { mx } from "@dxos/ui-theme";
13
- //#region src/hooks/follow.ts
14
- var DEFAULT_MAX_SPEED = 2;
15
- var DEFAULT_ACCELERATION = 12;
16
- var DEFAULT_DECELERATION = 6;
17
- /**
18
- * Gentler than the ramp up, and deliberately so.
19
- *
20
- * Braking distance is `v²/2d`, so matching the two rates sheds full speed in three rows — a travel
21
- * of any length then cruises to within a few rows of the target and stops, which reads as an abrupt
22
- * halt however correct the curve is. At a third of the acceleration the landing occupies nine rows
23
- * and a second and a half, which is long enough to see.
24
- */
25
- var DEFAULT_ROW_HEIGHT = 120;
26
- /** Within this distance the follow has arrived; below it the animation would only jitter. */
27
- var ARRIVED = .5;
28
- /**
29
- * Speed for the next frame of a follow.
30
- *
31
- * Ramps up to `maxSpeed` and back down so the travel starts and ends gently rather than jumping.
32
- * The cap is whichever is lower: the ceiling, or the speed from which the remaining distance can
33
- * still be shed at `acceleration` (`v = sqrt(2·a·d)`) — that second term is what turns the arrival
34
- * into a deceleration instead of a stop, and what keeps a growing target at cruise speed.
35
- *
36
- * Acceleration is rate-limited so a start is gradual; **deceleration is not**, because the braking
37
- * curve is already a smooth ramp to zero and easing onto it instead leaves the speed above the
38
- * curve for the whole approach — the follow then runs out of distance while still moving and the
39
- * arrival reads as a stop rather than a landing.
40
- */
41
- var stepVelocity = ({ velocity, distance, dt, maxSpeed, acceleration, deceleration = acceleration }) => {
42
- const braking = Math.sqrt(2 * deceleration * Math.max(distance, 0));
43
- const desired = Math.min(maxSpeed, braking);
44
- return velocity < desired ? Math.min(desired, velocity + acceleration * dt) : desired;
45
- };
46
- /**
47
- * **Not wired into `MessageList` at present.** The placement engine follows the tail by holding an
48
- * intent and correcting the offset, which arrives instantly; this class is the animated travel the
49
- * old engine used, kept with its tests because the glide is worth having back and nothing about it
50
- * was wrong. Wiring it is a decision about feel, not a repair.
51
- */
52
- /**
53
- * Keeps an element scrolled to its bottom while its content grows.
54
- *
55
- * Native `scrollTo({ behavior: 'smooth' })` restarts its easing on every call, so a target that
56
- * moves every chunk never leaves the start of the curve — the motion reads as a stutter. This
57
- * carries velocity across frames instead: it accelerates as a stream begins, cruises while content
58
- * keeps arriving, and decelerates onto the tail when it stops.
59
- *
60
- * `@dxos/ui-editor`'s `createCrawler` follows a streaming CodeMirror document the same way, with a
61
- * critically-damped spring. The two are deliberately separate: that one is bound to `view.scrollDOM`
62
- * and driven by editor effects, where a feed owns its own scroll container. The models differ in
63
- * feel — a spring's speed falls off with distance, where this holds a ceiling and brakes onto the
64
- * target, which keeps a fast stream at a constant rate rather than an ever-widening lag.
65
- */
66
- var ScrollFollower = class {
67
- _element;
68
- #maxSpeed;
69
- #acceleration;
70
- #deceleration;
71
- #rowHeight;
72
- #trailing;
73
- #frame;
74
- #velocity = 0;
75
- #timestamp = 0;
76
- #braking = false;
77
- constructor(_element, { maxSpeed = DEFAULT_MAX_SPEED, acceleration = DEFAULT_ACCELERATION, deceleration = DEFAULT_DECELERATION, rowHeight = () => DEFAULT_ROW_HEIGHT, trailing = () => 0 } = {}) {
78
- this._element = _element;
79
- this.#maxSpeed = maxSpeed;
80
- this.#acceleration = acceleration;
81
- this.#deceleration = deceleration;
82
- this.#rowHeight = rowHeight;
83
- this.#trailing = trailing;
84
- }
85
- /**
86
- * The bottom of the last row, which is not the bottom of the document.
87
- *
88
- * A feed may reserve empty space below its last row so that row can be brought to the top of the
89
- * viewport. That space is somewhere the reader may go and nowhere the follow should: chasing the
90
- * element's own maximum walks the feed off the end of the conversation and keeps going.
91
- */
92
- get #bottom() {
93
- return this._element.scrollHeight - this.#trailing() - this._element.clientHeight;
94
- }
95
- get running() {
96
- return this.#frame !== void 0;
97
- }
98
- /** Current speed, rows/s. Exposed so a harness can show the ramp. */
99
- get velocity() {
100
- return this.#velocity / Math.max(this.#rowHeight(), 1);
101
- }
102
- /** Starts (or continues) travelling towards the bottom. Safe to call on every content change. */
103
- start() {
104
- this.#braking = false;
105
- if (this.#frame !== void 0) return;
106
- this.#timestamp = performance.now();
107
- this.#frame = requestAnimationFrame(this.#tick);
108
- }
109
- /**
110
- * Coasts to a halt, shedding speed at `deceleration` — what a stream ending or a stop control
111
- * means. Cutting the animation instead would halt mid-glide, which is the one motion the reader
112
- * cannot have caused and so reads as a fault.
113
- */
114
- stop() {
115
- if (this.#frame === void 0) {
116
- this.#velocity = 0;
117
- return;
118
- }
119
- this.#braking = true;
120
- }
121
- /**
122
- * Halts immediately, discarding the velocity — the reader has taken over, or the list is going
123
- * away. Coasting here would fight a hand already on the scrollbar.
124
- */
125
- cancel() {
126
- if (this.#frame !== void 0) {
127
- cancelAnimationFrame(this.#frame);
128
- this.#frame = void 0;
129
- }
130
- this.#braking = false;
131
- this.#velocity = 0;
132
- }
133
- /** Jumps to the bottom with no animation. */
134
- jump() {
135
- this.cancel();
136
- this._element.scrollTop = Math.max(0, this.#bottom);
137
- }
138
- #tick = (timestamp) => {
139
- const dt = Math.min((timestamp - this.#timestamp) / 1e3, .05);
140
- this.#timestamp = timestamp;
141
- const target = Math.max(0, this.#bottom);
142
- const distance = target - this._element.scrollTop;
143
- if (this.#braking) {
144
- const rowHeight = Math.max(this.#rowHeight(), 1);
145
- this.#velocity = Math.max(0, this.#velocity - this.#deceleration * rowHeight * dt);
146
- if (this.#velocity <= 0 || distance <= ARRIVED) {
147
- this.#frame = void 0;
148
- this.#velocity = 0;
149
- this.#braking = false;
150
- return;
151
- }
152
- this._element.scrollTop = Math.min(target, this._element.scrollTop + this.#velocity * dt);
153
- this.#frame = requestAnimationFrame(this.#tick);
154
- return;
155
- }
156
- if (distance <= ARRIVED) {
157
- this._element.scrollTop = target;
158
- this.#frame = void 0;
159
- this.#velocity = 0;
160
- return;
161
- }
162
- const rowHeight = Math.max(this.#rowHeight(), 1);
163
- this.#velocity = stepVelocity({
164
- velocity: this.#velocity,
165
- distance,
166
- dt,
167
- maxSpeed: this.#maxSpeed * rowHeight,
168
- acceleration: this.#acceleration * rowHeight,
169
- deceleration: this.#deceleration * rowHeight
170
- });
171
- this._element.scrollTop = Math.min(target, this._element.scrollTop + this.#velocity * dt);
172
- this.#frame = requestAnimationFrame(this.#tick);
173
- };
174
- };
175
- //#endregion
176
12
  //#region src/hooks/useDecorations.tsx
177
13
  var DecorationsContext = createContext(/* @__PURE__ */ new Map());
178
14
  /**
@@ -203,6 +39,8 @@ var useDecorations = (id) => {
203
39
  };
204
40
  //#endregion
205
41
  //#region src/hooks/useFeedNavigation.ts
42
+ /** How long a commanded destination outranks the scroll offset as "where the reader is". */
43
+ var PENDING_WINDOW = 1500;
206
44
  /**
207
45
  * The one navigation seam (SPEC: useFeedNavigation): toolbar buttons, arrow keys, the outline rail
208
46
  * and the minimap all call this, so they cannot disagree about what a step is.
@@ -216,190 +54,55 @@ var useDecorations = (id) => {
216
54
  * Distinct from react-ui-list's `useListNavigation`, which is focus traversal over fully-mounted
217
55
  * rows — a different problem in the same naming family.
218
56
  */
219
- var useFeedNavigation = ({ controller, stops, current, count, scrollPastEnd, onNavigate }) => {
220
- const jumpTo = useCallback((index, behavior = "auto") => {
221
- onNavigate?.(index);
222
- const align = !scrollPastEnd && index >= count() - 1 ? "end" : "start";
223
- controller.current?.scrollToIndex(index, align, behavior);
224
- }, [
225
- controller,
57
+ var useFeedNavigation = ({ controller, stops, current, count, onNavigate }) => {
58
+ const pending = useRef(void 0);
59
+ const optionsRef = useRef({
60
+ stops,
61
+ current,
226
62
  count,
227
- scrollPastEnd,
228
63
  onNavigate
229
- ]);
230
- return {
231
- step: useCallback((delta) => {
232
- const all = stops();
233
- if (!all.length) return;
234
- const at = current();
235
- const next = delta > 0 ? all.find(({ index }) => index > at) ?? all[all.length - 1] : [...all].reverse().find(({ index }) => index < at) ?? all[0];
236
- jumpTo(next.index, "smooth");
237
- }, [
238
- stops,
239
- current,
240
- jumpTo
241
- ]),
242
- jumpTo,
243
- first: useCallback(() => jumpTo(0), [jumpTo]),
244
- last: useCallback(() => jumpTo(count() - 1), [jumpTo, count])
245
- };
246
- };
247
- //#endregion
248
- //#region src/hooks/useFollow.ts
249
- /** Distance from the end within which the reader counts as being at it. */
250
- var STICKY_THRESHOLD = 32;
251
- /** How long after a gesture a scroll is still attributable to the reader. */
252
- var GESTURE_WINDOW = 300;
253
- /**
254
- * The sticky tail as an intent (SPEC: useFollow) — a policy over the window, not part of it.
255
- *
256
- * Withdrawn by one thing only: a scroll that moves *backwards*. That is the reader; a correction
257
- * never moves the offset back, it moves the end away. Re-deriving the intent from proximity on
258
- * every commit cannot survive its own corrections — measuring the tail grows the document under a
259
- * reader who has not moved, the gap opens past any threshold worth having, and the follow reads
260
- * that as the reader leaving and disengages for good (`bridge/Tail`, the second defect).
261
- *
262
- * Reusable for any streaming container — transcripts, logs — because nothing here knows about
263
- * messages: it needs a scroller, a placement, and the two numbers that say the content grew.
264
- */
265
- var useFollow = ({ scrollerRef, placement, extent, count, axis = "block", reserve = 0, enabled, glide = true }) => {
266
- const following = useRef(!!enabled);
267
- const wasEnabled = useRef(enabled);
268
- if (wasEnabled.current !== enabled) {
269
- wasEnabled.current = enabled;
270
- following.current = !!enabled;
271
- }
272
- /**
273
- * Where the reader has to be for the last row's rendered edge to rest on the viewport's.
274
- *
275
- * From the row's own rectangle when it is mounted, and from the arithmetic only when it is not:
276
- * measured extents are rounded to integers and rendered rows are fractional, so over a mounted
277
- * window the sum drifts from the layout by tens of pixels — an "arrived" that the reader can see
278
- * is short. The rect already includes the window's transform, so the delta is exact.
279
- */
280
- const endTarget = useCallback(() => {
281
- const scroller = scrollerRef.current;
282
- if (!scroller) return 0;
283
- const last = scroller.querySelector(`[data-index="${count - 1}"]`);
284
- if (last) {
285
- const current = axis === "block" ? scroller.scrollTop : scroller.scrollLeft;
286
- const delta = axis === "block" ? last.getBoundingClientRect().bottom - scroller.getBoundingClientRect().bottom : last.getBoundingClientRect().right - scroller.getBoundingClientRect().right;
287
- return Math.max(0, Math.round(current + delta));
288
- }
289
- return placement.endOffset();
290
- }, [
64
+ });
65
+ optionsRef.current = {
66
+ stops,
67
+ current,
291
68
  count,
292
- axis,
293
- placement,
294
- scrollerRef.current
295
- ]);
296
- const follower = useMemo(() => {
297
- const scroller = scrollerRef.current;
298
- if (!scroller || axis !== "block") return;
299
- return new ScrollFollower(scroller, {
300
- maxSpeed: 8,
301
- acceleration: 32,
302
- deceleration: 10,
303
- trailing: () => scroller.scrollHeight - scroller.clientHeight - endTarget(),
304
- rowHeight: () => {
305
- const { first, last } = placement.layout();
306
- const sizes = [];
307
- for (let index = first; index <= last; index++) sizes.push(placement.extentOf(index));
308
- sizes.sort((a, b) => a - b);
309
- const median = sizes.length ? sizes[Math.floor(sizes.length / 2)] : 100;
310
- return Math.min(median, scroller.clientHeight || median);
311
- }
312
- });
313
- }, [
314
- scrollerRef.current,
315
- axis,
316
- placement,
317
- endTarget
318
- ]);
319
- useLayoutEffect(() => () => follower?.cancel(), [follower]);
320
- const lastOffset = useRef(0);
321
- const gestureAt = useRef(0);
322
- useLayoutEffect(() => {
323
- const scroller = scrollerRef.current;
324
- if (!scroller || !enabled) return;
325
- const onGesture = () => {
326
- gestureAt.current = performance.now();
327
- };
328
- const onScroll = () => {
329
- const current = axis === "block" ? scroller.scrollTop : scroller.scrollLeft;
330
- const viewport = axis === "block" ? scroller.clientHeight : scroller.clientWidth;
331
- const total = axis === "block" ? scroller.scrollHeight : scroller.scrollWidth;
332
- const back = current < lastOffset.current - 1;
333
- lastOffset.current = current;
334
- if (total - reserve - current - viewport <= STICKY_THRESHOLD) following.current = true;
335
- else if (back && performance.now() - gestureAt.current < GESTURE_WINDOW) {
336
- following.current = false;
337
- follower?.cancel();
338
- }
339
- };
340
- scroller.addEventListener("scroll", onScroll, { passive: true });
341
- for (const event of [
342
- "wheel",
343
- "touchmove",
344
- "keydown",
345
- "pointerdown"
346
- ]) scroller.addEventListener(event, onGesture, { passive: true });
347
- return () => {
348
- scroller.removeEventListener("scroll", onScroll);
349
- for (const event of [
350
- "wheel",
351
- "touchmove",
352
- "keydown",
353
- "pointerdown"
354
- ]) scroller.removeEventListener(event, onGesture);
69
+ onNavigate
70
+ };
71
+ const jumpTo = useCallback((index, behavior = "auto") => {
72
+ const { count, onNavigate } = optionsRef.current;
73
+ pending.current = {
74
+ index,
75
+ at: performance.now()
355
76
  };
356
- }, [
357
- scrollerRef.current,
358
- enabled,
359
- axis,
360
- reserve,
361
- follower
362
- ]);
363
- const positioned = useRef(false);
364
- useLayoutEffect(() => {
365
- const scroller = scrollerRef.current;
366
- if (!enabled || !following.current || !count || !scroller) return;
367
- const target = endTarget();
368
- const gap = target - (axis === "block" ? scroller.scrollTop : scroller.scrollLeft);
369
- if (Math.abs(gap) <= 1) {
370
- positioned.current = true;
371
- return;
372
- }
373
- const viewport = axis === "block" ? scroller.clientHeight : scroller.clientWidth;
374
- if (positioned.current && glide && follower && gap > 0 && gap <= viewport) {
375
- follower.start();
376
- return;
377
- }
378
- follower?.cancel();
379
- placement.scrollTo(target);
380
- lastOffset.current = target;
381
- if (axis === "block") scroller.scrollTop = target;
382
- else scroller.scrollLeft = target;
383
- }, [
384
- enabled,
385
- count,
386
- extent,
387
- placement,
388
- axis,
389
- glide,
390
- follower,
391
- endTarget,
392
- scrollerRef.current
77
+ onNavigate?.(index);
78
+ const align = index >= count() - 1 ? "end" : "start";
79
+ controller.current?.scrollToIndex(index, align, behavior);
80
+ }, [controller]);
81
+ const step = useCallback((delta) => {
82
+ const { stops, current } = optionsRef.current;
83
+ const all = stops();
84
+ if (!all.length) return;
85
+ const flight = pending.current;
86
+ const at = flight && performance.now() - flight.at < PENDING_WINDOW && flight.index !== current() ? flight.index : current();
87
+ const next = delta > 0 ? all.find(({ index }) => index > at) ?? all[all.length - 1] : [...all].reverse().find(({ index }) => index < at) ?? all[0];
88
+ jumpTo(next.index, "smooth");
89
+ }, [jumpTo]);
90
+ const first = useCallback(() => jumpTo(0), [jumpTo]);
91
+ const last = useCallback(() => {
92
+ const { count } = optionsRef.current;
93
+ jumpTo(count() - 1);
94
+ }, [jumpTo]);
95
+ return useMemo(() => ({
96
+ step,
97
+ jumpTo,
98
+ first,
99
+ last
100
+ }), [
101
+ step,
102
+ jumpTo,
103
+ first,
104
+ last
393
105
  ]);
394
- return { onNavigate: useCallback((index) => {
395
- const wants = index >= count - 1;
396
- following.current = !!enabled && wants;
397
- if (!wants) follower?.cancel();
398
- }, [
399
- enabled,
400
- count,
401
- follower
402
- ]) };
403
106
  };
404
107
  //#endregion
405
108
  //#region src/hooks/useItemSelection.tsx
@@ -614,7 +317,7 @@ var build = (registry, editable, themeMode) => [
614
317
  * Each item owns a document, so streaming appends and per-message editing stay local — where a
615
318
  * single thread-wide document needs a cursor and a range table to know which message it is touching.
616
319
  */
617
- var MarkdownBlock = memo(({ text, editable = false, registry, hits, onWidgetsChange }) => {
320
+ var MarkdownBlock = memo(({ text, stream, editable = false, registry, hits, onWidgetsChange }) => {
618
321
  const { themeMode } = useThemeContext();
619
322
  const [view, setView] = useState(null);
620
323
  const [widgets, setWidgets] = useState([]);
@@ -653,21 +356,74 @@ var MarkdownBlock = memo(({ text, editable = false, registry, hits, onWidgetsCha
653
356
  setView(null);
654
357
  };
655
358
  }, [extensions]);
656
- const currentRef = useRef(text);
359
+ const targetRef = useRef(text);
360
+ const dispatchedRef = useRef(text);
361
+ const dripRaf = useRef(0);
657
362
  useEffect(() => {
658
- if (!view || currentRef.current === text) return;
659
- const previous = currentRef.current;
660
- currentRef.current = text;
363
+ if (!view) return;
364
+ targetRef.current = text;
661
365
  initialTextRef.current = text;
662
- view.dispatch(text.startsWith(previous) ? { changes: {
663
- from: previous.length,
664
- insert: text.slice(previous.length)
665
- } } : { changes: {
666
- from: 0,
667
- to: view.state.doc.length,
668
- insert: text
669
- } });
670
- }, [view, text]);
366
+ if (dispatchedRef.current === text) return;
367
+ if (!text.startsWith(dispatchedRef.current)) {
368
+ cancelAnimationFrame(dripRaf.current);
369
+ dripRaf.current = 0;
370
+ dispatchedRef.current = text;
371
+ view.dispatch({ changes: {
372
+ from: 0,
373
+ to: view.state.doc.length,
374
+ insert: text
375
+ } });
376
+ return;
377
+ }
378
+ if (!stream) {
379
+ cancelAnimationFrame(dripRaf.current);
380
+ dripRaf.current = 0;
381
+ const from = dispatchedRef.current.length;
382
+ dispatchedRef.current = text;
383
+ view.dispatch({ changes: {
384
+ from,
385
+ insert: text.slice(from)
386
+ } });
387
+ return;
388
+ }
389
+ if (dripRaf.current) return;
390
+ const tick = () => {
391
+ const target = targetRef.current;
392
+ const dispatched = dispatchedRef.current;
393
+ if (!target.startsWith(dispatched)) {
394
+ dripRaf.current = 0;
395
+ return;
396
+ }
397
+ const backlog = target.slice(dispatched.length);
398
+ if (!backlog.length) {
399
+ dripRaf.current = 0;
400
+ return;
401
+ }
402
+ let take = Math.max(2, Math.ceil(backlog.length / 30));
403
+ if (backlog[0] === "<") {
404
+ const close = backlog.indexOf(">");
405
+ take = close === -1 ? 0 : Math.max(take, close + 1);
406
+ } else {
407
+ const tag = backlog.indexOf("<");
408
+ if (tag !== -1) take = Math.min(take, tag);
409
+ }
410
+ if (take > 0) {
411
+ const insert = backlog.slice(0, take);
412
+ dispatchedRef.current = dispatched + insert;
413
+ view.dispatch({ changes: {
414
+ from: dispatched.length,
415
+ insert
416
+ } });
417
+ }
418
+ dripRaf.current = requestAnimationFrame(tick);
419
+ };
420
+ dripRaf.current = requestAnimationFrame(tick);
421
+ }, [
422
+ view,
423
+ text,
424
+ stream
425
+ ]);
426
+ useEffect(() => () => cancelAnimationFrame(dripRaf.current), []);
671
427
  const highlighted = useRef(false);
672
428
  useEffect(() => {
673
429
  if (!view || !hits?.length && !highlighted.current) return;
@@ -751,6 +507,12 @@ var tag = (name, content, block, attributes) => {
751
507
  return block.pending ? `${open}${text}` : `${open}${text}</${name}>`;
752
508
  };
753
509
  var escapeXml = (raw) => raw.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
510
+ /**
511
+ * Whether a message is the reader's own prompt. Role alone cannot say: tool results travel back as
512
+ * `user`-role messages (the AI protocol convention), and a synthetic turn is system-generated input
513
+ * — neither is the reader speaking, so neither gets the prompt frame or counts as a stop.
514
+ */
515
+ var isPrompt = (message) => message.sender.role === "user" && message.blocks.some((block) => block._tag === "text" && block.disposition !== "synthetic");
754
516
  /** Plain text of a message, used by search and copy so both read the same projection. */
755
517
  var messageText = (message, renderer) => {
756
518
  const content = renderer(message);
@@ -810,107 +572,6 @@ var sliceFeed = (messages, renderer, range) => {
810
572
  return parts.join("\n\n");
811
573
  };
812
574
  //#endregion
813
- //#region src/model/list-model.ts
814
- /**
815
- * The low-level reactive collection the virtualizer binds to (SPEC: ListModel).
816
- *
817
- * Two faces on one state, deliberately: a plain synchronous face (`count`, `at`, `subscribe`) that
818
- * the virtualizer consumes without dragging an atom registry into every story and test, and an
819
- * atom face (`rowsAtom`) for hosts that live in atom-land — derivations (stops, decorations,
820
- * readouts) hang off the atom rather than becoming parallel props. Both are the same array; the
821
- * subscribe face is not a fallback but the binding's contract.
822
- */
823
- var ListModel = class {
824
- #items;
825
- #getId;
826
- #listeners = /* @__PURE__ */ new Set();
827
- /** The rows as an atom, for derivations. Registry-managed by whoever consumes it. */
828
- rowsAtom;
829
- #registry;
830
- constructor({ items = [], getId }) {
831
- this.#items = [...items];
832
- this.#getId = getId;
833
- this.rowsAtom = Atom.make(this.#items);
834
- this.#registry = Registry.make();
835
- }
836
- get count() {
837
- return this.#items.length;
838
- }
839
- get items() {
840
- return this.#items;
841
- }
842
- getId = (index) => {
843
- const item = this.#items[index];
844
- return item === void 0 ? `missing-${index}` : this.#getId(item);
845
- };
846
- at = (index) => this.#items[index];
847
- indexOf(id) {
848
- return this.#items.findIndex((item) => this.#getId(item) === id);
849
- }
850
- subscribe(listener) {
851
- this.#listeners.add(listener);
852
- return () => this.#listeners.delete(listener);
853
- }
854
- append(items) {
855
- if (!items.length) return;
856
- this.#items = [...this.#items, ...items];
857
- this.#publish({ appended: items.length });
858
- }
859
- prepend(items) {
860
- if (!items.length) return;
861
- this.#items = [...items, ...this.#items];
862
- this.#publish({ prepended: items.length });
863
- }
864
- /** An item changed in place — same identity, new content. */
865
- update(id) {
866
- this.#publish({ updated: [id] });
867
- }
868
- /**
869
- * Replace one item with a successor of the same identity — the streaming case for *frozen*
870
- * items. An ECHO object mutates and `update` announces it; a schema-made message is frozen, so
871
- * the chunk arrives as a fresh value under the old id. Same event either way: `updated`.
872
- */
873
- patch(id, next) {
874
- const index = this.#items.findIndex((item) => this.#getId(item) === id);
875
- if (index < 0 || this.#getId(next) !== id) return;
876
- this.#items = [
877
- ...this.#items.slice(0, index),
878
- next,
879
- ...this.#items.slice(index + 1)
880
- ];
881
- this.#publish({ updated: [id] });
882
- }
883
- /**
884
- * The adapter for hosts that hand over a whole new array (a React prop, a query result).
885
- *
886
- * The one place the prepend inference survives, because this is the one caller that genuinely
887
- * does not know what changed: the old first item is found in the new array, and everything before
888
- * it was prepended. The virtualizer itself is always told (SPEC F-7.1).
889
- */
890
- replace(items) {
891
- const previous = this.#items;
892
- const previousFirst = previous.length ? this.#getId(previous[0]) : void 0;
893
- this.#items = [...items];
894
- let prepended = 0;
895
- if (previousFirst !== void 0 && items.length > previous.length) {
896
- const grew = items.length - previous.length;
897
- for (let index = 1; index <= grew; index++) if (items[index] !== void 0 && this.#getId(items[index]) === previousFirst) {
898
- prepended = index;
899
- break;
900
- }
901
- }
902
- const appended = prepended ? 0 : Math.max(0, items.length - previous.length);
903
- this.#publish({
904
- prepended: prepended || void 0,
905
- appended: appended || void 0
906
- });
907
- }
908
- #publish(change) {
909
- this.#registry.set(this.rowsAtom, this.#items);
910
- for (const listener of this.#listeners) listener(change);
911
- }
912
- };
913
- //#endregion
914
575
  //#region src/model/feed.ts
915
576
  /**
916
577
  * The native case, not an add-on (SPEC: FeedModel): a ListModel of messages carrying the feed's
@@ -944,6 +605,7 @@ var FeedModel = class extends ListModel {
944
605
  }
945
606
  setStreaming(id) {
946
607
  const previous = this.#streamingId;
608
+ if (previous === id) return;
947
609
  this.#streamingId = id;
948
610
  for (const changed of [previous, id]) if (changed) this.update(changed);
949
611
  }
@@ -961,7 +623,7 @@ var FeedModel = class extends ListModel {
961
623
  * knows when it changed — callers that want reactivity derive from `rowsAtom`.
962
624
  */
963
625
  stops() {
964
- const predicate = this.#stops === "message" ? () => true : this.#stops === "prompt" ? (message) => message.sender.role === "user" : this.#stops;
626
+ const predicate = this.#stops === "message" ? () => true : this.#stops === "prompt" ? isPrompt : this.#stops;
965
627
  const stops = [];
966
628
  this.messages.forEach((message, index) => {
967
629
  if (predicate(message, index)) stops.push({
@@ -1011,26 +673,13 @@ var fromMessages = (messages, options = {}) => new FeedModel({
1011
673
  //#endregion
1012
674
  //#region src/model/useListModel.ts
1013
675
  /**
1014
- * A model over an array the host re-creates per render — the React-prop case.
676
+ * A feed model over a message array the host re-creates per render — the React-prop case.
1015
677
  *
1016
678
  * The model is stable; the array is folded into it with `replace`, which is where the prepend
1017
679
  * inference lives (and the only place — SPEC F-7.1). In an effect rather than during render: the
1018
680
  * model publishes synchronously to its subscribers, and a subscriber invalidating itself while the
1019
681
  * host is still rendering is a cross-component update React rejects. The window it opens is one
1020
682
  * frame, and the window's own subscription closes it.
1021
- */
1022
- var useListModel = (items, getId) => {
1023
- const model = useMemo(() => new ListModel({
1024
- items,
1025
- getId
1026
- }), []);
1027
- useEffect(() => {
1028
- if (model.items !== items) model.replace(items);
1029
- }, [model, items]);
1030
- return model;
1031
- };
1032
- /**
1033
- * The feed-shaped twin, for a host that re-creates its message array per render.
1034
683
  *
1035
684
  * Streaming through this adapter is a `replace` whose length does not change; the model still
1036
685
  * publishes, so the window still re-renders, and the streaming item reconciles by document delta
@@ -1047,514 +696,6 @@ var useFeedModel = (messages, options = {}) => {
1047
696
  return model;
1048
697
  };
1049
698
  //#endregion
1050
- //#region src/virtualizer/placement.ts
1051
- var DEFAULT_OVERSCAN = 8;
1052
- /**
1053
- *
1054
- */
1055
- var Placement = class {
1056
- #count;
1057
- #getId;
1058
- #extents;
1059
- #viewport;
1060
- #overscan;
1061
- #reserve;
1062
- /** Measured extents by message id, so that reordering the model cannot invalidate them. */
1063
- #measured = /* @__PURE__ */ new Map();
1064
- /** The one authoritative position: this row starts exactly here. */
1065
- #anchor;
1066
- #scroll = 0;
1067
- constructor({ count, getId, extents, viewport, overscan = DEFAULT_OVERSCAN, reserve = 0 }) {
1068
- this.#count = count;
1069
- this.#getId = getId;
1070
- this.#extents = extents;
1071
- this.#viewport = viewport;
1072
- this.#overscan = overscan;
1073
- this.#reserve = reserve;
1074
- this.#anchor = {
1075
- id: count ? getId(0) : "",
1076
- index: 0,
1077
- start: 0
1078
- };
1079
- }
1080
- get scroll() {
1081
- return this.#scroll;
1082
- }
1083
- get count() {
1084
- return this.#count;
1085
- }
1086
- /** The viewport is the binding's to know: it is the one thing here that comes from the DOM. */
1087
- setViewport(viewport) {
1088
- this.#viewport = viewport;
1089
- }
1090
- /**
1091
- * The host's answer about extents can change — a row is edited, a panel opens, a story flips a
1092
- * fixture — and it is a *function*, so keeping the one handed over at construction quietly pins
1093
- * the layout to whatever was true then. Measured extents survive: they are keyed by id and this
1094
- * only replaces what the host would say about a row nobody has measured.
1095
- */
1096
- setExtents(extents) {
1097
- this.#extents = extents;
1098
- }
1099
- setReserve(reserve) {
1100
- this.#reserve = reserve;
1101
- }
1102
- /**
1103
- * The host's identity function, which is a closure over the host's own list.
1104
- *
1105
- * Appending a message gives a *new* `getId`, and a placement holding the one it was constructed
1106
- * with resolves the new row's index to an id nobody will ever measure. The binding then stores
1107
- * that row's measurement under the id the DOM carries and reads it back under the stale one, so
1108
- * the row is measured, found to disagree, measured again — every commit, for ever. It presents as
1109
- * a render loop rather than as a wrong size, which is what made `bridge/Tail` look like a tail
1110
- * defect (§8: measurement is only a correction if it can be read back).
1111
- */
1112
- setGetId(getId) {
1113
- this.#getId = getId;
1114
- }
1115
- get anchor() {
1116
- return this.#anchor;
1117
- }
1118
- /** Extent of one row: what was measured, or what the host says. */
1119
- extentOf(index) {
1120
- return this.#measured.get(this.#getId(index)) ?? this.#extents.of(index);
1121
- }
1122
- /**
1123
- * Absolute position of a row, derived from the anchor rather than summed from the start.
1124
- *
1125
- * Rows between here and the anchor are the only ones whose extents matter (§5): everything before
1126
- * the window is described by the anchor's position, not by adding up guesses about it.
1127
- */
1128
- positionOf(index) {
1129
- let position = this.#anchor.start;
1130
- if (index >= this.#anchor.index) for (let row = this.#anchor.index; row < index; row++) position += this.extentOf(row);
1131
- else for (let row = index; row < this.#anchor.index; row++) position -= this.extentOf(row);
1132
- return position;
1133
- }
1134
- /** The reader moved. Nothing is corrected here — only the mounted range changes. */
1135
- scrollTo(offset) {
1136
- this.#scroll = offset;
1137
- this.#reanchor();
1138
- }
1139
- /**
1140
- * A row turned out to be a different size than assumed.
1141
- *
1142
- * Rows at or after the anchor push the ones after them and leave the anchor alone. A row *before*
1143
- * the anchor moves the rows between it and the anchor, which is correct and unavoidable: the
1144
- * anchor is the thing being held still, and it is still being held.
1145
- */
1146
- measure(id, extent) {
1147
- if (this.#extents.exact) return;
1148
- this.#measured.set(id, extent);
1149
- }
1150
- /** The model changed length. `prepended` rows arrived before the anchor and shift its index. */
1151
- setCount(count, { prepended = 0 } = {}) {
1152
- this.#count = count;
1153
- if (prepended) this.#anchor = {
1154
- ...this.#anchor,
1155
- index: this.#anchor.index + prepended
1156
- };
1157
- }
1158
- /**
1159
- * Move to a row that may be nowhere near the window.
1160
- *
1161
- * The only operation that positions from estimates rather than from measurement, because there is
1162
- * no measured path to somewhere the reader has never been. A jump is a discontinuity the reader
1163
- * asked for, so being approximately right is what it can be.
1164
- */
1165
- jumpTo(index, align = "start") {
1166
- const clamped = Math.max(0, Math.min(index, this.#count - 1));
1167
- let start = 0;
1168
- for (let row = 0; row < clamped; row++) start += this.extentOf(row);
1169
- this.#anchor = {
1170
- id: this.#getId(clamped),
1171
- index: clamped,
1172
- start
1173
- };
1174
- const end = clamped === this.#count - 1 ? this.layout().sizerExtent - this.#reserve : start + this.extentOf(clamped);
1175
- this.#scroll = align === "end" ? end - this.#viewport : start;
1176
- this.#scroll = Math.max(0, this.#scroll);
1177
- this.#reanchor();
1178
- }
1179
- /**
1180
- * Where the reader has to be for `index` to sit at the start (or end) of the viewport.
1181
- *
1182
- * Reports; does not move. `jumpTo` re-anchors as well, which is right for a discontinuity and
1183
- * wrong for a glide: the anchor would arrive at the destination while the element was still
1184
- * travelling, so the reader would watch the rows they were scrolling *through* unmount and the
1185
- * journey happen over nothing.
1186
- */
1187
- offsetOf(index, align = "start") {
1188
- const clamped = Math.max(0, Math.min(index, this.#count - 1));
1189
- const start = this.positionOf(clamped);
1190
- return Math.max(0, align === "end" ? start + this.extentOf(clamped) - this.#viewport : start);
1191
- }
1192
- /**
1193
- * Where the reader has to be for the last row to rest against the end of the viewport.
1194
- *
1195
- * Derived from the anchor, like every other position, and deliberately **not** from a sum over the
1196
- * model: a follow asks this again on every commit, and each answer mounts rows whose measurement
1197
- * replaces the estimates that sum is over — so a sum-based end moves every time it is consulted,
1198
- * and consulting it is what moves it. That is not slow convergence, it is a feedback loop, and in
1199
- * React it terminates by exceeding the update limit rather than by settling (`bridge/Tail`).
1200
- *
1201
- * Anchor-relative, the answer only changes as the rows *between the anchor and the last row* are
1202
- * measured — each of them once — so a follow settles in a couple of commits and rows the reader
1203
- * has already passed cannot disturb it.
1204
- */
1205
- endOffset() {
1206
- if (!this.#count) return 0;
1207
- const last = this.#count - 1;
1208
- return Math.max(0, this.positionOf(last) + this.extentOf(last) - this.#viewport);
1209
- }
1210
- /** The mounted range, the parent's offset, and the extent the thumb is computed from. */
1211
- layout() {
1212
- if (!this.#count) return {
1213
- first: 0,
1214
- last: -1,
1215
- visible: {
1216
- first: 0,
1217
- last: -1
1218
- },
1219
- offset: 0,
1220
- sizerExtent: 0
1221
- };
1222
- const { first, last, visible } = this.#range();
1223
- const offset = this.positionOf(first);
1224
- let windowExtent = 0;
1225
- for (let row = first; row <= last; row++) windowExtent += this.extentOf(row);
1226
- let after = 0;
1227
- for (let row = last + 1; row < this.#count; row++) after += this.extentOf(row);
1228
- return {
1229
- first,
1230
- last,
1231
- visible,
1232
- offset,
1233
- sizerExtent: offset + windowExtent + after + this.#reserve
1234
- };
1235
- }
1236
- /**
1237
- * Repay the start edge's drift: shift every position so row 0 starts at exactly zero, and move
1238
- * the scroll by the same amount so nothing the reader sees moves.
1239
- *
1240
- * The first row starting at zero is the one absolute fact of content space, and estimates being
1241
- * replaced above the anchor break it: a feed opened at its tail whose rows measure taller than
1242
- * assumed pushes row 0 *negative*, where no scroll can reach it — the reader arrives at the top
1243
- * and the first messages simply are not there. This is the single correction that must touch the
1244
- * scroll (§7's rule is about corrections and readers sharing a channel; here both the content and
1245
- * the offset move by the same delta in the same commit, so the reader sees nothing at all).
1246
- * Returns the shift for the binding to apply to the element.
1247
- */
1248
- rebaseStart() {
1249
- if (!this.#count) return 0;
1250
- const shift = -this.positionOf(0);
1251
- if (shift === 0) return 0;
1252
- this.#anchor = {
1253
- ...this.#anchor,
1254
- start: this.#anchor.start + shift
1255
- };
1256
- this.#scroll = Math.max(0, this.#scroll + shift);
1257
- return shift;
1258
- }
1259
- /**
1260
- * What an edge revealed, if the window has reached one.
1261
- *
1262
- * The edges are where estimate meets ground truth: the first row starts at zero and the last one
1263
- * ends at the content's end, by definition, so a computed position that says otherwise is the
1264
- * measure of how wrong the estimates were. Reported rather than silently applied — absorbing it
1265
- * into the anchor would move what the reader is looking at, and at an edge they are looking at
1266
- * exactly the thing that would move.
1267
- */
1268
- drift() {
1269
- if (!this.#count) return;
1270
- const { first, last } = this.#range();
1271
- if (first === 0 && this.positionOf(0) !== 0) return {
1272
- edge: "start",
1273
- delta: this.positionOf(0)
1274
- };
1275
- if (last === this.#count - 1) {
1276
- const end = this.positionOf(last) + this.extentOf(last);
1277
- const { sizerExtent } = this.layout();
1278
- const content = sizerExtent - this.#reserve;
1279
- if (end !== content) return {
1280
- edge: "end",
1281
- delta: content - end
1282
- };
1283
- }
1284
- }
1285
- /** Visible rows, plus overscan, clamped to the model. */
1286
- #range() {
1287
- let first = this.#anchor.index;
1288
- while (first > 0 && this.positionOf(first) > this.#scroll) first--;
1289
- while (first < this.#count - 1 && this.positionOf(first) + this.extentOf(first) <= this.#scroll) first++;
1290
- let last = first;
1291
- let filled = this.positionOf(first) + this.extentOf(first);
1292
- while (last < this.#count - 1 && filled < this.#scroll + this.#viewport) {
1293
- last++;
1294
- filled += this.extentOf(last);
1295
- }
1296
- return {
1297
- first: Math.max(0, first - this.#overscan),
1298
- last: Math.min(this.#count - 1, last + this.#overscan),
1299
- visible: {
1300
- first,
1301
- last
1302
- }
1303
- };
1304
- }
1305
- /**
1306
- * The anchor is the first row the reader can see — always, not merely when the old one leaves
1307
- * the window.
1308
- *
1309
- * The anchor is the row held still while others are measured, so it must be the reader's
1310
- * reference point. Clamping the old anchor into the mounted range let it drift to the window's
1311
- * *far* edge after an upward scroll — below everything on screen — and a widget toggled open
1312
- * above it then pushed the whole viewport up by its own growth: the engine held a row the reader
1313
- * could not see still, at the expense of every row they could. Re-anchoring preserves the new
1314
- * anchor's computed position exactly, so the move itself shifts nothing.
1315
- */
1316
- #reanchor() {
1317
- if (!this.#count) return;
1318
- const { visible } = this.#range();
1319
- if (this.#anchor.index === visible.first) return;
1320
- const index = visible.first;
1321
- this.#anchor = {
1322
- id: this.#getId(index),
1323
- index,
1324
- start: this.positionOf(index)
1325
- };
1326
- }
1327
- };
1328
- //#endregion
1329
- //#region src/virtualizer/Window.tsx
1330
- /** Extent of the mounted rows, which is the parent's own size in content space. */
1331
- var windowExtentOf = (placement, first, last) => {
1332
- let extent = 0;
1333
- for (let index = first; index <= last; index++) extent += placement.extentOf(index);
1334
- return extent;
1335
- };
1336
- var useWindow = ({ scrollerRef, model, extents, axis = "block", overscan, reserve = 0, onEdge, onMismatch, onChange, controllerRef }) => {
1337
- const windowRef = useRef(null);
1338
- const [, render] = useState(0);
1339
- const invalidate = useCallback(() => render((value) => value + 1), []);
1340
- const count = model.count;
1341
- const getId = model.getId;
1342
- const placement = useMemo(() => new Placement({
1343
- count,
1344
- getId,
1345
- extents,
1346
- viewport: 0,
1347
- overscan
1348
- }), []);
1349
- const reported = useRef(/* @__PURE__ */ new Set());
1350
- placement.setGetId(getId);
1351
- useEffect(() => {
1352
- const unsubscribe = model.subscribe((change) => {
1353
- placement.setCount(model.count, { prepended: change.prepended ?? 0 });
1354
- invalidate();
1355
- });
1356
- placement.setCount(model.count, {});
1357
- invalidate();
1358
- return unsubscribe;
1359
- }, [
1360
- model,
1361
- placement,
1362
- invalidate
1363
- ]);
1364
- if (placement.count !== model.count) placement.setCount(model.count, {});
1365
- useImperativeHandle(controllerRef, () => ({ scrollToIndex: (index, align, behavior = "auto") => {
1366
- const scroller = scrollerRef.current;
1367
- const { first, last } = placement.layout();
1368
- if (behavior === "smooth" && scroller && index >= first && index <= last) {
1369
- const target = placement.offsetOf(index, align);
1370
- scroller.scrollTo(axis === "block" ? {
1371
- top: target,
1372
- behavior
1373
- } : {
1374
- left: target,
1375
- behavior
1376
- });
1377
- return;
1378
- }
1379
- placement.jumpTo(index, align);
1380
- if (scroller) {
1381
- if (axis === "block") scroller.scrollTop = placement.scroll;
1382
- else scroller.scrollLeft = placement.scroll;
1383
- }
1384
- invalidate();
1385
- } }), [
1386
- placement,
1387
- axis,
1388
- invalidate
1389
- ]);
1390
- placement.setExtents(extents);
1391
- placement.setReserve(reserve);
1392
- const { first, last, visible, offset, sizerExtent } = placement.layout();
1393
- useLayoutEffect(() => {
1394
- const scroller = scrollerRef.current;
1395
- if (!scroller) return;
1396
- const read = () => axis === "block" ? scroller.clientHeight : scroller.clientWidth;
1397
- placement.setViewport(read());
1398
- const observer = new ResizeObserver(() => {
1399
- placement.setViewport(read());
1400
- invalidate();
1401
- });
1402
- observer.observe(scroller);
1403
- const onScroll = () => {
1404
- placement.scrollTo(axis === "block" ? scroller.scrollTop : scroller.scrollLeft);
1405
- invalidate();
1406
- };
1407
- scroller.addEventListener("scroll", onScroll, { passive: true });
1408
- invalidate();
1409
- return () => {
1410
- observer.disconnect();
1411
- scroller.removeEventListener("scroll", onScroll);
1412
- };
1413
- }, [
1414
- placement,
1415
- axis,
1416
- invalidate
1417
- ]);
1418
- useLayoutEffect(() => {
1419
- const parent = windowRef.current;
1420
- if (!parent) return;
1421
- let changed = false;
1422
- for (const element of parent.children) {
1423
- const row = element;
1424
- const index = Number(row.dataset.index);
1425
- const id = row.dataset.objectId;
1426
- const actual = Math.round(axis === "block" ? row.offsetHeight : row.offsetWidth);
1427
- const declared = placement.extentOf(index);
1428
- if (extents.exact) {
1429
- const key = `${id}:${declared}:${actual}`;
1430
- if (actual !== declared && !reported.current.has(key)) {
1431
- reported.current.add(key);
1432
- onMismatch?.({
1433
- index,
1434
- id,
1435
- declared,
1436
- actual
1437
- });
1438
- }
1439
- continue;
1440
- }
1441
- if (actual && actual !== declared) {
1442
- placement.measure(id, actual);
1443
- changed = true;
1444
- }
1445
- }
1446
- if (placement.rebaseStart() !== 0) {
1447
- const scroller = scrollerRef.current;
1448
- if (scroller) {
1449
- if (axis === "block") scroller.scrollTop = placement.scroll;
1450
- else scroller.scrollLeft = placement.scroll;
1451
- }
1452
- changed = true;
1453
- }
1454
- const drift = placement.drift();
1455
- const key = drift && `${drift.edge}:${drift.delta}`;
1456
- if (drift && key && !reported.current.has(key)) {
1457
- reported.current.add(key);
1458
- onEdge?.(drift);
1459
- }
1460
- if (changed) invalidate();
1461
- });
1462
- const announced = useRef("");
1463
- const state = `${visible.first}:${visible.last}:${first}:${last}:${count}`;
1464
- if (onChange && announced.current !== state) {
1465
- announced.current = state;
1466
- const scroller = scrollerRef.current;
1467
- const viewport = (axis === "block" ? scroller?.clientHeight : scroller?.clientWidth) ?? 0;
1468
- const extent = windowExtentOf(placement, first, last);
1469
- queueMicrotask(() => onChange({
1470
- index: visible.first,
1471
- visible,
1472
- mounted: {
1473
- first,
1474
- last
1475
- },
1476
- count,
1477
- geometry: {
1478
- scroll: placement.scroll,
1479
- viewport,
1480
- total: sizerExtent,
1481
- window: {
1482
- start: offset,
1483
- extent
1484
- }
1485
- }
1486
- }));
1487
- }
1488
- return {
1489
- placement,
1490
- layout: {
1491
- first,
1492
- last,
1493
- visible,
1494
- offset,
1495
- sizerExtent
1496
- },
1497
- windowRef,
1498
- sizerExtent,
1499
- offset,
1500
- first,
1501
- last
1502
- };
1503
- };
1504
- /**
1505
- * The DOM shape, and nothing else.
1506
- *
1507
- * Three elements: a **sizer** that holds no rows and exists only to give the thumb something to
1508
- * measure, a **window** holding the mounted rows in normal flow, and the scroller around them.
1509
- */
1510
- var Window = ({ classNames, children, controllerRef, ...options }) => {
1511
- const scrollerRef = useRef(null);
1512
- const inner = useRef(null);
1513
- const { placement, windowRef, offset, sizerExtent, first, last } = useWindow({
1514
- ...options,
1515
- scrollerRef,
1516
- controllerRef: inner
1517
- });
1518
- const axis = options.axis ?? "block";
1519
- const main = axis === "block" ? "height" : "width";
1520
- const { getId } = options.model;
1521
- const follow = useFollow({
1522
- scrollerRef,
1523
- placement,
1524
- extent: sizerExtent,
1525
- count: options.model.count,
1526
- axis,
1527
- reserve: options.reserve,
1528
- enabled: options.sticky
1529
- });
1530
- useImperativeHandle(controllerRef, () => ({ scrollToIndex: (index, align, behavior) => {
1531
- follow.onNavigate(index);
1532
- inner.current?.scrollToIndex(index, align, behavior);
1533
- } }), [follow]);
1534
- const rows = [];
1535
- for (let index = first; index <= last; index++) rows.push(/* @__PURE__ */ jsx("div", {
1536
- "data-index": index,
1537
- "data-object-id": getId(index),
1538
- children: children(index, getId(index))
1539
- }, getId(index)));
1540
- return /* @__PURE__ */ jsxs("div", {
1541
- ref: scrollerRef,
1542
- className: mx("relative", axis === "block" ? "overflow-y-auto" : "overflow-x-auto", classNames),
1543
- style: { overflowAnchor: "none" },
1544
- "data-testid": "window.scroller",
1545
- children: [/* @__PURE__ */ jsx("div", {
1546
- style: { [main]: sizerExtent },
1547
- "data-testid": "window.sizer"
1548
- }), /* @__PURE__ */ jsx("div", {
1549
- ref: windowRef,
1550
- className: mx("absolute top-0 left-0 flex", axis === "block" ? "flex-col w-full" : "flex-row h-full"),
1551
- style: { transform: axis === "block" ? `translateY(${offset}px)` : `translateX(${offset}px)` },
1552
- "data-testid": "window.window",
1553
- children: rows
1554
- })]
1555
- });
1556
- };
1557
- //#endregion
1558
699
  //#region src/components/MessageList/position-log.ts
1559
700
  /** Movement below this is sub-pixel rounding, not a row changing place. */
1560
701
  var EPSILON = 1;
@@ -1723,7 +864,7 @@ var DEFAULT_ESTIMATE = 120;
1723
864
  * Renders no DOM of its own, so a toolbar or statusbar that reads its state can sit outside the
1724
865
  * scroll container — the scrolling part is `MessageList.Viewport`.
1725
866
  */
1726
- var MessageListRoot = ({ children, model, renderer = defaultRenderer, registry, Chrome = DefaultChrome, Custom, debug, estimateSize = DEFAULT_ESTIMATE, stickyBottom = false, scrollPastEnd = false, overscan = 8, onRangeChange }) => {
867
+ var MessageListRoot = ({ children, model, renderer = defaultRenderer, registry, Chrome = DefaultChrome, Custom, debug, estimateSize = DEFAULT_ESTIMATE, stickyBottom = false, tailLines = 0, overscan = 8, onRangeChange, controllerRef }) => {
1727
868
  const widgetStore = useMemo(createWidgetStateStore, []);
1728
869
  const [viewport, setViewportState] = useState(null);
1729
870
  const scrollerRef = useRef(null);
@@ -1755,46 +896,53 @@ var MessageListRoot = ({ children, model, renderer = defaultRenderer, registry,
1755
896
  estimateSize,
1756
897
  nominalSize
1757
898
  ]);
1758
- const reserve = useMemo(() => scrollPastEnd && viewport && model.count ? Math.max(0, viewport.clientHeight - nominalSize) : 0, [
1759
- scrollPastEnd,
1760
- viewport,
1761
- model.count,
1762
- nominalSize
1763
- ]);
899
+ const [reserve, setReserve] = useState(0);
900
+ useEffect(() => {
901
+ if (!viewport || !tailLines) {
902
+ setReserve(0);
903
+ return;
904
+ }
905
+ const lineHeight = parseFloat(getComputedStyle(viewport).lineHeight) || 24;
906
+ setReserve(Math.round(tailLines * lineHeight));
907
+ }, [viewport, tailLines]);
1764
908
  const controller = useRef(null);
909
+ const onWindowChange = useCallback((state) => {
910
+ const next = state.count ? {
911
+ startIndex: state.visible.first,
912
+ endIndex: state.visible.last
913
+ } : void 0;
914
+ setRange(next);
915
+ setMounted(state.mounted.last - state.mounted.first + 1);
916
+ if (next) {
917
+ onRangeChange?.(next);
918
+ setCurrent(next.startIndex);
919
+ if (next.startIndex === 0) model.more("start");
920
+ if (next.endIndex >= model.count - 1) model.more("end");
921
+ }
922
+ }, [
923
+ onRangeChange,
924
+ setCurrent,
925
+ model
926
+ ]);
1765
927
  const { placement, windowRef, offset, sizerExtent, first, last } = useWindow({
1766
928
  scrollerRef,
1767
929
  model,
1768
930
  extents,
1769
931
  overscan,
1770
932
  reserve,
1771
- onChange: useCallback((state) => {
1772
- const next = state.count ? {
1773
- startIndex: state.visible.first,
1774
- endIndex: state.visible.last
1775
- } : void 0;
1776
- setRange(next);
1777
- setMounted(state.mounted.last - state.mounted.first + 1);
1778
- if (next) {
1779
- onRangeChange?.(next);
1780
- setCurrent(next.startIndex);
1781
- if (next.startIndex === 0) model.more("start");
1782
- if (next.endIndex >= model.count - 1) model.more("end");
1783
- }
1784
- }, [
1785
- onRangeChange,
1786
- setCurrent,
1787
- model
1788
- ]),
933
+ onChange: onWindowChange,
1789
934
  controllerRef: controller
1790
935
  });
936
+ const changedAtRef = useRef(performance.now());
937
+ useEffect(() => model.subscribe(() => changedAtRef.current = performance.now()), [model]);
1791
938
  const follow = useFollow({
1792
939
  scrollerRef,
1793
940
  placement,
1794
941
  extent: sizerExtent,
1795
942
  count: model.count,
1796
943
  reserve,
1797
- enabled: stickyBottom
944
+ enabled: stickyBottom,
945
+ changedAt: () => changedAtRef.current
1798
946
  });
1799
947
  const jumps = useJumpDetector(viewport, debug);
1800
948
  const { shifts, breaks, trace: shiftTrace, record: recordPositions, reset: resetShifts } = usePositionLog();
@@ -1817,14 +965,24 @@ var MessageListRoot = ({ children, model, renderer = defaultRenderer, registry,
1817
965
  ]);
1818
966
  const scrollToIndex = useCallback((index, { align = "start", behavior = "auto" } = {}) => {
1819
967
  follow.onNavigate(index);
1820
- controller.current?.scrollToIndex(index, align === "center" ? "start" : align, behavior);
968
+ controller.current?.scrollToIndex(index, align, behavior);
1821
969
  }, [follow]);
1822
970
  const scrollToBottom = useCallback((options) => {
1823
- if (model.count) scrollToIndex(model.count - 1, {
971
+ if (!model.count) return;
972
+ const scroller = scrollerRef.current;
973
+ if (scroller && scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop <= 32) {
974
+ follow.onNavigate(model.count - 1);
975
+ return;
976
+ }
977
+ scrollToIndex(model.count - 1, {
1824
978
  align: "end",
1825
979
  ...options
1826
980
  });
1827
- }, [scrollToIndex, model]);
981
+ }, [
982
+ scrollToIndex,
983
+ model,
984
+ follow
985
+ ]);
1828
986
  useEffect(() => {
1829
987
  if (viewport) viewport.__feed = {
1830
988
  placement,
@@ -1851,7 +1009,6 @@ var MessageListRoot = ({ children, model, renderer = defaultRenderer, registry,
1851
1009
  stops: () => model.stops(),
1852
1010
  current: () => currentIndexRef.current,
1853
1011
  count: () => model.count,
1854
- scrollPastEnd,
1855
1012
  onNavigate: follow.onNavigate
1856
1013
  });
1857
1014
  useEffect(() => {
@@ -1873,6 +1030,23 @@ var MessageListRoot = ({ children, model, renderer = defaultRenderer, registry,
1873
1030
  return () => viewport.removeEventListener("keydown", onKeyDown);
1874
1031
  }, [viewport, navigation]);
1875
1032
  const selectionGroup = useMemo(createSelectionGroup, []);
1033
+ useEffect(() => {
1034
+ setRef(controllerRef, {
1035
+ model,
1036
+ scrollToBottom,
1037
+ scrollToIndex,
1038
+ navigation
1039
+ });
1040
+ return () => {
1041
+ setRef(controllerRef, null);
1042
+ };
1043
+ }, [
1044
+ controllerRef,
1045
+ model,
1046
+ scrollToBottom,
1047
+ scrollToIndex,
1048
+ navigation
1049
+ ]);
1876
1050
  return /* @__PURE__ */ jsx(SelectionGroupContext.Provider, {
1877
1051
  value: selectionGroup,
1878
1052
  children: /* @__PURE__ */ jsx(WidgetStateProvider, {
@@ -1915,8 +1089,15 @@ var MESSAGE_LIST_VIEWPORT_NAME = "MessageList.Viewport";
1915
1089
  * Scrolling belongs to `ScrollArea`, which owns the overlay thumbs and the padding tokens; the
1916
1090
  * placement needs only the element being scrolled, which `ScrollArea.Viewport` publishes.
1917
1091
  */
1918
- var MessageListViewport = composable(({ autoHide, centered, native, padding, scrollbars, thin, gutter, ...props }, forwardedRef) => {
1919
- const { model, Chrome, windowRef, offset, sizerExtent, first, last, setViewport } = useMessageListContext(MESSAGE_LIST_VIEWPORT_NAME);
1092
+ /**
1093
+ * Whether a message renders to nothing under the current renderer — e.g. a stats-only turn, whose
1094
+ * block the renderer deliberately maps to no output. The message stays in the model (identity,
1095
+ * stops, search all see it); only its chrome is withheld, so the reader is not shown an empty row
1096
+ * with a toolbar.
1097
+ */
1098
+ var isEmptyContent = (content, hasCustomRenderer) => content.kind === "markdown" && !content.text.trim() || content.kind === "html" && !content.html.trim() || content.kind === "custom" && !hasCustomRenderer;
1099
+ var MessageListViewport = composable(({ autoHide, centered, native, padding, scrollbars, thin, gutter = "md", ...props }, forwardedRef) => {
1100
+ const { model, renderer, Chrome, Custom, windowRef, offset, sizerExtent, first, last, setViewport } = useMessageListContext(MESSAGE_LIST_VIEWPORT_NAME);
1920
1101
  const { selectedIds, onSelect } = useItemSelectionValue();
1921
1102
  const handleViewportRef = useCallback((element) => {
1922
1103
  setViewport(element);
@@ -1926,10 +1107,11 @@ var MessageListViewport = composable(({ autoHide, centered, native, padding, scr
1926
1107
  for (let index = first; index <= last; index++) {
1927
1108
  const message = model.at(index);
1928
1109
  if (!message) continue;
1110
+ const empty = isEmptyContent(renderer(message), Boolean(Custom));
1929
1111
  rows.push(/* @__PURE__ */ jsx("div", {
1930
1112
  "data-index": index,
1931
1113
  "data-object-id": message.id,
1932
- children: /* @__PURE__ */ jsx(Column.Root, {
1114
+ children: !empty && /* @__PURE__ */ jsx(Column.Root, {
1933
1115
  gutter,
1934
1116
  children: /* @__PURE__ */ jsx(Column.Center, { children: /* @__PURE__ */ jsx(Chrome, {
1935
1117
  message,
@@ -1956,6 +1138,7 @@ var MessageListViewport = composable(({ autoHide, centered, native, padding, scr
1956
1138
  position: "relative",
1957
1139
  overflowAnchor: "none"
1958
1140
  },
1141
+ classNames: "dx-document",
1959
1142
  ref: handleViewportRef,
1960
1143
  children: [/* @__PURE__ */ jsx("div", {
1961
1144
  style: { height: sizerExtent },
@@ -1977,7 +1160,7 @@ var MESSAGE_LIST_ITEM_NAME = "MessageList.Item";
1977
1160
  * outside the scrolling window — a pinned message, a preview — through the same path.
1978
1161
  */
1979
1162
  var MessageListItem = composable(({ message, ...props }, forwardedRef) => {
1980
- const { renderer, registry, Custom, debug, reportWidgets } = useMessageListContext(MESSAGE_LIST_ITEM_NAME);
1163
+ const { model, renderer, registry, Custom, debug, reportWidgets } = useMessageListContext(MESSAGE_LIST_ITEM_NAME);
1981
1164
  const content = renderer(message);
1982
1165
  const decorations = useDecorations(message.id);
1983
1166
  const hits = useMemo(() => decorations.length ? decorations.map(({ range }) => [range.offset, range.offset + range.length]) : void 0, [decorations]);
@@ -1990,6 +1173,7 @@ var MessageListItem = composable(({ message, ...props }, forwardedRef) => {
1990
1173
  scope: message.id,
1991
1174
  children: /* @__PURE__ */ jsx(MarkdownBlock, {
1992
1175
  text: content.text,
1176
+ stream: model.streamingId === message.id,
1993
1177
  registry,
1994
1178
  hits,
1995
1179
  onWidgetsChange: handleWidgetsChange
@@ -2004,10 +1188,76 @@ var MessageListItem = composable(({ message, ...props }, forwardedRef) => {
2004
1188
  });
2005
1189
  });
2006
1190
  MessageListItem.displayName = MESSAGE_LIST_ITEM_NAME;
1191
+ var MESSAGE_LIST_NAV_NAME = "MessageList.Nav";
1192
+ /**
1193
+ * The step and jump controls, on the same seam as the arrow keys and the rails (SPEC F-3.2), so a
1194
+ * reader who switches between them does not change places. Lives outside the viewport — Root is
1195
+ * headless precisely so chrome like this can sit in a toolbar.
1196
+ *
1197
+ * While any of its buttons has focus, ArrowUp/Down step and Meta(Ctrl)+Arrow jumps to the ends —
1198
+ * the same keymap the viewport itself carries, so focus landing on the toolbar does not change
1199
+ * what the keys mean.
1200
+ */
1201
+ var MessageListNav = composable(({ ends = true, ...props }, forwardedRef) => {
1202
+ const { navigation } = useMessageListContext(MESSAGE_LIST_NAV_NAME);
1203
+ const onKeyDown = useCallback((event) => {
1204
+ const delta = event.key === "ArrowDown" ? 1 : event.key === "ArrowUp" ? -1 : 0;
1205
+ if (!delta || event.altKey || event.shiftKey) return;
1206
+ event.preventDefault();
1207
+ if (event.metaKey || event.ctrlKey) {
1208
+ if (delta > 0) navigation.last();
1209
+ else navigation.first();
1210
+ return;
1211
+ }
1212
+ navigation.step(delta);
1213
+ }, [navigation]);
1214
+ return /* @__PURE__ */ jsxs("div", {
1215
+ role: "group",
1216
+ ...composableProps(props),
1217
+ onKeyDown,
1218
+ ref: forwardedRef,
1219
+ children: [
1220
+ ends && /* @__PURE__ */ jsx(IconButton, {
1221
+ icon: "ph--arrow-line-up--regular",
1222
+ iconOnly: true,
1223
+ label: "First message",
1224
+ variant: "ghost",
1225
+ "data-testid": "feed.nav.top",
1226
+ onClick: () => navigation.first()
1227
+ }),
1228
+ /* @__PURE__ */ jsx(IconButton, {
1229
+ icon: "ph--caret-up--regular",
1230
+ iconOnly: true,
1231
+ label: "Previous message",
1232
+ variant: "ghost",
1233
+ "data-testid": "feed.nav.back",
1234
+ onClick: () => navigation.step(-1)
1235
+ }),
1236
+ /* @__PURE__ */ jsx(IconButton, {
1237
+ icon: "ph--caret-down--regular",
1238
+ iconOnly: true,
1239
+ label: "Next message",
1240
+ variant: "ghost",
1241
+ "data-testid": "feed.nav.forward",
1242
+ onClick: () => navigation.step(1)
1243
+ }),
1244
+ ends && /* @__PURE__ */ jsx(IconButton, {
1245
+ icon: "ph--arrow-line-down--regular",
1246
+ iconOnly: true,
1247
+ label: "Last message",
1248
+ variant: "ghost",
1249
+ "data-testid": "feed.nav.bottom",
1250
+ onClick: () => navigation.last()
1251
+ })
1252
+ ]
1253
+ });
1254
+ });
1255
+ MessageListNav.displayName = MESSAGE_LIST_NAV_NAME;
2007
1256
  var MessageList = {
2008
1257
  Root: MessageListRoot,
2009
1258
  Viewport: MessageListViewport,
2010
- Item: MessageListItem
1259
+ Item: MessageListItem,
1260
+ Nav: MessageListNav
2011
1261
  };
2012
1262
  //#endregion
2013
1263
  //#region src/components/Minimap/Minimap.tsx
@@ -2223,6 +1473,6 @@ var Outline = ({ classNames, markers, visibleRange, tickSize = TICK_SIZE, width
2223
1473
  });
2224
1474
  };
2225
1475
  //#endregion
2226
- export { ItemSelectionProvider as A, useWidgetState as C, SelectionGroupContext as D, setHighlights as E, useFeedNavigation as F, DecorationsProvider as I, useDecorations as L, useItemSelection as M, useItemSelectionValue as N, createSelectionGroup as O, useFollow as P, ScrollFollower as R, createWidgetStateStore as S, highlights as T, sliceFeed as _, useJumpDetector as a, WidgetScopeProvider as b, useFeedModel as c, fromMessages as d, ListModel as f, searchFeed as g, messageText as h, useMessageList as i, isAdditive as j, useSelectionGroup as k, useListModel as l, defaultRenderer as m, Minimap as n, usePositionLog as o, chatRenderer as p, MessageList as r, Window as s, Outline as t, FeedModel as u, MarkdownBlock as v, highlightTheme as w, WidgetStateProvider as x, HtmlBlock as y, stepVelocity as z };
1476
+ export { useItemSelection as A, highlights as C, useSelectionGroup as D, createSelectionGroup as E, useFeedNavigation as M, DecorationsProvider as N, ItemSelectionProvider as O, useDecorations as P, highlightTheme as S, SelectionGroupContext as T, HtmlBlock as _, useJumpDetector as a, createWidgetStateStore as b, FeedModel as c, defaultRenderer as d, isPrompt as f, MarkdownBlock as g, sliceFeed as h, useMessageList as i, useItemSelectionValue as j, isAdditive as k, fromMessages as l, searchFeed as m, Minimap as n, usePositionLog as o, messageText as p, MessageList as r, useFeedModel as s, Outline as t, chatRenderer as u, WidgetScopeProvider as v, setHighlights as w, useWidgetState as x, WidgetStateProvider as y };
2227
1477
 
2228
1478
  //# sourceMappingURL=chunk-src.mjs.map