@assistant-ui/react 0.15.25 → 0.15.27
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.
- package/dist/hooks/useToolCallElapsed.d.ts.map +1 -1
- package/dist/hooks/useToolCallElapsed.js +3 -2
- package/dist/hooks/useToolCallElapsed.js.map +1 -1
- package/dist/index.d.ts +31 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +3 -2
- package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/mcp-apps/app-frame.d.ts.map +1 -1
- package/dist/mcp-apps/app-frame.js +58 -48
- package/dist/mcp-apps/app-frame.js.map +1 -1
- package/dist/mcp-apps/bridge.d.ts.map +1 -1
- package/dist/mcp-apps/bridge.js +28 -3
- package/dist/mcp-apps/bridge.js.map +1 -1
- package/dist/mcp-apps/csp.d.ts +4 -0
- package/dist/mcp-apps/csp.d.ts.map +1 -0
- package/dist/mcp-apps/csp.js +93 -0
- package/dist/mcp-apps/csp.js.map +1 -0
- package/dist/mcp-apps/index.d.ts +1 -1
- package/dist/mcp-apps/index.d.ts.map +1 -1
- package/dist/mcp-apps/types.d.ts +19 -1
- package/dist/mcp-apps/types.d.ts.map +1 -1
- package/dist/mcp-apps/types.js.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.d.ts +1 -0
- package/dist/primitives/assistantModal/AssistantModalRoot.d.ts.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
- package/dist/primitives/attachment.d.ts +3 -1
- package/dist/primitives/attachment.d.ts.map +1 -1
- package/dist/primitives/attachment.js.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.js +14 -12
- package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
- package/dist/primitives/composer/ComposerCancel.d.ts +5 -1
- package/dist/primitives/composer/ComposerCancel.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerCancel.js +14 -1
- package/dist/primitives/composer/ComposerCancel.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.d.ts +14 -0
- package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +4 -3
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerRoot.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerRoot.js +24 -19
- package/dist/primitives/composer/ComposerRoot.js.map +1 -1
- package/dist/primitives/composer/composerInputSelector.d.ts +3 -0
- package/dist/primitives/composer/composerInputSelector.d.ts.map +1 -0
- package/dist/primitives/composer/composerInputSelector.js +7 -0
- package/dist/primitives/composer/composerInputSelector.js.map +1 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.d.ts +2 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.d.ts.map +1 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.js +42 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.js.map +1 -0
- package/dist/primitives/composer.d.ts +35 -14
- package/dist/primitives/composer.d.ts.map +1 -1
- package/dist/primitives/composer.js.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.d.ts +8 -1
- package/dist/primitives/message/MessagePartsGrouped.d.ts.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.js +58 -278
- package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.d.ts.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +5 -2
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
- package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadViewport.js +4 -3
- package/dist/primitives/thread/ThreadViewport.js.map +1 -1
- package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js +2 -2
- package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +90 -21
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
- package/dist/sandbox-host/SandboxHost.js +3 -2
- package/dist/sandbox-host/SandboxHost.js.map +1 -1
- package/dist/unstable/useComposerInput.d.ts +9 -4
- package/dist/unstable/useComposerInput.d.ts.map +1 -1
- package/dist/unstable/useComposerInput.js +6 -4
- package/dist/unstable/useComposerInput.js.map +1 -1
- package/dist/unstable/useComposerInputHistory.d.ts +3 -2
- package/dist/unstable/useComposerInputHistory.d.ts.map +1 -1
- package/dist/unstable/useComposerInputHistory.js +2 -2
- package/dist/unstable/useComposerInputHistory.js.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.d.ts +3 -2
- package/dist/unstable/useLiveCompletionAdapter.d.ts.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.js +8 -7
- package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
- package/dist/unstable/useMentionAdapter.d.ts +12 -3
- package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
- package/dist/unstable/useMentionAdapter.js +2 -2
- package/dist/unstable/useMentionAdapter.js.map +1 -1
- package/dist/unstable/useMessageStallDetection.d.ts +4 -2
- package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
- package/dist/unstable/useMessageStallDetection.js +2 -2
- package/dist/unstable/useMessageStallDetection.js.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.d.ts +5 -2
- package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.js +5 -4
- package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts +4 -0
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -1
- package/dist/unstable/webmcp/useWebMcpProvider.js +2 -0
- package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -1
- package/dist/utils/getSelectionMessageId.d.ts.map +1 -1
- package/dist/utils/getSelectionMessageId.js +31 -8
- package/dist/utils/getSelectionMessageId.js.map +1 -1
- package/dist/utils/hooks/useOnResizeContent.d.ts.map +1 -1
- package/dist/utils/hooks/useOnResizeContent.js +2 -22
- package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
- package/dist/utils/useIsomorphicLayoutEffect.d.ts +3 -0
- package/dist/utils/useIsomorphicLayoutEffect.d.ts.map +1 -0
- package/dist/utils/useIsomorphicLayoutEffect.js +6 -0
- package/dist/utils/useIsomorphicLayoutEffect.js.map +1 -0
- package/package.json +10 -10
- package/src/hooks/useToolCallElapsed.test.tsx +4 -2
- package/src/hooks/useToolCallElapsed.ts +3 -2
- package/src/index.ts +20 -2
- package/src/mcp-apps/McpAppRenderer.test.tsx +7 -2
- package/src/mcp-apps/McpAppRenderer.tsx +3 -9
- package/src/mcp-apps/app-frame.test.tsx +102 -6
- package/src/mcp-apps/app-frame.tsx +7 -10
- package/src/mcp-apps/bridge.test.ts +154 -0
- package/src/mcp-apps/bridge.ts +51 -9
- package/src/mcp-apps/csp.test.ts +112 -0
- package/src/mcp-apps/csp.ts +124 -0
- package/src/mcp-apps/index.ts +1 -0
- package/src/mcp-apps/types.ts +16 -1
- package/src/model-context/frame/AssistantFrame.test.ts +45 -0
- package/src/model-context/frame/SPEC_AssistantFrame.md +1 -0
- package/src/primitives/assistantModal/AssistantModalRoot.tsx +1 -0
- package/src/primitives/attachment.ts +4 -1
- package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +32 -1
- package/src/primitives/composer/ComposerAttachmentDropzone.tsx +14 -12
- package/src/primitives/composer/ComposerCancel.focus.test.tsx +307 -0
- package/src/primitives/composer/ComposerCancel.ts +8 -1
- package/src/primitives/composer/ComposerDisabledThread.test.tsx +121 -0
- package/src/primitives/composer/ComposerInput.tsx +19 -1
- package/src/primitives/composer/ComposerRoot.tsx +6 -5
- package/src/primitives/composer/composerInputSelector.ts +4 -0
- package/src/primitives/composer/useComposerCancelWithFocus.ts +57 -0
- package/src/primitives/composer.ts +56 -21
- package/src/primitives/message/MessagePartsGrouped.test.tsx +106 -2
- package/src/primitives/message/MessagePartsGrouped.tsx +36 -134
- package/src/primitives/selectionToolbar/SelectionToolbarRoot.test.tsx +20 -12
- package/src/primitives/selectionToolbar/SelectionToolbarRoot.tsx +6 -2
- package/src/primitives/thread/ThreadViewport.tsx +3 -3
- package/src/primitives/thread/topAnchor/useTopAnchorReserve.ts +2 -2
- package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +666 -63
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +201 -73
- package/src/sandbox-host/SandboxHost.tsx +3 -8
- package/src/unstable/useComposerInput.ts +9 -4
- package/src/unstable/useComposerInputHistory.ts +3 -2
- package/src/unstable/useLiveCompletionAdapter.ts +9 -14
- package/src/unstable/useMentionAdapter.ts +12 -3
- package/src/unstable/useMessageStallDetection.ts +4 -2
- package/src/unstable/useSlashCommandAdapter.ts +8 -4
- package/src/unstable/webmcp/useWebMcpProvider.ts +4 -0
- package/src/utils/getSelectionMessageId.test.ts +231 -0
- package/src/utils/getSelectionMessageId.ts +45 -7
- package/src/utils/hooks/useOnResizeContent.ts +2 -29
- package/src/utils/useIsomorphicLayoutEffect.test.tsx +21 -0
- package/src/utils/useIsomorphicLayoutEffect.ts +9 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useComposedRefs } from "radix-ui/internal";
|
|
4
|
-
import { useCallback,
|
|
4
|
+
import { useCallback, useRef, type RefCallback } from "react";
|
|
5
5
|
import { useAui, useAuiEvent, useAuiState } from "@assistant-ui/store";
|
|
6
6
|
import {
|
|
7
7
|
isUserScrollUp,
|
|
@@ -13,12 +13,21 @@ import { useOnScrollToBottom } from "../../utils/hooks/useOnScrollToBottom";
|
|
|
13
13
|
import { useManagedRef } from "../../utils/hooks/useManagedRef";
|
|
14
14
|
import { writableStore } from "../../context/ReadonlyStore";
|
|
15
15
|
import { useThreadViewportStore } from "../../context/react/ThreadViewportContext";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
import { useIsomorphicLayoutEffect } from "../../utils/useIsomorphicLayoutEffect";
|
|
17
|
+
|
|
18
|
+
const SCROLL_KEYS = new Set([
|
|
19
|
+
" ",
|
|
20
|
+
"ArrowUp",
|
|
21
|
+
"ArrowDown",
|
|
22
|
+
"PageUp",
|
|
23
|
+
"PageDown",
|
|
24
|
+
"Home",
|
|
25
|
+
"End",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
// Enter and Space activate focused controls, while navigation keys can
|
|
29
|
+
// interrupt scrolling without producing a scroll event.
|
|
30
|
+
const INTENT_CANCEL_KEYS = new Set(["Enter", ...SCROLL_KEYS]);
|
|
22
31
|
|
|
23
32
|
// A control that consumes the activation key itself instead of acting on thread
|
|
24
33
|
// content. `contenteditable="false"` marks a non-editable island inside an
|
|
@@ -35,6 +44,22 @@ const TEXT_ENTRY_SELECTOR = [
|
|
|
35
44
|
|
|
36
45
|
const MESSAGE_SELECTOR = "[data-message-id]";
|
|
37
46
|
|
|
47
|
+
const DISCLOSURE_SELECTOR = "[aria-expanded], summary";
|
|
48
|
+
|
|
49
|
+
// A popup trigger (menu, dialog, listbox) also carries aria-expanded, but what
|
|
50
|
+
// it opens is portaled away from the thread, so only disclosures count.
|
|
51
|
+
const isOpeningDisclosure = (target: Element): boolean => {
|
|
52
|
+
const control = target.closest(DISCLOSURE_SELECTOR);
|
|
53
|
+
if (!control) return false;
|
|
54
|
+
if (control.tagName === "SUMMARY")
|
|
55
|
+
return control.parentElement?.hasAttribute("open") === false;
|
|
56
|
+
return (
|
|
57
|
+
control.getAttribute("aria-expanded") === "false" &&
|
|
58
|
+
(control.getAttribute("aria-haspopup") ?? "false") === "false" &&
|
|
59
|
+
control.getAttribute("role") !== "combobox"
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
38
63
|
type MessageOffset = { readonly id: string; readonly offset: number };
|
|
39
64
|
|
|
40
65
|
const offsetInViewport = (element: Element, viewport: Element) =>
|
|
@@ -166,9 +191,15 @@ export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
|
|
|
166
191
|
// user actively scrolls up while content size is stable.
|
|
167
192
|
const scrollingToBottomBehaviorRef = useRef<ScrollBehavior | null>(null);
|
|
168
193
|
const followBottomRef = useRef(autoScroll);
|
|
194
|
+
// Set by expanding a disclosure; suspends follow without clearing its intent
|
|
195
|
+
// until a scroll gesture reaches an overflowing bottom (content that fits
|
|
196
|
+
// reads as at the bottom), a run starts, the thread changes, or the reader
|
|
197
|
+
// asks for the bottom again.
|
|
198
|
+
const followPausedRef = useRef(false);
|
|
199
|
+
const scrolledSincePauseRef = useRef(false);
|
|
169
200
|
const previousAutoScrollRef = useRef(autoScroll);
|
|
170
201
|
|
|
171
|
-
|
|
202
|
+
useIsomorphicLayoutEffect(() => {
|
|
172
203
|
const previousAutoScroll = previousAutoScrollRef.current;
|
|
173
204
|
previousAutoScrollRef.current = autoScroll;
|
|
174
205
|
if (previousAutoScroll || !autoScroll) return;
|
|
@@ -182,9 +213,12 @@ export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
|
|
|
182
213
|
if (!div) return;
|
|
183
214
|
|
|
184
215
|
followBottomRef.current = true;
|
|
216
|
+
followPausedRef.current = false;
|
|
185
217
|
scrollingToBottomBehaviorRef.current = behavior;
|
|
186
218
|
prependAnchorRef.current = null;
|
|
187
219
|
div.scrollTo({ top: div.scrollHeight, behavior });
|
|
220
|
+
lastScrollTop.current = div.scrollTop;
|
|
221
|
+
lastScrollHeight.current = div.scrollHeight;
|
|
188
222
|
}, []);
|
|
189
223
|
|
|
190
224
|
const cancelScheduledFrame = useCallback(() => {
|
|
@@ -205,7 +239,10 @@ export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
|
|
|
205
239
|
[cancelScheduledFrame, scrollToBottom],
|
|
206
240
|
);
|
|
207
241
|
|
|
208
|
-
|
|
242
|
+
useIsomorphicLayoutEffect(
|
|
243
|
+
() => () => cancelScheduledFrame(),
|
|
244
|
+
[cancelScheduledFrame],
|
|
245
|
+
);
|
|
209
246
|
|
|
210
247
|
const hasActiveTopAnchor = useCallback(() => {
|
|
211
248
|
const state = threadViewportStore.getState();
|
|
@@ -216,7 +253,7 @@ export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
|
|
|
216
253
|
);
|
|
217
254
|
}, [threadViewportStore]);
|
|
218
255
|
|
|
219
|
-
const
|
|
256
|
+
const onScroll = () => {
|
|
220
257
|
const div = divRef.current;
|
|
221
258
|
if (!div) return;
|
|
222
259
|
|
|
@@ -231,7 +268,9 @@ export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
|
|
|
231
268
|
const newIsAtBottom = isViewportAtBottom(div);
|
|
232
269
|
|
|
233
270
|
const isInFlightDownwardScroll =
|
|
234
|
-
|
|
271
|
+
scrollingToBottomBehaviorRef.current !== null &&
|
|
272
|
+
!newIsAtBottom &&
|
|
273
|
+
lastScrollTop.current < div.scrollTop;
|
|
235
274
|
if (isInFlightDownwardScroll) {
|
|
236
275
|
// no-op: a smooth scroll-to-bottom fires many midpoint scroll events
|
|
237
276
|
// before landing, don't flicker isAtBottom or clear intent mid-animation
|
|
@@ -249,6 +288,7 @@ export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
|
|
|
249
288
|
// keep intent alive until content can actually scroll
|
|
250
289
|
if (viewportOverflows(div)) {
|
|
251
290
|
scrollingToBottomBehaviorRef.current = null;
|
|
291
|
+
if (scrolledSincePauseRef.current) followPausedRef.current = false;
|
|
252
292
|
}
|
|
253
293
|
if (autoScroll) followBottomRef.current = true;
|
|
254
294
|
} else if (userScrolledUp) {
|
|
@@ -271,8 +311,13 @@ export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
|
|
|
271
311
|
lastScrollHeight.current = div.scrollHeight;
|
|
272
312
|
firstMessageRef.current = measureFirstMessage(div);
|
|
273
313
|
};
|
|
314
|
+
const onScrollRef = useRef(onScroll);
|
|
315
|
+
useIsomorphicLayoutEffect(() => {
|
|
316
|
+
onScrollRef.current = onScroll;
|
|
317
|
+
});
|
|
318
|
+
const handleScroll = useCallback(() => onScrollRef.current(), []);
|
|
274
319
|
|
|
275
|
-
const
|
|
320
|
+
const onResize = () => {
|
|
276
321
|
const div = divRef.current;
|
|
277
322
|
if (!div) return;
|
|
278
323
|
|
|
@@ -286,75 +331,156 @@ export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
|
|
|
286
331
|
lastObservedScrollHeight.current = scrollHeight;
|
|
287
332
|
lastObservedClientHeight.current = clientHeight;
|
|
288
333
|
|
|
289
|
-
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
334
|
+
// A scheduled frame owns the pending behavior; resize callbacks must not apply it early.
|
|
335
|
+
if (scheduledFrameRef.current === null) {
|
|
336
|
+
const scrollBehavior = scrollingToBottomBehaviorRef.current;
|
|
337
|
+
if (scrollBehavior && hasActiveTopAnchor()) {
|
|
338
|
+
// Let the top-anchor reserve own scrolling while a run starts to avoid a bottom-scroll race.
|
|
339
|
+
scrollingToBottomBehaviorRef.current = null;
|
|
340
|
+
} else if (scrollBehavior) {
|
|
341
|
+
scrollToBottom(scrollBehavior);
|
|
342
|
+
} else if (
|
|
343
|
+
autoScroll &&
|
|
344
|
+
!(isRunning && hasActiveTopAnchor()) &&
|
|
345
|
+
followBottomRef.current &&
|
|
346
|
+
!followPausedRef.current
|
|
347
|
+
) {
|
|
348
|
+
scrollToBottom("instant");
|
|
349
|
+
} else if (
|
|
350
|
+
prependAnchorRef.current &&
|
|
351
|
+
!keepMessageAt(div, prependAnchorRef.current, heldScrollTopRef)
|
|
352
|
+
) {
|
|
353
|
+
prependAnchorRef.current = null;
|
|
354
|
+
}
|
|
306
355
|
}
|
|
307
356
|
|
|
308
357
|
handleScroll();
|
|
358
|
+
};
|
|
359
|
+
const onResizeRef = useRef(onResize);
|
|
360
|
+
useIsomorphicLayoutEffect(() => {
|
|
361
|
+
onResizeRef.current = onResize;
|
|
309
362
|
});
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
363
|
+
const resizeRef = useOnResizeContent(() => onResizeRef.current());
|
|
364
|
+
|
|
365
|
+
const scrollRef = useManagedRef<HTMLElement>(
|
|
366
|
+
useCallback(
|
|
367
|
+
(el) => {
|
|
368
|
+
// A user gesture invalidates pending bottom-scroll intent; otherwise an
|
|
369
|
+
// intent kept alive by a non-overflowing thread (see handleScroll) hijacks
|
|
370
|
+
// the next content growth, e.g. expanding a collapsible tool call. Keyboard
|
|
371
|
+
// activation reaches that same content without ever emitting a pointer
|
|
372
|
+
// event, so it has to cancel the intent too.
|
|
373
|
+
const cancelPendingScrollToBottom = () => {
|
|
374
|
+
// With nothing pending, the last scroll event already published isAtBottom.
|
|
375
|
+
if (
|
|
376
|
+
scrollingToBottomBehaviorRef.current === null &&
|
|
377
|
+
scheduledFrameRef.current === null
|
|
378
|
+
)
|
|
379
|
+
return;
|
|
380
|
+
// A scheduled frame re-plants the intent when it runs, so clearing the
|
|
381
|
+
// ref alone leaves the gesture undone.
|
|
382
|
+
cancelScheduledFrame();
|
|
383
|
+
scrollingToBottomBehaviorRef.current = null;
|
|
384
|
+
handleScroll();
|
|
385
|
+
};
|
|
386
|
+
// The composer renders inside the viewport, so its keystrokes bubble here;
|
|
387
|
+
// cancellation keys only represent a gesture on thread content when they
|
|
388
|
+
// originate outside text entry.
|
|
389
|
+
const cancelOnKeyDown = (event: KeyboardEvent) => {
|
|
390
|
+
if (!INTENT_CANCEL_KEYS.has(event.key)) return;
|
|
391
|
+
const target = event.target as Element | null;
|
|
392
|
+
if (target?.closest?.(TEXT_ENTRY_SELECTOR)) return;
|
|
393
|
+
cancelPendingScrollToBottom();
|
|
394
|
+
};
|
|
395
|
+
// Expanding a disclosure inside a message, such as a reasoning block or a
|
|
396
|
+
// tool call card, means the reader is inspecting it, so bottom follow
|
|
397
|
+
// pauses until the reader scrolls or asks for the bottom again. Every
|
|
398
|
+
// other interaction, including copying or selecting text, keeps
|
|
399
|
+
// following. Activation is read from click, which keyboard activation also
|
|
400
|
+
// dispatches, in the capture phase so the state is read before any handler
|
|
401
|
+
// on the disclosure flips it or stops the event.
|
|
402
|
+
const pauseFollowOnExpand = (event: MouseEvent) => {
|
|
403
|
+
const target = event.target as Element | null;
|
|
404
|
+
if (
|
|
405
|
+
!target?.closest?.(MESSAGE_SELECTOR) ||
|
|
406
|
+
target.closest(TEXT_ENTRY_SELECTOR) ||
|
|
407
|
+
!isOpeningDisclosure(target)
|
|
408
|
+
)
|
|
409
|
+
return;
|
|
410
|
+
followPausedRef.current = true;
|
|
411
|
+
scrolledSincePauseRef.current = false;
|
|
412
|
+
cancelPendingScrollToBottom();
|
|
413
|
+
};
|
|
414
|
+
const noteScrollGesture = (event: Event) => {
|
|
415
|
+
if (event instanceof KeyboardEvent) {
|
|
416
|
+
if (!SCROLL_KEYS.has(event.key)) return;
|
|
417
|
+
if (
|
|
418
|
+
(event.target as Element | null)?.closest?.(TEXT_ENTRY_SELECTOR)
|
|
419
|
+
)
|
|
420
|
+
return;
|
|
421
|
+
} else if (event.type === "pointerdown" && event.target !== el) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
scrolledSincePauseRef.current = true;
|
|
425
|
+
};
|
|
426
|
+
const releasePrependAnchor = () => {
|
|
427
|
+
prependAnchorRef.current = null;
|
|
428
|
+
};
|
|
429
|
+
const gestures = [
|
|
430
|
+
"pointerdown",
|
|
431
|
+
"wheel",
|
|
432
|
+
"touchstart",
|
|
433
|
+
"keydown",
|
|
434
|
+
] as const;
|
|
435
|
+
const resumeGestures = [
|
|
436
|
+
"pointerdown",
|
|
437
|
+
"wheel",
|
|
438
|
+
"touchmove",
|
|
439
|
+
"keydown",
|
|
440
|
+
] as const;
|
|
441
|
+
el.addEventListener("scroll", handleScroll);
|
|
442
|
+
el.addEventListener("pointerdown", cancelPendingScrollToBottom);
|
|
443
|
+
el.addEventListener("wheel", cancelPendingScrollToBottom, {
|
|
444
|
+
passive: true,
|
|
445
|
+
});
|
|
446
|
+
el.addEventListener("touchstart", cancelPendingScrollToBottom, {
|
|
447
|
+
passive: true,
|
|
448
|
+
});
|
|
449
|
+
el.addEventListener("keydown", cancelOnKeyDown);
|
|
450
|
+
el.addEventListener("click", pauseFollowOnExpand, { capture: true });
|
|
451
|
+
for (const gesture of resumeGestures) {
|
|
452
|
+
el.addEventListener(gesture, noteScrollGesture, { passive: true });
|
|
453
|
+
}
|
|
454
|
+
for (const gesture of gestures) {
|
|
455
|
+
el.addEventListener(gesture, releasePrependAnchor, { passive: true });
|
|
456
|
+
}
|
|
457
|
+
return () => {
|
|
458
|
+
el.removeEventListener("scroll", handleScroll);
|
|
459
|
+
el.removeEventListener("pointerdown", cancelPendingScrollToBottom);
|
|
460
|
+
el.removeEventListener("wheel", cancelPendingScrollToBottom);
|
|
461
|
+
el.removeEventListener("touchstart", cancelPendingScrollToBottom);
|
|
462
|
+
el.removeEventListener("keydown", cancelOnKeyDown);
|
|
463
|
+
el.removeEventListener("click", pauseFollowOnExpand, {
|
|
464
|
+
capture: true,
|
|
465
|
+
});
|
|
466
|
+
for (const gesture of resumeGestures) {
|
|
467
|
+
el.removeEventListener(gesture, noteScrollGesture);
|
|
468
|
+
}
|
|
469
|
+
for (const gesture of gestures) {
|
|
470
|
+
el.removeEventListener(gesture, releasePrependAnchor);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
},
|
|
474
|
+
[cancelScheduledFrame, handleScroll],
|
|
475
|
+
),
|
|
476
|
+
);
|
|
351
477
|
|
|
352
478
|
// Earlier messages prepended above the reader put the old first message back
|
|
353
479
|
// where the reader saw it, so growth below it in the same commit (a
|
|
354
480
|
// streaming reply) is not mistaken for the page; where the browser already
|
|
355
481
|
// anchored the scroll, this is a no-op.
|
|
356
482
|
const previousRef = useRef({ threadId, firstMessageId });
|
|
357
|
-
|
|
483
|
+
useIsomorphicLayoutEffect(() => {
|
|
358
484
|
const previous = previousRef.current;
|
|
359
485
|
previousRef.current = { threadId, firstMessageId };
|
|
360
486
|
if (previous.threadId !== threadId) {
|
|
@@ -367,7 +493,7 @@ export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
|
|
|
367
493
|
!div ||
|
|
368
494
|
previousFirstMessageId === undefined ||
|
|
369
495
|
previousFirstMessageId === firstMessageId ||
|
|
370
|
-
(autoScroll && followBottomRef.current) ||
|
|
496
|
+
(autoScroll && followBottomRef.current && !followPausedRef.current) ||
|
|
371
497
|
scrollingToBottomBehaviorRef.current !== null
|
|
372
498
|
)
|
|
373
499
|
return;
|
|
@@ -397,7 +523,7 @@ export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
|
|
|
397
523
|
lastScrollHeight.current = div.scrollHeight;
|
|
398
524
|
}, [aui, autoScroll, firstMessageId, threadId]);
|
|
399
525
|
|
|
400
|
-
|
|
526
|
+
useIsomorphicLayoutEffect(() => {
|
|
401
527
|
if (!scrollToBottomOnInitialize) return;
|
|
402
528
|
if (!hasMessages) {
|
|
403
529
|
initializeScrollRequestedRef.current = false;
|
|
@@ -418,12 +544,14 @@ export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
|
|
|
418
544
|
|
|
419
545
|
useAuiEvent("thread.runStart", () => {
|
|
420
546
|
prependAnchorRef.current = null;
|
|
547
|
+
followPausedRef.current = false;
|
|
421
548
|
if (!scrollToBottomOnRunStart) return;
|
|
422
549
|
if (threadViewportStore.getState().turnAnchor === "top") return;
|
|
423
550
|
scheduleScrollToBottom("auto");
|
|
424
551
|
});
|
|
425
552
|
|
|
426
553
|
useAuiEvent("threads.selectionChanged", () => {
|
|
554
|
+
followPausedRef.current = false;
|
|
427
555
|
if (!scrollToBottomOnThreadSwitch) return;
|
|
428
556
|
scheduleScrollToBottom("instant");
|
|
429
557
|
});
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
useEffect,
|
|
6
|
-
useLayoutEffect,
|
|
7
|
-
useRef,
|
|
8
|
-
useState,
|
|
9
|
-
} from "react";
|
|
3
|
+
import { type CSSProperties, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { useIsomorphicLayoutEffect } from "../utils/useIsomorphicLayoutEffect";
|
|
10
5
|
import {
|
|
11
6
|
isShimLoadError,
|
|
12
7
|
type RenderedFrame,
|
|
@@ -95,7 +90,7 @@ export function SandboxHost({
|
|
|
95
90
|
createBridge,
|
|
96
91
|
onError,
|
|
97
92
|
});
|
|
98
|
-
|
|
93
|
+
useIsomorphicLayoutEffect(() => {
|
|
99
94
|
liveRef.current = { content, sandbox, createBridge, onError };
|
|
100
95
|
}, [content, sandbox, createBridge, onError]);
|
|
101
96
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
useTriggerPopoverAriaProps,
|
|
13
13
|
} from "../primitives/composer/useComposerInputState";
|
|
14
14
|
|
|
15
|
+
/** @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release. */
|
|
15
16
|
export type Unstable_UseComposerInputOptions = {
|
|
16
17
|
/**
|
|
17
18
|
* Disables the input in addition to the composer's own disabled sources
|
|
@@ -21,6 +22,7 @@ export type Unstable_UseComposerInputOptions = {
|
|
|
21
22
|
disabled?: boolean | undefined;
|
|
22
23
|
};
|
|
23
24
|
|
|
25
|
+
/** @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release. */
|
|
24
26
|
export type Unstable_ComposerInput = {
|
|
25
27
|
/** Current composer text, or `""` when the composer is not editing. */
|
|
26
28
|
value: string;
|
|
@@ -49,8 +51,6 @@ export type Unstable_ComposerInput = {
|
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
/**
|
|
52
|
-
* @deprecated Under active development and might change without notice.
|
|
53
|
-
*
|
|
54
54
|
* Headless bridge to the composer's text value and send action, for building a
|
|
55
55
|
* custom composer input without `ComposerPrimitive.Input`. It is a thin bridge,
|
|
56
56
|
* not a second input: it does not own keyboard behavior, autosize, IME or
|
|
@@ -58,6 +58,8 @@ export type Unstable_ComposerInput = {
|
|
|
58
58
|
* state. Spread `unstable_useTriggerPopoverAriaProps()` onto your element for
|
|
59
59
|
* trigger-popover combobox semantics.
|
|
60
60
|
*
|
|
61
|
+
* @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release.
|
|
62
|
+
*
|
|
61
63
|
* @example
|
|
62
64
|
* ```tsx
|
|
63
65
|
* const { value, setText, send, isDisabled, canSend } = unstable_useComposerInput();
|
|
@@ -66,6 +68,8 @@ export type Unstable_ComposerInput = {
|
|
|
66
68
|
* disabled={isDisabled}
|
|
67
69
|
* onChange={(e) => setText(e.target.value)}
|
|
68
70
|
* onKeyDown={(e) => {
|
|
71
|
+
* if (e.nativeEvent.isComposing || e.keyCode === 229) return;
|
|
72
|
+
*
|
|
69
73
|
* if (e.key === "Enter" && !e.shiftKey && canSend) {
|
|
70
74
|
* e.preventDefault();
|
|
71
75
|
* send();
|
|
@@ -104,17 +108,18 @@ export function unstable_useComposerInput(
|
|
|
104
108
|
return { value, setText, send, isDisabled, canSend };
|
|
105
109
|
}
|
|
106
110
|
|
|
111
|
+
/** @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release. */
|
|
107
112
|
export type Unstable_TriggerPopoverAriaProps = TriggerPopoverAriaProps;
|
|
108
113
|
|
|
109
114
|
/**
|
|
110
|
-
* @deprecated Under active development and might change without notice.
|
|
111
|
-
*
|
|
112
115
|
* ARIA combobox attributes for the focused element (typically the composer
|
|
113
116
|
* input) describing the open trigger popover, per the WAI-ARIA editable
|
|
114
117
|
* combobox pattern. Returns an empty object outside a `TriggerPopoverRoot` or
|
|
115
118
|
* when no popover is open. Spread these last so they take precedence over any
|
|
116
119
|
* matching ARIA props you set yourself, mirroring `ComposerPrimitive.Input`.
|
|
117
120
|
*
|
|
121
|
+
* @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release.
|
|
122
|
+
*
|
|
118
123
|
* @example
|
|
119
124
|
* ```tsx
|
|
120
125
|
* const aria = unstable_useTriggerPopoverAriaProps();
|
|
@@ -14,6 +14,7 @@ import type { ThreadMessage } from "@assistant-ui/core";
|
|
|
14
14
|
import { getThreadMessageText } from "@assistant-ui/core/internal";
|
|
15
15
|
import { useTriggerPopoverRootContextOptional } from "../primitives/composer/trigger/TriggerPopoverRootContext";
|
|
16
16
|
|
|
17
|
+
/** @deprecated Experimental since 2026-06-11. Not scheduled for removal; the API may change in any release. */
|
|
17
18
|
export type Unstable_ComposerInputHistory = {
|
|
18
19
|
/** Keydown handler to spread onto `ComposerPrimitive.Input`. */
|
|
19
20
|
onKeyDown: KeyboardEventHandler<HTMLTextAreaElement>;
|
|
@@ -45,8 +46,6 @@ const isOnLastLine = (value: string, caret: number): boolean =>
|
|
|
45
46
|
!value.slice(caret).includes("\n");
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
|
-
* @deprecated Under active development and might change without notice.
|
|
49
|
-
*
|
|
50
49
|
* Terminal-style input history for the thread composer: ArrowUp on an
|
|
51
50
|
* empty draft recalls previously sent user messages (newest first),
|
|
52
51
|
* ArrowDown steps back toward the newest and finally restores the draft
|
|
@@ -58,6 +57,8 @@ const isOnLastLine = (value: string, caret: number): boolean =>
|
|
|
58
57
|
* to modifier keys, and to consumer handlers that already called
|
|
59
58
|
* `preventDefault`. It is inert on edit composers.
|
|
60
59
|
*
|
|
60
|
+
* @deprecated Experimental since 2026-06-11. Not scheduled for removal; the API may change in any release.
|
|
61
|
+
*
|
|
61
62
|
* @example
|
|
62
63
|
* ```tsx
|
|
63
64
|
* const history = unstable_useComposerInputHistory();
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
useEffect,
|
|
6
|
-
useLayoutEffect,
|
|
7
|
-
useMemo,
|
|
8
|
-
useRef,
|
|
9
|
-
useState,
|
|
10
|
-
} from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { useIsomorphicLayoutEffect } from "../utils/useIsomorphicLayoutEffect";
|
|
11
5
|
import type {
|
|
12
6
|
Unstable_TriggerAdapter,
|
|
13
7
|
Unstable_TriggerItem,
|
|
14
8
|
} from "@assistant-ui/core";
|
|
15
9
|
|
|
10
|
+
/** @deprecated Experimental since 2026-06-15. Not scheduled for removal; the API may change in any release. */
|
|
16
11
|
export type Unstable_UseLiveCompletionAdapterOptions = {
|
|
17
12
|
/**
|
|
18
13
|
* Fetches the items for a query from an async source. Called debounced; the
|
|
@@ -35,8 +30,6 @@ export type Unstable_UseLiveCompletionAdapterOptions = {
|
|
|
35
30
|
const NO_QUERY = "\u0000";
|
|
36
31
|
|
|
37
32
|
/**
|
|
38
|
-
* @deprecated Under active development and may change without notice.
|
|
39
|
-
*
|
|
40
33
|
* Bridges an async completion source (a server search, a gateway RPC) into the
|
|
41
34
|
* synchronous `Unstable_TriggerAdapter` that `ComposerTriggerPopover` consumes.
|
|
42
35
|
* `search(query)` returns the last fetched items synchronously and schedules a
|
|
@@ -47,6 +40,8 @@ const NO_QUERY = "\u0000";
|
|
|
47
40
|
* `isLoading` is `true` while a fetch is in flight. Pass it to the popover's
|
|
48
41
|
* `isLoading` prop to render a loading state.
|
|
49
42
|
*
|
|
43
|
+
* @deprecated Experimental since 2026-06-15. Not scheduled for removal; the API may change in any release.
|
|
44
|
+
*
|
|
50
45
|
* @example
|
|
51
46
|
* ```tsx
|
|
52
47
|
* const mentions = unstable_useLiveCompletionAdapter({
|
|
@@ -75,7 +70,7 @@ export function unstable_useLiveCompletionAdapter(
|
|
|
75
70
|
|
|
76
71
|
const fetcherRef = useRef(fetcher);
|
|
77
72
|
// The debounce timer must only observe fetchers from committed renders.
|
|
78
|
-
|
|
73
|
+
useIsomorphicLayoutEffect(() => {
|
|
79
74
|
fetcherRef.current = fetcher;
|
|
80
75
|
}, [fetcher]);
|
|
81
76
|
|
|
@@ -143,7 +138,7 @@ export function unstable_useLiveCompletionAdapter(
|
|
|
143
138
|
);
|
|
144
139
|
|
|
145
140
|
const scheduleFetchRef = useRef(scheduleFetch);
|
|
146
|
-
|
|
141
|
+
useIsomorphicLayoutEffect(() => {
|
|
147
142
|
scheduleFetchRef.current = scheduleFetch;
|
|
148
143
|
}, [scheduleFetch]);
|
|
149
144
|
|
|
@@ -156,7 +151,7 @@ export function unstable_useLiveCompletionAdapter(
|
|
|
156
151
|
}, [cancelTimer, rearmPendingRetry]);
|
|
157
152
|
|
|
158
153
|
const cacheKeyRef = useRef(cacheKey);
|
|
159
|
-
|
|
154
|
+
useIsomorphicLayoutEffect(() => {
|
|
160
155
|
if (cacheKeyRef.current === cacheKey) return;
|
|
161
156
|
cacheKeyRef.current = cacheKey;
|
|
162
157
|
invalidatePending();
|
|
@@ -175,7 +170,7 @@ export function unstable_useLiveCompletionAdapter(
|
|
|
175
170
|
|
|
176
171
|
// Render-time searches can outlive an abandoned render, so they only arm
|
|
177
172
|
// request work after this hook commits.
|
|
178
|
-
|
|
173
|
+
useIsomorphicLayoutEffect(() => {
|
|
179
174
|
inactiveRef.current = false;
|
|
180
175
|
const deferredQuery = deferredQueryRef.current;
|
|
181
176
|
deferredQueryRef.current = null;
|
|
@@ -17,9 +17,14 @@ import {
|
|
|
17
17
|
type ModelContextSnapshotSource,
|
|
18
18
|
} from "./useModelContextSnapshot";
|
|
19
19
|
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Icon component shape consumed by `ComposerTriggerPopover`'s `iconMap`.
|
|
22
|
+
*
|
|
23
|
+
* @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release.
|
|
24
|
+
*/
|
|
21
25
|
export type Unstable_IconComponent = FC<{ className?: string }>;
|
|
22
26
|
|
|
27
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
23
28
|
export type Unstable_Mention = {
|
|
24
29
|
readonly id: string;
|
|
25
30
|
readonly type: string;
|
|
@@ -30,12 +35,14 @@ export type Unstable_Mention = {
|
|
|
30
35
|
readonly metadata?: ReadonlyJSONObject | undefined;
|
|
31
36
|
};
|
|
32
37
|
|
|
38
|
+
/** @deprecated Experimental since 2026-03-16. Not scheduled for removal; the API may change in any release. */
|
|
33
39
|
export type Unstable_MentionCategory = {
|
|
34
40
|
readonly id: string;
|
|
35
41
|
readonly label: string;
|
|
36
42
|
readonly items: readonly Unstable_Mention[];
|
|
37
43
|
};
|
|
38
44
|
|
|
45
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
39
46
|
export type Unstable_ModelContextToolsOptions = {
|
|
40
47
|
/**
|
|
41
48
|
* Wrap tools in a dedicated category. Selects drill-down mode on its own
|
|
@@ -48,6 +55,7 @@ export type Unstable_ModelContextToolsOptions = {
|
|
|
48
55
|
readonly icon?: string;
|
|
49
56
|
};
|
|
50
57
|
|
|
58
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
51
59
|
export type Unstable_UseMentionAdapterOptions = {
|
|
52
60
|
/**
|
|
53
61
|
* Flat mention list. Ignored when `categories` is set, and keeps the
|
|
@@ -78,6 +86,7 @@ export type Unstable_UseMentionAdapterOptions = {
|
|
|
78
86
|
readonly fallbackIcon?: Unstable_IconComponent;
|
|
79
87
|
};
|
|
80
88
|
|
|
89
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
81
90
|
export type Unstable_MentionDirective = {
|
|
82
91
|
readonly formatter: Unstable_DirectiveFormatter;
|
|
83
92
|
readonly onInserted?: ((item: Unstable_TriggerItem) => void) | undefined;
|
|
@@ -120,12 +129,12 @@ const toolMentionSource: ModelContextSnapshotSource<
|
|
|
120
129
|
};
|
|
121
130
|
|
|
122
131
|
/**
|
|
123
|
-
* @deprecated Under active development and might change without notice.
|
|
124
|
-
*
|
|
125
132
|
* Creates a spreadable `{ adapter, directive }` bundle for `@` mentions.
|
|
126
133
|
* Supports tools registered in model context, explicit items, or both —
|
|
127
134
|
* flat or categorized.
|
|
128
135
|
*
|
|
136
|
+
* @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release.
|
|
137
|
+
*
|
|
129
138
|
* @example
|
|
130
139
|
* ```tsx
|
|
131
140
|
* const mention = unstable_useMentionAdapter();
|