@assistant-ui/react 0.11.39 → 0.11.43

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 (161) hide show
  1. package/dist/client/AssistantClient.d.ts.map +1 -1
  2. package/dist/client/AssistantClient.js.map +1 -1
  3. package/dist/client/ModelContext.d.ts +1 -1
  4. package/dist/client/ModelContext.d.ts.map +1 -1
  5. package/dist/client/ModelContext.js.map +1 -1
  6. package/dist/client/ModelContextClient.d.ts +1 -1
  7. package/dist/client/ThreadMessageClient.d.ts +1 -0
  8. package/dist/client/ThreadMessageClient.d.ts.map +1 -1
  9. package/dist/client/ThreadMessageClient.js +3 -1
  10. package/dist/client/ThreadMessageClient.js.map +1 -1
  11. package/dist/client/types/Message.d.ts +2 -0
  12. package/dist/client/types/Message.d.ts.map +1 -1
  13. package/dist/client/types/ModelContext.d.ts +1 -1
  14. package/dist/client/types/ModelContext.d.ts.map +1 -1
  15. package/dist/client/types/Tools.d.ts +1 -2
  16. package/dist/client/types/Tools.d.ts.map +1 -1
  17. package/dist/context/providers/ThreadViewportProvider.d.ts +5 -1
  18. package/dist/context/providers/ThreadViewportProvider.d.ts.map +1 -1
  19. package/dist/context/providers/ThreadViewportProvider.js +17 -6
  20. package/dist/context/providers/ThreadViewportProvider.js.map +1 -1
  21. package/dist/context/react/AssistantApiContext.d.ts +1 -1
  22. package/dist/context/react/AssistantApiContext.d.ts.map +1 -1
  23. package/dist/context/react/AssistantApiContext.js +1 -2
  24. package/dist/context/react/AssistantApiContext.js.map +1 -1
  25. package/dist/context/stores/ThreadViewport.d.ts +33 -3
  26. package/dist/context/stores/ThreadViewport.d.ts.map +1 -1
  27. package/dist/context/stores/ThreadViewport.js +67 -5
  28. package/dist/context/stores/ThreadViewport.js.map +1 -1
  29. package/dist/devtools/DevToolsHooks.d.ts +1 -1
  30. package/dist/devtools/DevToolsHooks.d.ts.map +1 -1
  31. package/dist/devtools/DevToolsHooks.js.map +1 -1
  32. package/dist/legacy-runtime/AssistantRuntimeProvider.d.ts.map +1 -1
  33. package/dist/legacy-runtime/AssistantRuntimeProvider.js +2 -1
  34. package/dist/legacy-runtime/AssistantRuntimeProvider.js.map +1 -1
  35. package/dist/legacy-runtime/client/ComposerRuntimeClient.d.ts +3 -3
  36. package/dist/legacy-runtime/client/ComposerRuntimeClient.d.ts.map +1 -1
  37. package/dist/legacy-runtime/client/ComposerRuntimeClient.js.map +1 -1
  38. package/dist/legacy-runtime/client/EventManagerRuntimeClient.d.ts +1 -1
  39. package/dist/legacy-runtime/client/ThreadRuntimeClient.js.map +1 -1
  40. package/dist/legacy-runtime/runtime/MessageRuntime.d.ts +3 -0
  41. package/dist/legacy-runtime/runtime/MessageRuntime.d.ts.map +1 -1
  42. package/dist/legacy-runtime/runtime/MessageRuntime.js.map +1 -1
  43. package/dist/legacy-runtime/runtime/RuntimeBindings.d.ts +2 -0
  44. package/dist/legacy-runtime/runtime/RuntimeBindings.d.ts.map +1 -1
  45. package/dist/legacy-runtime/runtime/ThreadRuntime.d.ts +1 -0
  46. package/dist/legacy-runtime/runtime/ThreadRuntime.d.ts.map +1 -1
  47. package/dist/legacy-runtime/runtime/ThreadRuntime.js +6 -3
  48. package/dist/legacy-runtime/runtime/ThreadRuntime.js.map +1 -1
  49. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts +1 -1
  50. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts.map +1 -1
  51. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js +1 -1
  52. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js.map +1 -1
  53. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts +8 -1
  54. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts.map +1 -1
  55. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
  56. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +23 -11
  57. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
  58. package/dist/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.js +5 -5
  59. package/dist/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.js.map +1 -1
  60. package/dist/legacy-runtime/runtime-cores/core/BaseThreadRuntimeCore.d.ts +1 -0
  61. package/dist/legacy-runtime/runtime-cores/core/BaseThreadRuntimeCore.d.ts.map +1 -1
  62. package/dist/legacy-runtime/runtime-cores/core/ThreadRuntimeCore.d.ts +1 -0
  63. package/dist/legacy-runtime/runtime-cores/core/ThreadRuntimeCore.d.ts.map +1 -1
  64. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.d.ts +2 -0
  65. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  66. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.d.ts +2 -0
  67. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  68. package/dist/legacy-runtime/runtime-cores/utils/MessageRepository.d.ts +1 -0
  69. package/dist/legacy-runtime/runtime-cores/utils/MessageRepository.d.ts.map +1 -1
  70. package/dist/legacy-runtime/runtime-cores/utils/MessageRepository.js +2 -1
  71. package/dist/legacy-runtime/runtime-cores/utils/MessageRepository.js.map +1 -1
  72. package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts +2 -2
  73. package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
  74. package/dist/primitives/composer/ComposerAttachmentDropzone.js +31 -11
  75. package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
  76. package/dist/primitives/composer/index.d.ts +1 -0
  77. package/dist/primitives/composer/index.d.ts.map +1 -1
  78. package/dist/primitives/composer/index.js +2 -0
  79. package/dist/primitives/composer/index.js.map +1 -1
  80. package/dist/primitives/message/MessageRoot.d.ts +3 -0
  81. package/dist/primitives/message/MessageRoot.d.ts.map +1 -1
  82. package/dist/primitives/message/MessageRoot.js +24 -2
  83. package/dist/primitives/message/MessageRoot.js.map +1 -1
  84. package/dist/primitives/messagePart/useMessagePartData.d.ts.map +1 -1
  85. package/dist/primitives/messagePart/useMessagePartData.js.map +1 -1
  86. package/dist/primitives/thread/ThreadScrollToBottom.d.ts +7 -2
  87. package/dist/primitives/thread/ThreadScrollToBottom.d.ts.map +1 -1
  88. package/dist/primitives/thread/ThreadScrollToBottom.js +7 -4
  89. package/dist/primitives/thread/ThreadScrollToBottom.js.map +1 -1
  90. package/dist/primitives/thread/ThreadViewport.d.ts +17 -3
  91. package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
  92. package/dist/primitives/thread/ThreadViewport.js +19 -5
  93. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  94. package/dist/primitives/thread/ThreadViewportFooter.d.ts +31 -0
  95. package/dist/primitives/thread/ThreadViewportFooter.d.ts.map +1 -0
  96. package/dist/primitives/thread/ThreadViewportFooter.js +27 -0
  97. package/dist/primitives/thread/ThreadViewportFooter.js.map +1 -0
  98. package/dist/primitives/thread/ThreadViewportSlack.d.ts +20 -0
  99. package/dist/primitives/thread/ThreadViewportSlack.d.ts.map +1 -0
  100. package/dist/primitives/thread/ThreadViewportSlack.js +77 -0
  101. package/dist/primitives/thread/ThreadViewportSlack.js.map +1 -0
  102. package/dist/primitives/thread/index.d.ts +3 -0
  103. package/dist/primitives/thread/index.d.ts.map +1 -1
  104. package/dist/primitives/thread/index.js +7 -1
  105. package/dist/primitives/thread/index.js.map +1 -1
  106. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts +6 -0
  107. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  108. package/dist/primitives/thread/useThreadViewportAutoScroll.js +17 -8
  109. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  110. package/dist/tests/setup.js +14 -14
  111. package/dist/tests/setup.js.map +1 -1
  112. package/dist/utils/hooks/useOnScrollToBottom.d.ts +3 -1
  113. package/dist/utils/hooks/useOnScrollToBottom.d.ts.map +1 -1
  114. package/dist/utils/hooks/useOnScrollToBottom.js.map +1 -1
  115. package/dist/utils/hooks/useSizeHandle.d.ts +11 -0
  116. package/dist/utils/hooks/useSizeHandle.d.ts.map +1 -0
  117. package/dist/utils/hooks/useSizeHandle.js +30 -0
  118. package/dist/utils/hooks/useSizeHandle.js.map +1 -0
  119. package/dist/utils/tap-store/derived-scopes.d.ts +2 -1
  120. package/dist/utils/tap-store/derived-scopes.d.ts.map +1 -1
  121. package/dist/utils/tap-store/derived-scopes.js.map +1 -1
  122. package/dist/utils/tap-store/store.d.ts +2 -1
  123. package/dist/utils/tap-store/store.d.ts.map +1 -1
  124. package/dist/utils/tap-store/store.js.map +1 -1
  125. package/package.json +6 -6
  126. package/src/client/AssistantClient.ts +1 -1
  127. package/src/client/ModelContext.ts +1 -1
  128. package/src/client/ThreadMessageClient.tsx +4 -1
  129. package/src/client/types/Message.ts +3 -0
  130. package/src/client/types/ModelContext.ts +1 -1
  131. package/src/client/types/Tools.ts +1 -2
  132. package/src/context/providers/ThreadViewportProvider.tsx +27 -5
  133. package/src/context/react/AssistantApiContext.tsx +2 -5
  134. package/src/context/stores/ThreadViewport.tsx +125 -7
  135. package/src/devtools/DevToolsHooks.ts +1 -1
  136. package/src/legacy-runtime/AssistantRuntimeProvider.tsx +6 -1
  137. package/src/legacy-runtime/client/ComposerRuntimeClient.ts +3 -3
  138. package/src/legacy-runtime/client/ThreadRuntimeClient.ts +2 -2
  139. package/src/legacy-runtime/runtime/MessageRuntime.ts +2 -0
  140. package/src/legacy-runtime/runtime/RuntimeBindings.ts +2 -0
  141. package/src/legacy-runtime/runtime/ThreadRuntime.ts +6 -3
  142. package/src/legacy-runtime/runtime-cores/assistant-transport/runManager.ts +2 -2
  143. package/src/legacy-runtime/runtime-cores/assistant-transport/types.ts +8 -1
  144. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.tsx +26 -11
  145. package/src/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.ts +5 -5
  146. package/src/legacy-runtime/runtime-cores/core/ThreadRuntimeCore.tsx +1 -0
  147. package/src/legacy-runtime/runtime-cores/utils/MessageRepository.tsx +1 -0
  148. package/src/primitives/composer/ComposerAttachmentDropzone.tsx +35 -12
  149. package/src/primitives/composer/index.ts +1 -0
  150. package/src/primitives/message/MessageRoot.tsx +45 -2
  151. package/src/primitives/messagePart/useMessagePartData.tsx +0 -1
  152. package/src/primitives/thread/ThreadScrollToBottom.tsx +12 -3
  153. package/src/primitives/thread/ThreadViewport.tsx +35 -9
  154. package/src/primitives/thread/ThreadViewportFooter.tsx +57 -0
  155. package/src/primitives/thread/ThreadViewportSlack.tsx +109 -0
  156. package/src/primitives/thread/index.ts +3 -0
  157. package/src/primitives/thread/useThreadViewportAutoScroll.tsx +24 -12
  158. package/src/utils/hooks/useOnScrollToBottom.tsx +3 -1
  159. package/src/utils/hooks/useSizeHandle.ts +43 -0
  160. package/src/utils/tap-store/derived-scopes.ts +2 -1
  161. package/src/utils/tap-store/store.ts +1 -1
