@assistant-ui/react 0.15.24 → 0.15.26

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 (204) 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 +8 -8
  4. package/dist/hooks/useToolCallElapsed.js.map +1 -1
  5. package/dist/index.d.ts +39 -7
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -3
  8. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  9. package/dist/mcp-apps/McpAppRenderer.js +3 -2
  10. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  11. package/dist/mcp-apps/app-frame.d.ts.map +1 -1
  12. package/dist/mcp-apps/app-frame.js +56 -46
  13. package/dist/mcp-apps/app-frame.js.map +1 -1
  14. package/dist/mcp-apps/csp.d.ts +4 -0
  15. package/dist/mcp-apps/csp.d.ts.map +1 -0
  16. package/dist/mcp-apps/csp.js +93 -0
  17. package/dist/mcp-apps/csp.js.map +1 -0
  18. package/dist/mcp-apps/types.d.ts +1 -0
  19. package/dist/mcp-apps/types.d.ts.map +1 -1
  20. package/dist/mcp-apps/types.js.map +1 -1
  21. package/dist/model-context/makeAssistantVisible.d.ts +1 -0
  22. package/dist/model-context/makeAssistantVisible.d.ts.map +1 -1
  23. package/dist/model-context/makeAssistantVisible.js +31 -12
  24. package/dist/model-context/makeAssistantVisible.js.map +1 -1
  25. package/dist/primitives/assistantModal/AssistantModalRoot.d.ts +1 -0
  26. package/dist/primitives/assistantModal/AssistantModalRoot.d.ts.map +1 -1
  27. package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
  28. package/dist/primitives/attachment.d.ts +3 -1
  29. package/dist/primitives/attachment.d.ts.map +1 -1
  30. package/dist/primitives/attachment.js.map +1 -1
  31. package/dist/primitives/composer/ComposerCancel.d.ts +5 -1
  32. package/dist/primitives/composer/ComposerCancel.d.ts.map +1 -1
  33. package/dist/primitives/composer/ComposerCancel.js +14 -1
  34. package/dist/primitives/composer/ComposerCancel.js.map +1 -1
  35. package/dist/primitives/composer/ComposerInput.d.ts +15 -1
  36. package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
  37. package/dist/primitives/composer/ComposerInput.js +5 -4
  38. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  39. package/dist/primitives/composer/ComposerRoot.d.ts.map +1 -1
  40. package/dist/primitives/composer/ComposerRoot.js +24 -19
  41. package/dist/primitives/composer/ComposerRoot.js.map +1 -1
  42. package/dist/primitives/composer/composerInputSelector.d.ts +3 -0
  43. package/dist/primitives/composer/composerInputSelector.d.ts.map +1 -0
  44. package/dist/primitives/composer/composerInputSelector.js +7 -0
  45. package/dist/primitives/composer/composerInputSelector.js.map +1 -0
  46. package/dist/primitives/composer/trigger/TriggerPopover.d.ts +11 -11
  47. package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
  48. package/dist/primitives/composer/trigger/TriggerPopover.js +62 -60
  49. package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
  50. package/dist/primitives/composer/trigger/TriggerPopoverAction.d.ts +8 -8
  51. package/dist/primitives/composer/trigger/TriggerPopoverAction.d.ts.map +1 -1
  52. package/dist/primitives/composer/trigger/TriggerPopoverAction.js +6 -6
  53. package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
  54. package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts +3 -3
  55. package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts.map +1 -1
  56. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -1
  57. package/dist/primitives/composer/trigger/TriggerPopoverDirective.d.ts +9 -9
  58. package/dist/primitives/composer/trigger/TriggerPopoverDirective.d.ts.map +1 -1
  59. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +7 -7
  60. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
  61. package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts +5 -5
  62. package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts.map +1 -1
  63. package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -1
  64. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts +8 -8
  65. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +8 -8
  66. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
  67. package/dist/primitives/composer/trigger/index.d.ts +1 -1
  68. package/dist/primitives/composer/useComposerCancelWithFocus.d.ts +2 -0
  69. package/dist/primitives/composer/useComposerCancelWithFocus.d.ts.map +1 -0
  70. package/dist/primitives/composer/useComposerCancelWithFocus.js +42 -0
  71. package/dist/primitives/composer/useComposerCancelWithFocus.js.map +1 -0
  72. package/dist/primitives/composer.d.ts +55 -15
  73. package/dist/primitives/composer.d.ts.map +1 -1
  74. package/dist/primitives/composer.js +8 -1
  75. package/dist/primitives/composer.js.map +1 -1
  76. package/dist/primitives/message/MessagePartsGrouped.d.ts +8 -1
  77. package/dist/primitives/message/MessagePartsGrouped.d.ts.map +1 -1
  78. package/dist/primitives/message/MessagePartsGrouped.js +58 -278
  79. package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
  80. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.d.ts.map +1 -1
  81. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +5 -2
  82. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
  83. package/dist/primitives/thread/ThreadLoadEarlier.d.ts +19 -0
  84. package/dist/primitives/thread/ThreadLoadEarlier.d.ts.map +1 -0
  85. package/dist/primitives/thread/ThreadLoadEarlier.js +36 -0
  86. package/dist/primitives/thread/ThreadLoadEarlier.js.map +1 -0
  87. package/dist/primitives/thread/ThreadMessages.d.ts +1 -1
  88. package/dist/primitives/thread/ThreadMessages.d.ts.map +1 -1
  89. package/dist/primitives/thread/ThreadMessages.js +2 -2
  90. package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
  91. package/dist/primitives/thread/ThreadViewport.js +4 -3
  92. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  93. package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js +2 -2
  94. package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js.map +1 -1
  95. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  96. package/dist/primitives/thread/useThreadViewportAutoScroll.js +195 -17
  97. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  98. package/dist/primitives/thread.d.ts +2 -0
  99. package/dist/primitives/thread.d.ts.map +1 -1
  100. package/dist/primitives/thread.js +5 -2
  101. package/dist/primitives/thread.js.map +1 -1
  102. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
  103. package/dist/sandbox-host/SandboxHost.js +9 -3
  104. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  105. package/dist/unstable/useComposerInput.d.ts +7 -4
  106. package/dist/unstable/useComposerInput.d.ts.map +1 -1
  107. package/dist/unstable/useComposerInput.js +4 -4
  108. package/dist/unstable/useComposerInput.js.map +1 -1
  109. package/dist/unstable/useComposerInputHistory.d.ts +3 -2
  110. package/dist/unstable/useComposerInputHistory.d.ts.map +1 -1
  111. package/dist/unstable/useComposerInputHistory.js +2 -2
  112. package/dist/unstable/useComposerInputHistory.js.map +1 -1
  113. package/dist/unstable/useLiveCompletionAdapter.d.ts +3 -2
  114. package/dist/unstable/useLiveCompletionAdapter.d.ts.map +1 -1
  115. package/dist/unstable/useLiveCompletionAdapter.js +8 -7
  116. package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
  117. package/dist/unstable/useMentionAdapter.d.ts +12 -3
  118. package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
  119. package/dist/unstable/useMentionAdapter.js +2 -2
  120. package/dist/unstable/useMentionAdapter.js.map +1 -1
  121. package/dist/unstable/useMessageStallDetection.d.ts +4 -2
  122. package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
  123. package/dist/unstable/useMessageStallDetection.js +12 -8
  124. package/dist/unstable/useMessageStallDetection.js.map +1 -1
  125. package/dist/unstable/useSlashCommandAdapter.d.ts +5 -2
  126. package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
  127. package/dist/unstable/useSlashCommandAdapter.js +5 -4
  128. package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
  129. package/dist/unstable/webmcp/useWebMcpProvider.d.ts +4 -0
  130. package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -1
  131. package/dist/unstable/webmcp/useWebMcpProvider.js +2 -0
  132. package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -1
  133. package/dist/utils/getSelectionMessageId.d.ts.map +1 -1
  134. package/dist/utils/getSelectionMessageId.js +31 -8
  135. package/dist/utils/getSelectionMessageId.js.map +1 -1
  136. package/dist/utils/hooks/useOnResizeContent.d.ts.map +1 -1
  137. package/dist/utils/hooks/useOnResizeContent.js +2 -22
  138. package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
  139. package/dist/utils/smooth/useSmooth.js +6 -6
  140. package/dist/utils/smooth/useSmooth.js.map +1 -1
  141. package/dist/utils/useIsomorphicLayoutEffect.d.ts +3 -0
  142. package/dist/utils/useIsomorphicLayoutEffect.d.ts.map +1 -0
  143. package/dist/utils/useIsomorphicLayoutEffect.js +6 -0
  144. package/dist/utils/useIsomorphicLayoutEffect.js.map +1 -0
  145. package/package.json +12 -11
  146. package/src/hooks/useToolCallElapsed.test.tsx +37 -0
  147. package/src/hooks/useToolCallElapsed.ts +10 -7
  148. package/src/index.ts +47 -5
  149. package/src/mcp-apps/McpAppRenderer.tsx +3 -9
  150. package/src/mcp-apps/app-frame.test.tsx +38 -0
  151. package/src/mcp-apps/app-frame.tsx +5 -8
  152. package/src/mcp-apps/csp.test.ts +112 -0
  153. package/src/mcp-apps/csp.ts +124 -0
  154. package/src/mcp-apps/types.ts +1 -0
  155. package/src/model-context/frame/AssistantFrame.test.ts +45 -0
  156. package/src/model-context/frame/SPEC_AssistantFrame.md +1 -0
  157. package/src/model-context/makeAssistantVisible.test.tsx +315 -5
  158. package/src/model-context/makeAssistantVisible.tsx +43 -7
  159. package/src/primitives/assistantModal/AssistantModalRoot.tsx +1 -0
  160. package/src/primitives/attachment.ts +4 -1
  161. package/src/primitives/composer/ComposerCancel.focus.test.tsx +307 -0
  162. package/src/primitives/composer/ComposerCancel.ts +8 -1
  163. package/src/primitives/composer/ComposerInput.tsx +20 -2
  164. package/src/primitives/composer/ComposerRoot.tsx +6 -5
  165. package/src/primitives/composer/composerInputSelector.ts +4 -0
  166. package/src/primitives/composer/trigger/TriggerPopover.tsx +11 -11
  167. package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +10 -13
  168. package/src/primitives/composer/trigger/TriggerPopoverCategories.tsx +2 -2
  169. package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +11 -14
  170. package/src/primitives/composer/trigger/TriggerPopoverExports.test.ts +61 -0
  171. package/src/primitives/composer/trigger/TriggerPopoverItems.tsx +3 -3
  172. package/src/primitives/composer/trigger/TriggerPopoverRootContext.tsx +8 -8
  173. package/src/primitives/composer/useComposerCancelWithFocus.ts +57 -0
  174. package/src/primitives/composer.ts +70 -15
  175. package/src/primitives/message/MessagePartsGrouped.test.tsx +106 -2
  176. package/src/primitives/message/MessagePartsGrouped.tsx +36 -134
  177. package/src/primitives/message/MessageRoot.test.tsx +125 -2
  178. package/src/primitives/selectionToolbar/SelectionToolbarRoot.test.tsx +20 -12
  179. package/src/primitives/selectionToolbar/SelectionToolbarRoot.tsx +6 -2
  180. package/src/primitives/thread/ThreadLoadEarlier.tsx +35 -0
  181. package/src/primitives/thread/ThreadMessages.tsx +1 -0
  182. package/src/primitives/thread/ThreadViewport.tsx +3 -3
  183. package/src/primitives/thread/topAnchor/useTopAnchorReserve.ts +2 -2
  184. package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +922 -68
  185. package/src/primitives/thread/useThreadViewportAutoScroll.ts +330 -56
  186. package/src/primitives/thread.ts +2 -0
  187. package/src/sandbox-host/SandboxHost.test.tsx +34 -0
  188. package/src/sandbox-host/SandboxHost.tsx +11 -12
  189. package/src/tests/toolCallTiming.test.tsx +19 -2
  190. package/src/unstable/useComposerInput.ts +7 -4
  191. package/src/unstable/useComposerInputHistory.ts +3 -2
  192. package/src/unstable/useLiveCompletionAdapter.ts +9 -14
  193. package/src/unstable/useMentionAdapter.ts +12 -3
  194. package/src/unstable/useMessageStallDetection.test.tsx +40 -0
  195. package/src/unstable/useMessageStallDetection.ts +14 -8
  196. package/src/unstable/useSlashCommandAdapter.ts +8 -4
  197. package/src/unstable/webmcp/useWebMcpProvider.ts +4 -0
  198. package/src/utils/getSelectionMessageId.test.ts +231 -0
  199. package/src/utils/getSelectionMessageId.ts +45 -7
  200. package/src/utils/hooks/useOnResizeContent.ts +2 -29
  201. package/src/utils/smooth/useSmooth.test.tsx +16 -5
  202. package/src/utils/smooth/useSmooth.ts +7 -7
  203. package/src/utils/useIsomorphicLayoutEffect.test.tsx +21 -0
  204. package/src/utils/useIsomorphicLayoutEffect.ts +9 -0
