@assistant-ui/react 0.15.24 → 0.15.25

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 (90) hide show
  1. package/dist/hooks/useToolCallElapsed.d.ts +3 -1
  2. package/dist/hooks/useToolCallElapsed.d.ts.map +1 -1
  3. package/dist/hooks/useToolCallElapsed.js +7 -8
  4. package/dist/hooks/useToolCallElapsed.js.map +1 -1
  5. package/dist/index.d.ts +10 -4
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -3
  8. package/dist/model-context/makeAssistantVisible.d.ts +1 -0
  9. package/dist/model-context/makeAssistantVisible.d.ts.map +1 -1
  10. package/dist/model-context/makeAssistantVisible.js +31 -12
  11. package/dist/model-context/makeAssistantVisible.js.map +1 -1
  12. package/dist/primitives/composer/ComposerInput.d.ts +1 -1
  13. package/dist/primitives/composer/ComposerInput.js +1 -1
  14. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  15. package/dist/primitives/composer/trigger/TriggerPopover.d.ts +11 -11
  16. package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
  17. package/dist/primitives/composer/trigger/TriggerPopover.js +62 -60
  18. package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
  19. package/dist/primitives/composer/trigger/TriggerPopoverAction.d.ts +8 -8
  20. package/dist/primitives/composer/trigger/TriggerPopoverAction.d.ts.map +1 -1
  21. package/dist/primitives/composer/trigger/TriggerPopoverAction.js +6 -6
  22. package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
  23. package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts +3 -3
  24. package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts.map +1 -1
  25. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -1
  26. package/dist/primitives/composer/trigger/TriggerPopoverDirective.d.ts +9 -9
  27. package/dist/primitives/composer/trigger/TriggerPopoverDirective.d.ts.map +1 -1
  28. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +7 -7
  29. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
  30. package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts +5 -5
  31. package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts.map +1 -1
  32. package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -1
  33. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts +8 -8
  34. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +8 -8
  35. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
  36. package/dist/primitives/composer/trigger/index.d.ts +1 -1
  37. package/dist/primitives/composer.d.ts +20 -1
  38. package/dist/primitives/composer.d.ts.map +1 -1
  39. package/dist/primitives/composer.js +8 -1
  40. package/dist/primitives/composer.js.map +1 -1
  41. package/dist/primitives/thread/ThreadLoadEarlier.d.ts +19 -0
  42. package/dist/primitives/thread/ThreadLoadEarlier.d.ts.map +1 -0
  43. package/dist/primitives/thread/ThreadLoadEarlier.js +36 -0
  44. package/dist/primitives/thread/ThreadLoadEarlier.js.map +1 -0
  45. package/dist/primitives/thread/ThreadMessages.d.ts +1 -1
  46. package/dist/primitives/thread/ThreadMessages.d.ts.map +1 -1
  47. package/dist/primitives/thread/ThreadMessages.js +2 -2
  48. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  49. package/dist/primitives/thread/useThreadViewportAutoScroll.js +111 -2
  50. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  51. package/dist/primitives/thread.d.ts +2 -0
  52. package/dist/primitives/thread.d.ts.map +1 -1
  53. package/dist/primitives/thread.js +5 -2
  54. package/dist/primitives/thread.js.map +1 -1
  55. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
  56. package/dist/sandbox-host/SandboxHost.js +6 -1
  57. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  58. package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
  59. package/dist/unstable/useMessageStallDetection.js +10 -6
  60. package/dist/unstable/useMessageStallDetection.js.map +1 -1
  61. package/dist/utils/smooth/useSmooth.js +6 -6
  62. package/dist/utils/smooth/useSmooth.js.map +1 -1
  63. package/package.json +5 -4
  64. package/src/hooks/useToolCallElapsed.test.tsx +35 -0
  65. package/src/hooks/useToolCallElapsed.ts +9 -7
  66. package/src/index.ts +28 -3
  67. package/src/model-context/makeAssistantVisible.test.tsx +315 -5
  68. package/src/model-context/makeAssistantVisible.tsx +43 -7
  69. package/src/primitives/composer/ComposerInput.tsx +1 -1
  70. package/src/primitives/composer/trigger/TriggerPopover.tsx +11 -11
  71. package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +10 -13
  72. package/src/primitives/composer/trigger/TriggerPopoverCategories.tsx +2 -2
  73. package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +11 -14
  74. package/src/primitives/composer/trigger/TriggerPopoverExports.test.ts +61 -0
  75. package/src/primitives/composer/trigger/TriggerPopoverItems.tsx +3 -3
  76. package/src/primitives/composer/trigger/TriggerPopoverRootContext.tsx +8 -8
  77. package/src/primitives/composer.ts +21 -1
  78. package/src/primitives/message/MessageRoot.test.tsx +125 -2
  79. package/src/primitives/thread/ThreadLoadEarlier.tsx +35 -0
  80. package/src/primitives/thread/ThreadMessages.tsx +1 -0
  81. package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +256 -5
  82. package/src/primitives/thread/useThreadViewportAutoScroll.ts +147 -1
  83. package/src/primitives/thread.ts +2 -0
  84. package/src/sandbox-host/SandboxHost.test.tsx +34 -0
  85. package/src/sandbox-host/SandboxHost.tsx +8 -4
  86. package/src/tests/toolCallTiming.test.tsx +19 -2
  87. package/src/unstable/useMessageStallDetection.test.tsx +40 -0
  88. package/src/unstable/useMessageStallDetection.ts +10 -6
  89. package/src/utils/smooth/useSmooth.test.tsx +16 -5
  90. package/src/utils/smooth/useSmooth.ts +7 -7