@@ -447,6 +447,7 @@ export class ThreadRuntimeImpl implements ThreadRuntime {
447
447
  return {
448
448
  message,
449
449
  parentId: messages[idx - 1]?.id ?? null,
450
+ index: idx,
450
451
  };
451
452
  },
452
453
  );
@@ -468,19 +469,20 @@ export class ThreadRuntimeImpl implements ThreadRuntime {
468
469
  private _getMessageRuntime(
469
470
  path: MessageRuntimePath,
470
471
  callback: () =>
471
- | { parentId: string | null; message: ThreadMessage }
472
+ | { parentId: string | null; message: ThreadMessage; index: number }
472
473
  | undefined,
473
474
  ) {
474
475
  return new MessageRuntimeImpl(
475
476
  new ShallowMemoizeSubject({
476
477
  path,
477
478
  getState: () => {
478
- const { message, parentId } = callback() ?? {};
479
+ const { message, parentId, index } = callback() ?? {};
479
480
 
480
481
  const { messages, speech: speechState } =
481
482
  this._threadBinding.getState();
482
483
 
483
- if (!message || parentId === undefined) return SKIP_UPDATE;
484
+ if (!message || parentId === undefined || index === undefined)
485
+ return SKIP_UPDATE;
484
486
 
485
487
  const thread = this._threadBinding.getState();
486
488
 
@@ -491,6 +493,7 @@ export class ThreadRuntimeImpl implements ThreadRuntime {
491
493
  ...message,
492
494
  ...{ [symbolInnerMessage]: (message as any)[symbolInnerMessage] },
493
495
 
496
+ index,
494
497
  isLast: messages.at(-1)?.id === message.id,
495
498
  parentId,
496
499
 
@@ -11,7 +11,7 @@ export function useRunManager(config: {
11
11
  onRun: (signal: AbortSignal) => Promise<void>;
12
12
  onFinish?: (() => void) | undefined;
13
13
  onCancel?: (() => void) | undefined;
14
- onError?: ((error: Error) => void) | undefined;
14
+ onError?: ((error: Error) => void | Promise<void>) | undefined;
15
15
  }): RunManager {
16
16
  const [isRunning, setIsRunning] = useState(false);
17
17
  const stateRef = useRef({
@@ -37,7 +37,7 @@ export function useRunManager(config: {
37
37
  if (ac.signal.aborted) {
38
38
  onCancelRef.current?.();
39
39
  } else {
40
- onErrorRef.current?.(error as Error);
40
+ await onErrorRef.current?.(error as Error);
41
41
  }
42
42
  } finally {
43
43
  onFinishRef.current?.();
@@ -97,10 +97,17 @@ export type AssistantTransportOptions<T> = {
97
97
  commands: AssistantTransportCommand[];
98
98
  updateState: (updater: (state: T) => T) => void;
99
99
  },
100
- ) => void;
100
+ ) => void | Promise<void>;
101
+ /**
102
+ * Called when commands are cancelled.
103
+ *
104
+ * When an error occurs, queued commands are automatically cancelled after `onError` settles.
105
+ * In this case, the `error` parameter contains the error that caused the cancellation.
106
+ */
101
107
  onCancel?: (params: {
102
108
  commands: AssistantTransportCommand[];
103
109
  updateState: (updater: (state: T) => T) => void;
110
+ error?: Error;
104
111
  }) => void;
105
112
  adapters?: {
106
113
  attachments?: AttachmentAdapter | undefined;
@@ -174,6 +174,9 @@ const useAssistantTransportThreadRuntime = <T,>(
174
174
  ...commandQueue.state.inTransit,
175
175
  ...commandQueue.state.queued,
176
176
  ];
177
+
178
+ commandQueue.reset();
179
+
177
180
  options.onCancel?.({
178
181
  commands: cmds,
179
182
  updateState: (updater) => {
@@ -181,19 +184,31 @@ const useAssistantTransportThreadRuntime = <T,>(
181
184
  rerender((prev) => prev + 1);
182
185
  },
183
186
  });
187
+ },
188
+ onError: async (error) => {
189
+ const inTransitCmds = [...commandQueue.state.inTransit];
190
+ const queuedCmds = [...commandQueue.state.queued];
184
191
 
185
192
  commandQueue.reset();
186
- },
187
- onError: (error) => {
188
- const cmds = [...commandQueue.state.inTransit];
189
- options.onError?.(error as Error, {
190
- commands: cmds,
191
- updateState: (updater) => {
192
- agentStateRef.current = updater(agentStateRef.current);
193
- rerender((prev) => prev + 1);
194
- },
195
- });
196
- commandQueue.markDelivered();
193
+
194
+ try {
195
+ await options.onError?.(error as Error, {
196
+ commands: inTransitCmds,
197
+ updateState: (updater) => {
198
+ agentStateRef.current = updater(agentStateRef.current);
199
+ rerender((prev) => prev + 1);
200
+ },
201
+ });
202
+ } finally {
203
+ options.onCancel?.({
204
+ commands: queuedCmds,
205
+ updateState: (updater) => {
206
+ agentStateRef.current = updater(agentStateRef.current);
207
+ rerender((prev) => prev + 1);
208
+ },
209
+ error: error as Error,
210
+ });
211
+ }
197
212
  },
198
213
  });
199
214
 
@@ -131,7 +131,7 @@ export function useToolInvocations({
131
131
  });
132
132
 
133
133
  const ignoredToolIds = useRef<Set<string>>(new Set());
134
- const isInititialState = useRef(true);
134
+ const isInitialState = useRef(true);
135
135
 
136
136
  useEffect(() => {
137
137
  const processMessages = (
@@ -140,7 +140,7 @@ export function useToolInvocations({
140
140
  messages.forEach((message) => {
141
141
  message.content.forEach((content) => {
142
142
  if (content.type === "tool-call") {
143
- if (isInititialState.current) {
143
+ if (isInitialState.current) {
144
144
  ignoredToolIds.current.add(content.toolCallId);
145
145
  } else {
146
146
  if (ignoredToolIds.current.has(content.toolCallId)) {
@@ -225,8 +225,8 @@ export function useToolInvocations({
225
225
 
226
226
  processMessages(state.messages);
227
227
 
228
- if (isInititialState.current) {
229
- isInititialState.current = false;
228
+ if (isInitialState.current) {
229
+ isInitialState.current = false;
230
230
  }
231
231
  }, [state, controller, onResult]);
232
232
 
@@ -244,7 +244,7 @@ export function useToolInvocations({
244
244
  return {
245
245
  reset: () => {
246
246
  abort();
247
- isInititialState.current = true;
247
+ isInitialState.current = true;
248
248
  },
249
249
  abort,
250
250
  resume: (toolCallId: string, payload: unknown) => {
@@ -79,6 +79,7 @@ export type ThreadRuntimeCore = Readonly<{
79
79
  | {
80
80
  parentId: string | null;
81
81
  message: ThreadMessage;
82
+ index: number;
82
83
  }
83
84
  | undefined;
84
85
 
@@ -339,6 +339,7 @@ export class MessageRepository {
339
339
  return {
340
340
  parentId: message.prev?.current.id ?? null,
341
341
  message: message.current,
342
+ index: message.level,
342
343
  };
343
344
  }
344
345
 
@@ -1,10 +1,12 @@
1
+ "use client";
2
+
1
3
  import { forwardRef, useCallback, useState } from "react";
2
4
 
3
5
  import { Slot } from "@radix-ui/react-slot";
4
6
  import React from "react";
5
7
  import { useAssistantApi } from "../../context";
6
8
 
7
- export namespace ComposerAttachmentDropzonePrimitive {
9
+ export namespace ComposerPrimitiveAttachmentDropzone {
8
10
  export type Element = HTMLDivElement;
9
11
  export type Props = React.HTMLAttributes<HTMLDivElement> & {
10
12
  asChild?: boolean | undefined;
@@ -12,19 +14,40 @@ export namespace ComposerAttachmentDropzonePrimitive {
12
14
  };
13
15
  }
14
16
 
15
- export const ComposerAttachmentDropzone = forwardRef<
17
+ export const ComposerPrimitiveAttachmentDropzone = forwardRef<
16
18
  HTMLDivElement,
17
- ComposerAttachmentDropzonePrimitive.Props
19
+ ComposerPrimitiveAttachmentDropzone.Props
18
20
  >(({ disabled, asChild = false, children, ...rest }, ref) => {
19
21
  const [isDragging, setIsDragging] = useState(false);
20
22
  const api = useAssistantApi();
21
23
 
22
- const handleDrag = useCallback(
24
+ const handleDragEnterCapture = useCallback(
25
+ (e: React.DragEvent) => {
26
+ if (disabled) return;
27
+ e.preventDefault();
28
+ setIsDragging(true);
29
+ },
30
+ [disabled],
31
+ );
32
+
33
+ const handleDragOverCapture = useCallback(
23
34
  (e: React.DragEvent) => {
24
35
  if (disabled) return;
25
36
  e.preventDefault();
26
- e.stopPropagation();
27
- setIsDragging(e.type === "dragenter" || e.type === "dragover");
37
+ if (!isDragging) setIsDragging(true);
38
+ },
39
+ [disabled, isDragging],
40
+ );
41
+
42
+ const handleDragLeaveCapture = useCallback(
43
+ (e: React.DragEvent) => {
44
+ if (disabled) return;
45
+ e.preventDefault();
46
+ const next = e.relatedTarget as Node | null;
47
+ if (next && e.currentTarget.contains(next)) {
48
+ return;
49
+ }
50
+ setIsDragging(false);
28
51
  },
29
52
  [disabled],
30
53
  );
@@ -33,7 +56,6 @@ export const ComposerAttachmentDropzone = forwardRef<
33
56
  async (e: React.DragEvent) => {
34
57
  if (disabled) return;
35
58
  e.preventDefault();
36
- e.stopPropagation();
37
59
  setIsDragging(false);
38
60
  for (const file of e.dataTransfer.files) {
39
61
  try {
@@ -47,10 +69,10 @@ export const ComposerAttachmentDropzone = forwardRef<
47
69
  );
48
70
 
49
71
  const dragProps = {
50
- onDragEnter: handleDrag,
51
- onDragOver: handleDrag,
52
- onDragLeave: handleDrag,
53
- onDrop: handleDrop,
72
+ onDragEnterCapture: handleDragEnterCapture,
73
+ onDragOverCapture: handleDragOverCapture,
74
+ onDragLeaveCapture: handleDragLeaveCapture,
75
+ onDropCapture: handleDrop,
54
76
  };
55
77
 
56
78
  const Comp = asChild ? Slot : "div";
@@ -67,4 +89,5 @@ export const ComposerAttachmentDropzone = forwardRef<
67
89
  );
68
90
  });
69
91
 
70
- ComposerAttachmentDropzone.displayName = "ComposerPrimitive.AttachmentDropzone";
92
+ ComposerPrimitiveAttachmentDropzone.displayName =
93
+ "ComposerPrimitive.AttachmentDropzone";
@@ -5,4 +5,5 @@ export { ComposerPrimitiveCancel as Cancel } from "./ComposerCancel";
5
5
  export { ComposerPrimitiveAddAttachment as AddAttachment } from "./ComposerAddAttachment";
6
6
  export { ComposerPrimitiveAttachments as Attachments } from "./ComposerAttachments";
7
7
  export { ComposerPrimitiveAttachmentByIndex as AttachmentByIndex } from "./ComposerAttachments";
8
+ export { ComposerPrimitiveAttachmentDropzone as AttachmentDropzone } from "./ComposerAttachmentDropzone";
8
9
  export { ComposerPrimitiveIf as If } from "./ComposerIf";
@@ -9,7 +9,10 @@ import {
9
9
  } from "react";
10
10
  import { useAssistantApi, useAssistantState } from "../../context";
11
11
  import { useManagedRef } from "../../utils/hooks/useManagedRef";
12
+ import { useSizeHandle } from "../../utils/hooks/useSizeHandle";
12
13
  import { useComposedRefs } from "@radix-ui/react-compose-refs";
14
+ import { useThreadViewport } from "../../context/react/ThreadViewportContext";
15
+ import { ThreadPrimitiveViewportSlack } from "../thread/ThreadViewportSlack";
13
16
 
14
17
  const useIsHoveringRef = () => {
15
18
  const api = useAssistantApi();
@@ -41,6 +44,34 @@ const useIsHoveringRef = () => {
41
44
  return useManagedRef(callbackRef);
42
45
  };
43
46
 
47
+ /**
48
+ * Hook that registers the anchor user message as a content inset.
49
+ * Only registers if: user message, at index messages.length-2, and last message is assistant.
50
+ */
51
+ const useMessageViewportRef = () => {
52
+ const turnAnchor = useThreadViewport((s) => s.turnAnchor);
53
+ const registerUserHeight = useThreadViewport(
54
+ (s) => s.registerUserMessageHeight,
55
+ );
56
+
57
+ // inset rules:
58
+ // - the previous user message before the last assistant message registers its full height
59
+ const shouldRegisterAsInset = useAssistantState(
60
+ ({ thread, message }) =>
61
+ turnAnchor === "top" &&
62
+ message.role === "user" &&
63
+ message.index === thread.messages.length - 2 &&
64
+ thread.messages.at(-1)?.role === "assistant",
65
+ );
66
+
67
+ const getHeight = useCallback((el: HTMLElement) => el.offsetHeight, []);
68
+
69
+ return useSizeHandle(
70
+ shouldRegisterAsInset ? registerUserHeight : null,
71
+ getHeight,
72
+ );
73
+ };
74
+
44
75
  export namespace MessagePrimitiveRoot {
45
76
  export type Element = ComponentRef<typeof Primitive.div>;
46
77
  /**
@@ -57,6 +88,9 @@ export namespace MessagePrimitiveRoot {
57
88
  * hover state management for the message. It automatically tracks when the user
58
89
  * is hovering over the message, which can be used by child components like action bars.
59
90
  *
91
+ * When `turnAnchor="top"` is set on the viewport, this component
92
+ * registers itself as the scroll anchor if it's the last user message.
93
+ *
60
94
  * @example
61
95
  * ```tsx
62
96
  * <MessagePrimitive.Root>
@@ -73,9 +107,18 @@ export const MessagePrimitiveRoot = forwardRef<
73
107
  MessagePrimitiveRoot.Props
74
108
  >((props, forwardRef) => {
75
109
  const isHoveringRef = useIsHoveringRef();
76
- const ref = useComposedRefs<HTMLDivElement>(forwardRef, isHoveringRef);
110
+ const anchorUserMessageRef = useMessageViewportRef();
111
+ const ref = useComposedRefs<HTMLDivElement>(
112
+ forwardRef,
113
+ isHoveringRef,
114
+ anchorUserMessageRef,
115
+ );
77
116
 
78
- return <Primitive.div {...props} ref={ref} />;
117
+ return (
118
+ <ThreadPrimitiveViewportSlack>
119
+ <Primitive.div {...props} ref={ref} />
120
+ </ThreadPrimitiveViewportSlack>
121
+ );
79
122
  });
80
123
 
81
124
  MessagePrimitiveRoot.displayName = "MessagePrimitive.Root";
@@ -3,7 +3,6 @@
3
3
  import { useAssistantState } from "../../context";
4
4
  import { DataMessagePart } from "../../types";
5
5
 
6
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
7
6
  export const useMessagePartData = <T = any,>(name?: string) => {
8
7
  const part = useAssistantState(({ part }) => {
9
8
  if (part.type !== "data") {
@@ -11,14 +11,22 @@ import {
11
11
  useThreadViewportStore,
12
12
  } from "../../context/react/ThreadViewportContext";
13
13
 
14
- const useThreadScrollToBottom = () => {
14
+ export namespace useThreadScrollToBottom {
15
+ export type Options = {
16
+ behavior?: ScrollBehavior | undefined;
17
+ };
18
+ }
19
+
20
+ const useThreadScrollToBottom = ({
21
+ behavior,
22
+ }: useThreadScrollToBottom.Options = {}) => {
15
23
  const isAtBottom = useThreadViewport((s) => s.isAtBottom);
16
24
 
17
25
  const threadViewportStore = useThreadViewportStore();
18
26
 
19
27
  const handleScrollToBottom = useCallback(() => {
20
- threadViewportStore.getState().scrollToBottom();
21
- }, [threadViewportStore]);
28
+ threadViewportStore.getState().scrollToBottom({ behavior });
29
+ }, [threadViewportStore, behavior]);
22
30
 
23
31
  if (isAtBottom) return null;
24
32
  return handleScrollToBottom;
@@ -32,4 +40,5 @@ export namespace ThreadPrimitiveScrollToBottom {
32
40
  export const ThreadPrimitiveScrollToBottom = createActionButton(
33
41
  "ThreadPrimitive.ScrollToBottom",
34
42
  useThreadScrollToBottom,
43
+ ["behavior"],
35
44
  );
@@ -2,9 +2,16 @@
2
2
 
3
3
  import { useComposedRefs } from "@radix-ui/react-compose-refs";
4
4
  import { Primitive } from "@radix-ui/react-primitive";
5
- import { type ComponentRef, forwardRef, ComponentPropsWithoutRef } from "react";
5
+ import {
6
+ type ComponentRef,
7
+ forwardRef,
8
+ ComponentPropsWithoutRef,
9
+ useCallback,
10
+ } from "react";
6
11
  import { useThreadViewportAutoScroll } from "./useThreadViewportAutoScroll";
7
- import { ThreadViewportProvider } from "../../context/providers/ThreadViewportProvider";
12
+ import { ThreadPrimitiveViewportProvider } from "../../context/providers/ThreadViewportProvider";
13
+ import { useSizeHandle } from "../../utils/hooks/useSizeHandle";
14
+ import { useThreadViewport } from "../../context/react/ThreadViewportContext";
8
15
 
9
16
  export namespace ThreadPrimitiveViewport {
10
17
  export type Element = ComponentRef<typeof Primitive.div>;
@@ -12,12 +19,31 @@ export namespace ThreadPrimitiveViewport {
12
19
  /**
13
20
  * Whether to automatically scroll to the bottom when new messages are added.
14
21
  * When enabled, the viewport will automatically scroll to show the latest content.
15
- * @default true
22
+ *
23
+ * Default false if `turnAnchor` is "top", otherwise defaults to true.
16
24
  */
17
25
  autoScroll?: boolean | undefined;
26
+
27
+ /**
28
+ * Controls scroll anchoring behavior for new messages.
29
+ * - "bottom" (default): Messages anchor at the bottom, classic chat behavior.
30
+ * - "top": New user messages anchor at the top of the viewport for a focused reading experience.
31
+ */
32
+ turnAnchor?: "top" | "bottom" | undefined;
18
33
  };
19
34
  }
20
35
 
36
+ const useViewportSizeRef = () => {
37
+ const register = useThreadViewport((s) => s.registerViewport);
38
+ const getHeight = useCallback(
39
+ (el: HTMLElement) =>
40
+ el.clientHeight - parseFloat(getComputedStyle(el).paddingTop),
41
+ [],
42
+ );
43
+
44
+ return useSizeHandle(register, getHeight);
45
+ };
46
+
21
47
  const ThreadPrimitiveViewportScrollable = forwardRef<
22
48
  ThreadPrimitiveViewport.Element,
23
49
  ThreadPrimitiveViewport.Props
@@ -25,8 +51,8 @@ const ThreadPrimitiveViewportScrollable = forwardRef<
25
51
  const autoScrollRef = useThreadViewportAutoScroll<HTMLDivElement>({
26
52
  autoScroll,
27
53
  });
28
-
29
- const ref = useComposedRefs(forwardedRef, autoScrollRef);
54
+ const viewportSizeRef = useViewportSizeRef();
55
+ const ref = useComposedRefs(forwardedRef, autoScrollRef, viewportSizeRef);
30
56
 
31
57
  return (
32
58
  <Primitive.div {...rest} ref={ref}>
@@ -47,7 +73,7 @@ ThreadPrimitiveViewportScrollable.displayName =
47
73
  *
48
74
  * @example
49
75
  * ```tsx
50
- * <ThreadPrimitive.Viewport autoScroll={true}>
76
+ * <ThreadPrimitive.Viewport turnAnchor="top">
51
77
  * <ThreadPrimitive.Messages components={{ Message: MyMessage }} />
52
78
  * </ThreadPrimitive.Viewport>
53
79
  * ```
@@ -55,11 +81,11 @@ ThreadPrimitiveViewportScrollable.displayName =
55
81
  export const ThreadPrimitiveViewport = forwardRef<
56
82
  ThreadPrimitiveViewport.Element,
57
83
  ThreadPrimitiveViewport.Props
58
- >((props, ref) => {
84
+ >(({ turnAnchor, ...props }, ref) => {
59
85
  return (
60
- <ThreadViewportProvider>
86
+ <ThreadPrimitiveViewportProvider options={{ turnAnchor }}>
61
87
  <ThreadPrimitiveViewportScrollable {...props} ref={ref} />
62
- </ThreadViewportProvider>
88
+ </ThreadPrimitiveViewportProvider>
63
89
  );
64
90
  });
65
91
 
@@ -0,0 +1,57 @@
1
+ "use client";
2
+
3
+ import { useComposedRefs } from "@radix-ui/react-compose-refs";
4
+ import { Primitive } from "@radix-ui/react-primitive";
5
+ import {
6
+ type ComponentRef,
7
+ forwardRef,
8
+ ComponentPropsWithoutRef,
9
+ useCallback,
10
+ } from "react";
11
+ import { useSizeHandle } from "../../utils/hooks/useSizeHandle";
12
+ import { useThreadViewport } from "../../context/react/ThreadViewportContext";
13
+
14
+ export namespace ThreadPrimitiveViewportFooter {
15
+ export type Element = ComponentRef<typeof Primitive.div>;
16
+ export type Props = ComponentPropsWithoutRef<typeof Primitive.div>;
17
+ }
18
+
19
+ /**
20
+ * A footer container that measures its height for scroll calculations.
21
+ *
22
+ * This component measures its height and provides it to the viewport context
23
+ * for use in scroll calculations (e.g., ViewportSlack min-height).
24
+ *
25
+ * Multiple ViewportFooter components can be used - their heights are summed.
26
+ *
27
+ * Typically used with `className="sticky bottom-0"` to keep the footer
28
+ * visible at the bottom of the viewport while scrolling.
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * <ThreadPrimitive.Viewport>
33
+ * <ThreadPrimitive.Messages components={{ ... }} />
34
+ * <ThreadPrimitive.ViewportFooter className="sticky bottom-0">
35
+ * <Composer />
36
+ * </ThreadPrimitive.ViewportFooter>
37
+ * </ThreadPrimitive.Viewport>
38
+ * ```
39
+ */
40
+ export const ThreadPrimitiveViewportFooter = forwardRef<
41
+ ThreadPrimitiveViewportFooter.Element,
42
+ ThreadPrimitiveViewportFooter.Props
43
+ >((props, forwardedRef) => {
44
+ const register = useThreadViewport((s) => s.registerContentInset);
45
+ const getHeight = useCallback((el: HTMLElement) => {
46
+ const marginTop = parseFloat(getComputedStyle(el).marginTop) || 0;
47
+ return el.offsetHeight + marginTop;
48
+ }, []);
49
+
50
+ const resizeRef = useSizeHandle(register, getHeight);
51
+
52
+ const ref = useComposedRefs(forwardedRef, resizeRef);
53
+
54
+ return <Primitive.div {...props} ref={ref} />;
55
+ });
56
+
57
+ ThreadPrimitiveViewportFooter.displayName = "ThreadPrimitive.ViewportFooter";
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ import { Slot } from "@radix-ui/react-slot";
4
+ import {
5
+ createContext,
6
+ type FC,
7
+ type ReactNode,
8
+ useCallback,
9
+ useContext,
10
+ } from "react";
11
+ import { useThreadViewportStore } from "../../context/react/ThreadViewportContext";
12
+ import { useAssistantState } from "../../context";
13
+ import { useManagedRef } from "../../utils/hooks/useManagedRef";
14
+
15
+ const SlackNestingContext = createContext(false);
16
+
17
+ const parseCssLength = (value: string, element: HTMLElement): number => {
18
+ const match = value.match(/^([\d.]+)(em|px|rem)$/);
19
+ if (!match) return 0;
20
+
21
+ const num = parseFloat(match[1]!);
22
+ const unit = match[2];
23
+
24
+ if (unit === "px") return num;
25
+ if (unit === "em") {
26
+ const fontSize = parseFloat(getComputedStyle(element).fontSize) || 16;
27
+ return num * fontSize;
28
+ }
29
+ if (unit === "rem") {
30
+ const rootFontSize =
31
+ parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
32
+ return num * rootFontSize;
33
+ }
34
+ return 0;
35
+ };
36
+
37
+ export type ThreadViewportSlackProps = {
38
+ /** Threshold at which the user message height clamps to the offset */
39
+ fillClampThreshold?: string;
40
+ /** Offset used when clamping large user messages */
41
+ fillClampOffset?: string;
42
+ children: ReactNode;
43
+ };
44
+
45
+ /**
46
+ * A slot component that provides minimum height to enable scroll anchoring.
47
+ *
48
+ * When using `turnAnchor="top"`, this component ensures there is
49
+ * enough scroll room below the anchor point (last user message) for it to scroll
50
+ * to the top of the viewport. The min-height is applied only to the last
51
+ * assistant message.
52
+ *
53
+ * This component is used internally by MessagePrimitive.Root.
54
+ */
55
+ export const ThreadPrimitiveViewportSlack: FC<ThreadViewportSlackProps> = ({
56
+ children,
57
+ fillClampThreshold = "10em",
58
+ fillClampOffset = "6em",
59
+ }) => {
60
+ const isLast = useAssistantState(({ message }) => message.isLast);
61
+ const threadViewportStore = useThreadViewportStore({ optional: true });
62
+ const isNested = useContext(SlackNestingContext);
63
+
64
+ const callback = useCallback(
65
+ (el: HTMLElement) => {
66
+ if (!threadViewportStore || isNested) return;
67
+
68
+ const updateMinHeight = () => {
69
+ const state = threadViewportStore.getState();
70
+ if (state.turnAnchor === "top" && isLast) {
71
+ const { viewport, inset, userMessage } = state.height;
72
+ const threshold = parseCssLength(fillClampThreshold, el);
73
+ const offset = parseCssLength(fillClampOffset, el);
74
+ const clampAdjustment =
75
+ userMessage <= threshold ? userMessage : offset;
76
+
77
+ const minHeight = Math.max(0, viewport - inset - clampAdjustment);
78
+ el.style.minHeight = `${minHeight}px`;
79
+ el.style.flexShrink = "0";
80
+ el.style.transition = "min-height 0s";
81
+ } else {
82
+ el.style.minHeight = "";
83
+ el.style.flexShrink = "";
84
+ el.style.transition = "";
85
+ }
86
+ };
87
+
88
+ updateMinHeight();
89
+ return threadViewportStore.subscribe(updateMinHeight);
90
+ },
91
+ [
92
+ threadViewportStore,
93
+ isLast,
94
+ isNested,
95
+ fillClampThreshold,
96
+ fillClampOffset,
97
+ ],
98
+ );
99
+
100
+ const ref = useManagedRef<HTMLElement>(callback);
101
+
102
+ return (
103
+ <SlackNestingContext.Provider value={true}>
104
+ <Slot ref={ref}>{children}</Slot>
105
+ </SlackNestingContext.Provider>
106
+ );
107
+ };
108
+
109
+ ThreadPrimitiveViewportSlack.displayName = "ThreadPrimitive.ViewportSlack";
@@ -2,6 +2,9 @@ export { ThreadPrimitiveRoot as Root } from "./ThreadRoot";
2
2
  export { ThreadPrimitiveEmpty as Empty } from "./ThreadEmpty";
3
3
  export { ThreadPrimitiveIf as If } from "./ThreadIf";
4
4
  export { ThreadPrimitiveViewport as Viewport } from "./ThreadViewport";
5
+ export { ThreadPrimitiveViewportProvider as ViewportProvider } from "../../context/providers/ThreadViewportProvider";
6
+ export { ThreadPrimitiveViewportFooter as ViewportFooter } from "./ThreadViewportFooter";
7
+ export { ThreadPrimitiveViewportSlack as ViewportSlack } from "./ThreadViewportSlack";
5
8
  export { ThreadPrimitiveMessages as Messages } from "./ThreadMessages";
6
9
  export { ThreadPrimitiveMessageByIndex as MessageByIndex } from "./ThreadMessages";
7
10
  export { ThreadPrimitiveScrollToBottom as ScrollToBottom } from "./ThreadScrollToBottom";