@@ -43,10 +43,37 @@ const findQuoteMarker = (
43
43
  return marker;
44
44
  };
45
45
 
46
- const intersectsExcluded = (scope: Element, selection: Selection): boolean => {
47
- const ranges = Array.from({ length: selection.rangeCount }, (_, i) =>
48
- selection.getRangeAt(i),
46
+ const normalizeRangeEnd = (range: Range): Range => {
47
+ if (range.endOffset !== 0 || range.collapsed) return range;
48
+
49
+ const walker = range.endContainer.ownerDocument?.createTreeWalker(
50
+ range.commonAncestorContainer,
51
+ NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
49
52
  );
53
+ if (!walker) return range;
54
+ walker.currentNode = range.endContainer;
55
+
56
+ while (walker.previousNode()) {
57
+ const node = walker.currentNode;
58
+ const offset = node instanceof Text ? node.length : node.childNodes.length;
59
+ if (range.comparePoint(node, offset) < 0) return range;
60
+ const element = node instanceof Element ? node : node.parentElement;
61
+ const marker = element?.closest(QUOTE_SELECTABLE_SELECTOR);
62
+ if (marker && isExcluded(marker) && !node.contains(range.endContainer))
63
+ return range;
64
+ if (!(node instanceof Text) || !node.data.trim()) continue;
65
+
66
+ const normalized = range.cloneRange();
67
+ normalized.setEnd(node, offset);
68
+ return normalized.collapsed ? range : normalized;
69
+ }
70
+ return range;
71
+ };
72
+
73
+ const intersectsExcluded = (
74
+ scope: Element,
75
+ ranges: readonly Range[],
76
+ ): boolean => {
50
77
  for (const marker of scope.querySelectorAll(QUOTE_SELECTABLE_SELECTOR)) {
51
78
  if (!isExcluded(marker)) continue;
52
79
  if (ranges.some((range) => range.intersectsNode(marker))) return true;
@@ -58,9 +85,21 @@ export const getSelectionMessageId = (
58
85
  selection: Selection,
59
86
  root?: Element | null,
60
87
  ): string | null => {
61
- const { anchorNode, focusNode } = selection;
88
+ let { anchorNode, focusNode } = selection;
62
89
  if (!anchorNode || !focusNode) return null;
63
90
 
91
+ const ranges = Array.from({ length: selection.rangeCount }, (_, i) =>
92
+ selection.getRangeAt(i),
93
+ );
94
+ if (ranges.length === 1) {
95
+ const range = normalizeRangeEnd(ranges[0]!);
96
+ if (range !== ranges[0]) {
97
+ ranges[0] = range;
98
+ anchorNode = range.startContainer;
99
+ focusNode = range.endContainer;
100
+ }
101
+ }
102
+
64
103
  const anchorMessageElement = findMessageElement(anchorNode);
65
104
  const focusMessageElement = findMessageElement(focusNode);
66
105
 
@@ -86,10 +125,9 @@ export const getSelectionMessageId = (
86
125
 
87
126
  const scope = anchorMarker ?? anchorMessageElement;
88
127
 
89
- for (let i = 0; i < selection.rangeCount; i++) {
90
- const { commonAncestorContainer } = selection.getRangeAt(i);
128
+ for (const { commonAncestorContainer } of ranges) {
91
129
  if (!scope.contains(commonAncestorContainer)) return null;
92
130
  }
93
131
 
94
- return intersectsExcluded(scope, selection) ? null : messageId;
132
+ return intersectsExcluded(scope, ranges) ? null : messageId;
95
133
  };
@@ -1,3 +1,4 @@
1
+ import { observeContentResize } from "@assistant-ui/store/client";
1
2
  import { useCallbackRef } from "radix-ui/internal";
2
3
  import { useCallback } from "react";
3
4
  import { useManagedRef } from "./useManagedRef";
@@ -6,35 +7,7 @@ export const useOnResizeContent = (callback: () => void) => {
6
7
  const callbackRef = useCallbackRef(callback);
7
8
 
8
9
  const refCallback = useCallback(
9
- (el: HTMLElement) => {
10
- const resizeObserver = new ResizeObserver(() => {
11
- callbackRef();
12
- });
13
-
14
- const mutationObserver = new MutationObserver((mutations) => {
15
- // Filter out style-only attribute mutations to prevent feedback loops
16
- // with code paths that write styles in response to viewport changes.
17
- const hasRelevantMutation = mutations.some(
18
- (m) => m.type !== "attributes" || m.attributeName !== "style",
19
- );
20
- if (hasRelevantMutation) {
21
- callbackRef();
22
- }
23
- });
24
-
25
- resizeObserver.observe(el);
26
- mutationObserver.observe(el, {
27
- childList: true,
28
- subtree: true,
29
- attributes: true,
30
- characterData: true,
31
- });
32
-
33
- return () => {
34
- resizeObserver.disconnect();
35
- mutationObserver.disconnect();
36
- };
37
- },
10
+ (el: HTMLElement) => observeContentResize(el, callbackRef),
38
11
  [callbackRef],
39
12
  );
40
13
 
@@ -1,5 +1,6 @@
1
1
  /** @vitest-environment jsdom */
2
2
  import { act, renderHook } from "@testing-library/react";
3
+ import { renderToString } from "react-dom/server";
3
4
  import { afterEach, describe, expect, it, vi } from "vitest";
4
5
  import type {
5
6
  MessagePartState,
@@ -68,7 +69,7 @@ const driveAndCount = (minCommitMs: number) => {
68
69
  .spyOn(globalThis, "cancelAnimationFrame")
69
70
  .mockImplementation(() => {});
70
71
  let now = 1000;
71
- const nowSpy = vi.spyOn(Date, "now").mockImplementation(() => now);
72
+ const nowSpy = vi.spyOn(performance, "now").mockImplementation(() => now);
72
73
 
73
74
  const state = runningState("0123456789");
74
75
  const { result, unmount } = renderHook(() =>
@@ -105,6 +106,16 @@ describe("useSmooth", () => {
105
106
  Reflect.deleteProperty(window, "matchMedia");
106
107
  });
107
108
 
109
+ it("does not read the wall clock during server render", () => {
110
+ const nowSpy = vi.spyOn(Date, "now");
111
+ const Probe = () => (
112
+ <span>{useSmooth(runningState("streaming"), true).text}</span>
113
+ );
114
+
115
+ expect(renderToString(<Probe />)).toBe("<span></span>");
116
+ expect(nowSpy).not.toHaveBeenCalled();
117
+ });
118
+
108
119
  it("returns the input state unchanged when disabled", () => {
109
120
  const state = textState("hello");
110
121
  const { result } = renderHook(() => useSmooth(state, false));
@@ -189,8 +200,8 @@ describe("useSmooth", () => {
189
200
  return raf.length;
190
201
  });
191
202
  vi.spyOn(globalThis, "cancelAnimationFrame").mockImplementation(() => {});
192
- let now = 1_000_000;
193
- vi.spyOn(Date, "now").mockImplementation(() => now);
203
+ let now = 0;
204
+ vi.spyOn(performance, "now").mockImplementation(() => now);
194
205
 
195
206
  const frame = () => {
196
207
  if (raf.length === 0) return;
@@ -246,7 +257,7 @@ describe("useSmooth", () => {
246
257
  });
247
258
  vi.spyOn(globalThis, "cancelAnimationFrame").mockImplementation(() => {});
248
259
  let now = 1000;
249
- vi.spyOn(Date, "now").mockImplementation(() => now);
260
+ vi.spyOn(performance, "now").mockImplementation(() => now);
250
261
 
251
262
  const frame = () => {
252
263
  if (raf.length === 0) return;
@@ -297,7 +308,7 @@ describe("useSmooth", () => {
297
308
  });
298
309
  vi.spyOn(globalThis, "cancelAnimationFrame").mockImplementation(() => {});
299
310
  let now = 1000;
300
- vi.spyOn(Date, "now").mockImplementation(() => now);
311
+ vi.spyOn(performance, "now").mockImplementation(() => now);
301
312
 
302
313
  const frame = () => {
303
314
  if (raf.length === 0) return;
@@ -49,8 +49,8 @@ const DEFAULT_MAX_CHAR_INTERVAL_MS = 5;
49
49
 
50
50
  class TextStreamAnimator {
51
51
  private animationFrameId: number | null = null;
52
- private lastUpdateTime: number = Date.now();
53
- public lastCommitTime: number = 0;
52
+ private lastUpdateTime: number = 0;
53
+ public lastCommitTime: number = -Infinity;
54
54
 
55
55
  public targetText: string = "";
56
56
  public drainMs: number = DEFAULT_DRAIN_MS;
@@ -68,7 +68,7 @@ class TextStreamAnimator {
68
68
 
69
69
  start() {
70
70
  if (this.animationFrameId !== null) return;
71
- this.lastUpdateTime = Date.now();
71
+ this.lastUpdateTime = performance.now();
72
72
  this.animate();
73
73
  }
74
74
 
@@ -80,7 +80,7 @@ class TextStreamAnimator {
80
80
  }
81
81
 
82
82
  private animate = () => {
83
- const currentTime = Date.now();
83
+ const currentTime = performance.now();
84
84
  const deltaTime = currentTime - this.lastUpdateTime;
85
85
  let timeToConsume = deltaTime;
86
86
 
@@ -214,8 +214,8 @@ export const useSmooth = (
214
214
  }
215
215
  }, [smoothStatusStore, enabled, text, displayedText, state.status]);
216
216
 
217
- const [animatorRef] = useState<TextStreamAnimator>(
218
- new TextStreamAnimator(displayedText, setText),
217
+ const [animatorRef] = useState(
218
+ () => new TextStreamAnimator(displayedText, setText),
219
219
  );
220
220
 
221
221
  useEffect(() => {
@@ -240,7 +240,7 @@ export const useSmooth = (
240
240
  if (state.status.type === "running") {
241
241
  animatorRef.currentText = "";
242
242
  animatorRef.targetText = text;
243
- animatorRef.lastCommitTime = 0;
243
+ animatorRef.lastCommitTime = -Infinity;
244
244
  animatorRef.start();
245
245
  } else {
246
246
  animatorRef.currentText = text;
@@ -0,0 +1,21 @@
1
+ import { createElement } from "react";
2
+ import { renderToString } from "react-dom/server";
3
+ import { expect, it, vi } from "vitest";
4
+ import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect";
5
+
6
+ it("does not warn about useLayoutEffect during server rendering", () => {
7
+ const errors = vi.spyOn(console, "error").mockImplementation(() => {});
8
+ function Probe() {
9
+ useIsomorphicLayoutEffect(() => {});
10
+ return null;
11
+ }
12
+
13
+ try {
14
+ renderToString(createElement(Probe));
15
+ expect(errors.mock.calls.flat().join(" ")).not.toMatch(
16
+ /useLayoutEffect does nothing on the server/,
17
+ );
18
+ } finally {
19
+ errors.mockRestore();
20
+ }
21
+ });
@@ -0,0 +1,9 @@
1
+ import { useEffect, useLayoutEffect } from "react";
2
+
3
+ const hasWindow =
4
+ typeof (globalThis as typeof globalThis & { window?: unknown }).window !==
5
+ "undefined";
6
+
7
+ export const useIsomorphicLayoutEffect = hasWindow
8
+ ? useLayoutEffect
9
+ : useEffect;