@@ -1,20 +1,17 @@
1
1
  "use client";
2
2
 
3
- import type {
4
- Unstable_DirectiveFormatter,
5
- Unstable_TriggerItem,
6
- } from "@assistant-ui/core";
7
- import { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
3
+ import type { DirectiveFormatter, TriggerItem } from "@assistant-ui/core";
4
+ import { defaultDirectiveFormatter } from "@assistant-ui/core";
8
5
  import { useEffect, useInsertionEffect, useRef, type FC } from "react";
9
6
  import { useTriggerBehaviorRegistration } from "./TriggerPopover";
10
7
  import type { TriggerBehavior } from "./triggerSelectionResource";
11
8
 
12
9
  export namespace ComposerPrimitiveTriggerPopoverDirective {
13
10
  export type Props = {
14
- /** Defaults to `unstable_defaultDirectiveFormatter`. */
15
- readonly formatter?: Unstable_DirectiveFormatter | undefined;
11
+ /** Defaults to `defaultDirectiveFormatter`. */
12
+ readonly formatter?: DirectiveFormatter | undefined;
16
13
  /** Fires after an item has been inserted into the composer. */
17
- readonly onInserted?: ((item: Unstable_TriggerItem) => void) | undefined;
14
+ readonly onInserted?: ((item: TriggerItem) => void) | undefined;
18
15
  };
19
16
  }
20
17
 
@@ -22,16 +19,16 @@ export namespace ComposerPrimitiveTriggerPopoverDirective {
22
19
  * Configures a `<TriggerPopover>` to insert a directive chip when an item is
23
20
  * selected. Render exactly one behavior sub-primitive per `<TriggerPopover>`.
24
21
  *
25
- * Exposed as `ComposerPrimitive.Unstable_TriggerPopover.Directive`.
22
+ * Exposed as `ComposerPrimitive.TriggerPopover.Directive`.
26
23
  *
27
24
  * @example
28
25
  * ```tsx
29
- * <ComposerPrimitive.Unstable_TriggerPopover char="@" adapter={mentionAdapter}>
30
- * <ComposerPrimitive.Unstable_TriggerPopover.Directive
31
- * formatter={unstable_defaultDirectiveFormatter}
26
+ * <ComposerPrimitive.TriggerPopover char="@" adapter={mentionAdapter}>
27
+ * <ComposerPrimitive.TriggerPopover.Directive
28
+ * formatter={defaultDirectiveFormatter}
32
29
  * onInserted={(item) => track("mention", item.id)}
33
30
  * />
34
- * </ComposerPrimitive.Unstable_TriggerPopover>
31
+ * </ComposerPrimitive.TriggerPopover>
35
32
  * ```
36
33
  */
37
34
  export const ComposerPrimitiveTriggerPopoverDirective: FC<
@@ -46,7 +43,7 @@ export const ComposerPrimitiveTriggerPopoverDirective: FC<
46
43
  useEffect(() => {
47
44
  const behavior: TriggerBehavior = {
48
45
  kind: "directive",
49
- formatter: formatter ?? unstable_defaultDirectiveFormatter,
46
+ formatter: formatter ?? defaultDirectiveFormatter,
50
47
  onInserted: (item) => onInsertedRef.current?.(item),
51
48
  };
52
49
  return register(behavior);
@@ -0,0 +1,61 @@
1
+ import { describe, expect, expectTypeOf, it } from "vitest";
2
+ import * as ComposerPrimitive from "../../composer";
3
+ import {
4
+ defaultDirectiveFormatter,
5
+ unstable_defaultDirectiveFormatter,
6
+ type DirectiveFormatter,
7
+ type TriggerAdapter,
8
+ type TriggerCategory,
9
+ type TriggerItem,
10
+ type Unstable_DirectiveFormatter,
11
+ type Unstable_TriggerBehavior,
12
+ type Unstable_TriggerItem,
13
+ } from "../../../index";
14
+ import type { TriggerBehavior } from "./triggerSelectionResource";
15
+
16
+ describe("stable trigger popover exports", () => {
17
+ it("keeps every old composer member as an alias of its stable member", () => {
18
+ expect(ComposerPrimitive.Unstable_TriggerPopover).toBe(
19
+ ComposerPrimitive.TriggerPopover,
20
+ );
21
+ expect(ComposerPrimitive.Unstable_TriggerPopover.Directive).toBe(
22
+ ComposerPrimitive.TriggerPopover.Directive,
23
+ );
24
+ expect(ComposerPrimitive.Unstable_TriggerPopover.Action).toBe(
25
+ ComposerPrimitive.TriggerPopover.Action,
26
+ );
27
+ expect(ComposerPrimitive.Unstable_TriggerPopoverRoot).toBe(
28
+ ComposerPrimitive.TriggerPopoverRoot,
29
+ );
30
+ expect(ComposerPrimitive.Unstable_TriggerPopoverCategories).toBe(
31
+ ComposerPrimitive.TriggerPopoverCategories,
32
+ );
33
+ expect(ComposerPrimitive.Unstable_TriggerPopoverCategoryItem).toBe(
34
+ ComposerPrimitive.TriggerPopoverCategoryItem,
35
+ );
36
+ expect(ComposerPrimitive.Unstable_TriggerPopoverItems).toBe(
37
+ ComposerPrimitive.TriggerPopoverItems,
38
+ );
39
+ expect(ComposerPrimitive.Unstable_TriggerPopoverItem).toBe(
40
+ ComposerPrimitive.TriggerPopoverItem,
41
+ );
42
+ expect(ComposerPrimitive.Unstable_TriggerPopoverBack).toBe(
43
+ ComposerPrimitive.TriggerPopoverBack,
44
+ );
45
+ });
46
+
47
+ it("keeps the old members' namespaced prop types", () => {
48
+ expectTypeOf<ComposerPrimitive.Unstable_TriggerPopoverItem.Props>().toEqualTypeOf<ComposerPrimitive.TriggerPopoverItem.Props>();
49
+ expectTypeOf<ComposerPrimitive.Unstable_TriggerPopoverBack.Element>().toEqualTypeOf<ComposerPrimitive.TriggerPopoverBack.Element>();
50
+ });
51
+
52
+ it("exports the stable adapter, item, category and formatter names beside the old ones", () => {
53
+ expect(unstable_defaultDirectiveFormatter).toBe(defaultDirectiveFormatter);
54
+ expectTypeOf<Unstable_TriggerItem>().toEqualTypeOf<TriggerItem>();
55
+ expectTypeOf<Unstable_DirectiveFormatter>().toEqualTypeOf<DirectiveFormatter>();
56
+ expectTypeOf<TriggerAdapter["categories"]>().returns.toEqualTypeOf<
57
+ readonly TriggerCategory[]
58
+ >();
59
+ expectTypeOf<Unstable_TriggerBehavior>().toEqualTypeOf<TriggerBehavior>();
60
+ });
61
+ });
@@ -10,7 +10,7 @@ import {
10
10
  } from "react";
11
11
  import { composeEventHandlers } from "radix-ui/internal";
12
12
  import { useTriggerPopoverScopeContext } from "./TriggerPopover";
13
- import type { Unstable_TriggerItem } from "@assistant-ui/core";
13
+ import type { TriggerItem } from "@assistant-ui/core";
14
14
 
15
15
  export namespace ComposerPrimitiveTriggerPopoverItems {
16
16
  export type Element = ComponentRef<typeof Primitive.div>;
@@ -18,7 +18,7 @@ export namespace ComposerPrimitiveTriggerPopoverItems {
18
18
  ComponentPropsWithoutRef<typeof Primitive.div>,
19
19
  "children"
20
20
  > & {
21
- children: (items: readonly Unstable_TriggerItem[]) => ReactNode;
21
+ children: (items: readonly TriggerItem[]) => ReactNode;
22
22
  };
23
23
  }
24
24
 
@@ -53,7 +53,7 @@ ComposerPrimitiveTriggerPopoverItems.displayName =
53
53
  export namespace ComposerPrimitiveTriggerPopoverItem {
54
54
  export type Element = ComponentRef<typeof Primitive.button>;
55
55
  export type Props = ComponentPropsWithoutRef<typeof Primitive.button> & {
56
- item: Unstable_TriggerItem;
56
+ item: TriggerItem;
57
57
  index?: number | undefined;
58
58
  };
59
59
  }
@@ -294,21 +294,21 @@ const TriggerPopoverRootInner: FC<
294
294
  *
295
295
  * @example
296
296
  * ```tsx
297
- * <ComposerPrimitive.Unstable_TriggerPopoverRoot>
298
- * <ComposerPrimitive.Unstable_TriggerPopover char="@" adapter={mentionAdapter}>
299
- * <ComposerPrimitive.Unstable_TriggerPopover.Directive formatter={formatter} />
297
+ * <ComposerPrimitive.TriggerPopoverRoot>
298
+ * <ComposerPrimitive.TriggerPopover char="@" adapter={mentionAdapter}>
299
+ * <ComposerPrimitive.TriggerPopover.Directive formatter={formatter} />
300
300
  * ...
301
- * </ComposerPrimitive.Unstable_TriggerPopover>
301
+ * </ComposerPrimitive.TriggerPopover>
302
302
  *
303
- * <ComposerPrimitive.Unstable_TriggerPopover char="/" adapter={slashAdapter}>
304
- * <ComposerPrimitive.Unstable_TriggerPopover.Action onExecute={handler} />
303
+ * <ComposerPrimitive.TriggerPopover char="/" adapter={slashAdapter}>
304
+ * <ComposerPrimitive.TriggerPopover.Action onExecute={handler} />
305
305
  * ...
306
- * </ComposerPrimitive.Unstable_TriggerPopover>
306
+ * </ComposerPrimitive.TriggerPopover>
307
307
  *
308
308
  * <ComposerPrimitive.Root>
309
309
  * <ComposerPrimitive.Input />
310
310
  * </ComposerPrimitive.Root>
311
- * </ComposerPrimitive.Unstable_TriggerPopoverRoot>
311
+ * </ComposerPrimitive.TriggerPopoverRoot>
312
312
  * ```
313
313
  */
314
314
  export const ComposerPrimitiveTriggerPopoverRoot: FC<
@@ -1,3 +1,5 @@
1
+ import type { TriggerMatch, TriggerMatcher } from "./composer/trigger";
2
+
1
3
  export { ComposerPrimitiveRoot as Root } from "./composer/ComposerRoot";
2
4
  export { ComposerPrimitiveInput as Input } from "./composer/ComposerInput";
3
5
  export { ComposerPrimitiveSend as Send } from "./composer/ComposerSend";
@@ -15,12 +17,26 @@ export { ComposerPrimitiveQuoteText as QuoteText } from "./composer/ComposerQuot
15
17
  export { ComposerPrimitiveQuoteDismiss as QuoteDismiss } from "./composer/ComposerQuote";
16
18
  export { ComposerPrimitiveQueue as Queue } from "./composer/ComposerQueue";
17
19
 
20
+ export { ComposerPrimitiveTriggerPopover as TriggerPopover } from "./composer/trigger";
21
+ /** @deprecated Use `ComposerPrimitive.TriggerPopover` instead. */
18
22
  export { ComposerPrimitiveTriggerPopover as Unstable_TriggerPopover } from "./composer/trigger";
23
+ export { ComposerPrimitiveTriggerPopoverRoot as TriggerPopoverRoot } from "./composer/trigger";
24
+ /** @deprecated Use `ComposerPrimitive.TriggerPopoverRoot` instead. */
19
25
  export { ComposerPrimitiveTriggerPopoverRoot as Unstable_TriggerPopoverRoot } from "./composer/trigger";
26
+ export { ComposerPrimitiveTriggerPopoverCategories as TriggerPopoverCategories } from "./composer/trigger";
27
+ /** @deprecated Use `ComposerPrimitive.TriggerPopoverCategories` instead. */
20
28
  export { ComposerPrimitiveTriggerPopoverCategories as Unstable_TriggerPopoverCategories } from "./composer/trigger";
29
+ export { ComposerPrimitiveTriggerPopoverCategoryItem as TriggerPopoverCategoryItem } from "./composer/trigger";
30
+ /** @deprecated Use `ComposerPrimitive.TriggerPopoverCategoryItem` instead. */
21
31
  export { ComposerPrimitiveTriggerPopoverCategoryItem as Unstable_TriggerPopoverCategoryItem } from "./composer/trigger";
32
+ export { ComposerPrimitiveTriggerPopoverItems as TriggerPopoverItems } from "./composer/trigger";
33
+ /** @deprecated Use `ComposerPrimitive.TriggerPopoverItems` instead. */
22
34
  export { ComposerPrimitiveTriggerPopoverItems as Unstable_TriggerPopoverItems } from "./composer/trigger";
35
+ export { ComposerPrimitiveTriggerPopoverItem as TriggerPopoverItem } from "./composer/trigger";
36
+ /** @deprecated Use `ComposerPrimitive.TriggerPopoverItem` instead. */
23
37
  export { ComposerPrimitiveTriggerPopoverItem as Unstable_TriggerPopoverItem } from "./composer/trigger";
38
+ export { ComposerPrimitiveTriggerPopoverBack as TriggerPopoverBack } from "./composer/trigger";
39
+ /** @deprecated Use `ComposerPrimitive.TriggerPopoverBack` instead. */
24
40
  export { ComposerPrimitiveTriggerPopoverBack as Unstable_TriggerPopoverBack } from "./composer/trigger";
25
41
  export { useTriggerPopoverRootContext as unstable_useTriggerPopoverRootContext } from "./composer/trigger";
26
42
  export { useTriggerPopoverRootContextOptional as unstable_useTriggerPopoverRootContextOptional } from "./composer/trigger";
@@ -29,4 +45,8 @@ export { useTriggerPopoverScopeContextOptional as unstable_useTriggerPopoverScop
29
45
  export { useTriggerPopoverTriggers as unstable_useTriggerPopoverTriggers } from "./composer/trigger";
30
46
  export { useTriggerPopoverTriggersOptional as unstable_useTriggerPopoverTriggersOptional } from "./composer/trigger";
31
47
  export type { RegisteredTrigger as Unstable_RegisteredTrigger } from "./composer/trigger";
32
- export type { TriggerMatcher as Unstable_TriggerMatcher } from "./composer/trigger";
48
+ export type { TriggerMatch, TriggerMatcher } from "./composer/trigger";
49
+ /** @deprecated Use `TriggerMatcher` instead. */
50
+ export type Unstable_TriggerMatcher = TriggerMatcher;
51
+ /** @deprecated Use `TriggerMatch` instead. */
52
+ export type Unstable_TriggerMatch = TriggerMatch;
@@ -2,14 +2,26 @@
2
2
 
3
3
  import { act, render } from "@testing-library/react";
4
4
  import { createContext, useContext } from "react";
5
+ import type { ReactNode } from "react";
6
+ import { hydrateRoot } from "react-dom/client";
7
+ import { renderToString } from "react-dom/server";
5
8
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
- import type { ThreadMessageLike } from "@assistant-ui/core";
9
+ import type {
10
+ ChatModelAdapter,
11
+ ThreadMessageLike,
12
+ ThreadMessage,
13
+ } from "@assistant-ui/core";
7
14
  import {
8
15
  AssistantRuntimeProvider,
16
+ ThreadListItemRuntimeProvider,
9
17
  useExternalStoreRuntime,
18
+ useLocalRuntime,
10
19
  } from "@assistant-ui/core/react";
11
20
  import { useAuiState } from "@assistant-ui/store";
12
- import { ThreadPrimitiveMessageByIndex } from "../thread/ThreadMessages";
21
+ import {
22
+ ThreadPrimitiveMessageByIndex,
23
+ ThreadPrimitiveMessages,
24
+ } from "../thread/ThreadMessages";
13
25
  import { ThreadPrimitiveRoot } from "../thread/ThreadRoot";
14
26
  import { ThreadPrimitiveViewport } from "../thread/ThreadViewport";
15
27
  import { MessagePrimitiveRoot } from "./MessageRoot";
@@ -22,6 +34,14 @@ const messages: ThreadMessageLike[] = [
22
34
  },
23
35
  ];
24
36
 
37
+ const initialMessages: readonly ThreadMessageLike[] = [
38
+ { role: "user", content: "Hydrated message" },
39
+ ];
40
+
41
+ const chatModel: ChatModelAdapter = {
42
+ run: async () => ({ content: [] }),
43
+ };
44
+
25
45
  class TestResizeObserver {
26
46
  observe() {}
27
47
  unobserve() {}
@@ -62,6 +82,64 @@ const Example = ({ visible = true }: { visible?: boolean }) => {
62
82
  );
63
83
  };
64
84
 
85
+ const HydrationMessage = () => (
86
+ <MessagePrimitiveRoot data-testid="hydration-message" />
87
+ );
88
+
89
+ const LocalThread = ({
90
+ onRuntime,
91
+ turnAnchor,
92
+ }: {
93
+ onRuntime: (runtime: ReturnType<typeof useLocalRuntime>) => void;
94
+ turnAnchor: "bottom" | "top";
95
+ }) => {
96
+ const runtime = useLocalRuntime(chatModel, { initialMessages });
97
+ onRuntime(runtime);
98
+
99
+ return (
100
+ <AssistantRuntimeProvider runtime={runtime}>
101
+ <ThreadPrimitiveRoot>
102
+ <ThreadPrimitiveViewport
103
+ scrollToBottomOnInitialize={false}
104
+ turnAnchor={turnAnchor}
105
+ >
106
+ <ThreadPrimitiveMessages components={{ Message: HydrationMessage }} />
107
+ </ThreadPrimitiveViewport>
108
+ </ThreadPrimitiveRoot>
109
+ </AssistantRuntimeProvider>
110
+ );
111
+ };
112
+
113
+ const NestedLocalRuntime = ({
114
+ onRuntime,
115
+ turnAnchor,
116
+ }: {
117
+ onRuntime: (runtime: ReturnType<typeof useLocalRuntime>) => void;
118
+ turnAnchor: "bottom" | "top";
119
+ }) => {
120
+ const host = useExternalStoreRuntime<ThreadMessage>({
121
+ messages: [],
122
+ onNew: async () => {},
123
+ });
124
+
125
+ return (
126
+ <AssistantRuntimeProvider runtime={host}>
127
+ <ThreadListItemRuntimeProvider runtime={host.threads.mainItem}>
128
+ <LocalThread onRuntime={onRuntime} turnAnchor={turnAnchor} />
129
+ </ThreadListItemRuntimeProvider>
130
+ </AssistantRuntimeProvider>
131
+ );
132
+ };
133
+
134
+ const renderOnServer = (node: ReactNode) => {
135
+ vi.stubGlobal("document", undefined);
136
+ try {
137
+ return renderToString(node);
138
+ } finally {
139
+ vi.unstubAllGlobals();
140
+ }
141
+ };
142
+
65
143
  beforeEach(() => {
66
144
  vi.stubGlobal("ResizeObserver", TestResizeObserver);
67
145
  });
@@ -82,6 +160,51 @@ const mockHoveredElement = () => {
82
160
  };
83
161
 
84
162
  describe("MessagePrimitiveRoot", () => {
163
+ it.each(["bottom", "top"] as const)(
164
+ "hydrates nested local message ids with %s anchoring",
165
+ async (turnAnchor) => {
166
+ let serverRuntime: ReturnType<typeof useLocalRuntime> | undefined;
167
+ const html = renderOnServer(
168
+ <NestedLocalRuntime
169
+ onRuntime={(runtime) => (serverRuntime = runtime)}
170
+ turnAnchor={turnAnchor}
171
+ />,
172
+ );
173
+ const serverMessageId = serverRuntime!.thread.getState().messages[0]!.id;
174
+ const container = document.createElement("div");
175
+ container.innerHTML = html;
176
+ vi.stubGlobal("ResizeObserver", TestResizeObserver);
177
+
178
+ let clientRuntime: ReturnType<typeof useLocalRuntime> | undefined;
179
+ const errors = vi.spyOn(console, "error").mockImplementation(() => {});
180
+ let root: ReturnType<typeof hydrateRoot> | undefined;
181
+ await act(async () => {
182
+ root = hydrateRoot(
183
+ container,
184
+ <NestedLocalRuntime
185
+ onRuntime={(runtime) => (clientRuntime = runtime)}
186
+ turnAnchor={turnAnchor}
187
+ />,
188
+ );
189
+ });
190
+
191
+ const clientMessageId = clientRuntime!.thread.getState().messages[0]!.id;
192
+ const domMessageId = container
193
+ .querySelector("[data-testid='hydration-message']")!
194
+ .getAttribute("data-message-id");
195
+
196
+ expect(clientMessageId).toBe(serverMessageId);
197
+ expect(domMessageId).toBe(clientMessageId);
198
+ expect(
199
+ errors.mock.calls.some((args) =>
200
+ args.some((value) => String(value).includes("data-message-id")),
201
+ ),
202
+ ).toBe(false);
203
+
204
+ await act(async () => root?.unmount());
205
+ },
206
+ );
207
+
85
208
  it("synchronizes hover state while mounted", async () => {
86
209
  mockHoveredElement();
87
210
 
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { useCallback } from "react";
4
+ import { useAui, useAuiState } from "@assistant-ui/store";
5
+ import {
6
+ type ActionButtonElement,
7
+ type ActionButtonProps,
8
+ createActionButton,
9
+ } from "../../utils/createActionButton";
10
+
11
+ const useThreadLoadEarlier = () => {
12
+ const aui = useAui();
13
+ const hasEarlier = useAuiState((s) => s.thread.hasEarlier);
14
+ const loadEarlier = useCallback(() => {
15
+ void aui.thread.loadEarlier();
16
+ }, [aui]);
17
+ if (!hasEarlier) return null;
18
+ return loadEarlier;
19
+ };
20
+
21
+ export namespace ThreadPrimitiveLoadEarlier {
22
+ export type Element = ActionButtonElement;
23
+ export type Props = ActionButtonProps<typeof useThreadLoadEarlier>;
24
+ }
25
+
26
+ /**
27
+ * A button that loads the page before the first loaded message. Disabled
28
+ * while the runtime reports no earlier messages. A press while a page is
29
+ * loading joins that load, so the button stays enabled and keeps keyboard
30
+ * focus until the page lands; read `thread.isLoadingEarlier` to show progress.
31
+ */
32
+ export const ThreadPrimitiveLoadEarlier = createActionButton(
33
+ "ThreadPrimitive.LoadEarlier",
34
+ useThreadLoadEarlier,
35
+ );
@@ -5,4 +5,5 @@ export {
5
5
  ThreadPrimitiveMessagesImpl,
6
6
  ThreadPrimitiveMessageByIndex,
7
7
  ThreadPrimitiveUnstable_MessageById,
8
+ ThreadPrimitiveRow,
8
9
  } from "@assistant-ui/core/react";