@assistant-ui/react 0.14.15 → 0.14.18

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 (84) hide show
  1. package/dist/client/ExternalThread.d.ts +4 -3
  2. package/dist/client/ExternalThread.d.ts.map +1 -1
  3. package/dist/client/ExternalThread.js +46 -21
  4. package/dist/client/ExternalThread.js.map +1 -1
  5. package/dist/client/InMemoryThreadList.d.ts +1 -1
  6. package/dist/client/InMemoryThreadList.d.ts.map +1 -1
  7. package/dist/client/InMemoryThreadList.js +7 -5
  8. package/dist/client/InMemoryThreadList.js.map +1 -1
  9. package/dist/client/SingleThreadList.d.ts +1 -6
  10. package/dist/client/SingleThreadList.d.ts.map +1 -1
  11. package/dist/client/SingleThreadList.js +6 -4
  12. package/dist/client/SingleThreadList.js.map +1 -1
  13. package/dist/index.d.ts +4 -2
  14. package/dist/index.js +3 -1
  15. package/dist/mcp-apps/McpAppRenderer.d.ts +2 -10
  16. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  17. package/dist/mcp-apps/McpAppRenderer.js +3 -2
  18. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  19. package/dist/mcp-apps/McpAppsRemoteHost.d.ts +1 -8
  20. package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
  21. package/dist/mcp-apps/McpAppsRemoteHost.js +3 -2
  22. package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
  23. package/dist/primitives/composer/ComposerInput.js +3 -3
  24. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  25. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts +2 -10
  26. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -1
  27. package/dist/primitives/composer/trigger/TriggerPopoverResource.js +3 -2
  28. package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -1
  29. package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts +2 -6
  30. package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts.map +1 -1
  31. package/dist/primitives/composer/trigger/triggerDetectionResource.js +3 -2
  32. package/dist/primitives/composer/trigger/triggerDetectionResource.js.map +1 -1
  33. package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts +2 -17
  34. package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
  35. package/dist/primitives/composer/trigger/triggerKeyboardResource.js +3 -2
  36. package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
  37. package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts +2 -10
  38. package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
  39. package/dist/primitives/composer/trigger/triggerNavigationResource.js +3 -2
  40. package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
  41. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts +2 -10
  42. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
  43. package/dist/primitives/composer/trigger/triggerSelectionResource.js +3 -2
  44. package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
  45. package/dist/primitives/messagePart/MessagePartText.d.ts +5 -2
  46. package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -1
  47. package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
  48. package/dist/primitives/reasoning/useScrollLock.js +11 -2
  49. package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
  50. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  51. package/dist/primitives/thread/useThreadViewportAutoScroll.js +5 -0
  52. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  53. package/dist/unstable/useComposerInputHistory.d.ts +30 -0
  54. package/dist/unstable/useComposerInputHistory.d.ts.map +1 -0
  55. package/dist/unstable/useComposerInputHistory.js +117 -0
  56. package/dist/unstable/useComposerInputHistory.js.map +1 -0
  57. package/dist/utils/smooth/useSmooth.d.ts +40 -2
  58. package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
  59. package/dist/utils/smooth/useSmooth.js +48 -9
  60. package/dist/utils/smooth/useSmooth.js.map +1 -1
  61. package/package.json +31 -24
  62. package/src/client/ExternalThread.ts +70 -27
  63. package/src/client/InMemoryThreadList.ts +11 -7
  64. package/src/client/SingleThreadList.ts +29 -27
  65. package/src/index.ts +8 -0
  66. package/src/mcp-apps/McpAppRenderer.tsx +5 -3
  67. package/src/mcp-apps/McpAppsRemoteHost.ts +5 -3
  68. package/src/primitives/composer/ComposerInput.test.tsx +1 -1
  69. package/src/primitives/composer/ComposerInput.tsx +3 -3
  70. package/src/primitives/composer/trigger/TriggerPopoverResource.ts +5 -3
  71. package/src/primitives/composer/trigger/triggerDetectionResource.ts +21 -21
  72. package/src/primitives/composer/trigger/triggerKeyboardResource.test.ts +5 -4
  73. package/src/primitives/composer/trigger/triggerKeyboardResource.ts +99 -101
  74. package/src/primitives/composer/trigger/triggerNavigationResource.ts +92 -98
  75. package/src/primitives/composer/trigger/triggerSelectionResource.ts +76 -76
  76. package/src/primitives/messagePart/MessagePartText.tsx +3 -2
  77. package/src/primitives/reasoning/useScrollLock.ts +25 -2
  78. package/src/primitives/thread/useThreadViewportAutoScroll.ts +8 -0
  79. package/src/tests/external-thread-branches.test.tsx +160 -0
  80. package/src/tests/shouldContinue.test.ts +33 -0
  81. package/src/unstable/useComposerInputHistory.test.tsx +201 -0
  82. package/src/unstable/useComposerInputHistory.ts +160 -0
  83. package/src/utils/smooth/useSmooth.test.tsx +95 -0
  84. package/src/utils/smooth/useSmooth.ts +82 -10
@@ -5,12 +5,17 @@ import { useAui, useAuiState } from "@assistant-ui/store";
5
5
  import { useEffect, useMemo, useRef, useState } from "@assistant-ui/tap/react-shim";
6
6
  import { useCallbackRef } from "@radix-ui/react-use-callback-ref";
7
7
  //#region src/utils/smooth/useSmooth.ts
8
+ const DEFAULT_DRAIN_MS = 250;
9
+ const DEFAULT_MAX_CHAR_INTERVAL_MS = 5;
8
10
  var TextStreamAnimator = class {
9
11
  currentText;
10
12
  setText;
11
13
  animationFrameId = null;
12
14
  lastUpdateTime = Date.now();
13
15
  targetText = "";
16
+ drainMs = DEFAULT_DRAIN_MS;
17
+ maxCharIntervalMs = DEFAULT_MAX_CHAR_INTERVAL_MS;
18
+ maxCharsPerFrame = Infinity;
14
19
  constructor(currentText, setText) {
15
20
  this.currentText = currentText;
16
21
  this.setText = setText;
@@ -30,12 +35,14 @@ var TextStreamAnimator = class {
30
35
  const currentTime = Date.now();
31
36
  let timeToConsume = currentTime - this.lastUpdateTime;
32
37
  const remainingChars = this.targetText.length - this.currentText.length;
33
- const baseTimePerChar = Math.min(5, 250 / remainingChars);
38
+ const baseTimePerChar = Math.min(this.maxCharIntervalMs, this.drainMs / remainingChars);
39
+ const frameLimit = Math.min(remainingChars, this.maxCharsPerFrame);
34
40
  let charsToAdd = 0;
35
- while (timeToConsume >= baseTimePerChar && charsToAdd < remainingChars) {
41
+ while (timeToConsume >= baseTimePerChar && charsToAdd < frameLimit) {
36
42
  charsToAdd++;
37
43
  timeToConsume -= baseTimePerChar;
38
44
  }
45
+ if (charsToAdd === frameLimit && frameLimit === this.maxCharsPerFrame) timeToConsume = 0;
39
46
  if (charsToAdd !== remainingChars) this.animationFrameId = requestAnimationFrame(this.animate);
40
47
  else this.animationFrameId = null;
41
48
  if (charsToAdd === 0) return;
@@ -45,8 +52,30 @@ var TextStreamAnimator = class {
45
52
  };
46
53
  };
47
54
  const SMOOTH_STATUS = Object.freeze({ type: "running" });
55
+ const positiveOr = (value, fallback) => value !== void 0 && value > 0 ? value : fallback;
56
+ /**
57
+ * Animates streamed message part text with a typewriter-style reveal.
58
+ *
59
+ * Takes the current part state and a `smooth` argument: `false` disables,
60
+ * `true` uses the default rate, and a {@link SmoothOptions} object tunes
61
+ * the reveal. Returns the part state with `text` replaced by the revealed
62
+ * prefix and `status` reporting `running` until the reveal catches up.
63
+ *
64
+ * @example
65
+ * ```tsx
66
+ * const { text, status } = useSmooth(useMessagePartText(), {
67
+ * drainMs: 500,
68
+ * maxCharsPerFrame: 30,
69
+ * });
70
+ * ```
71
+ */
48
72
  const useSmooth = (state, smooth = false) => {
49
73
  const { text } = state;
74
+ const options = typeof smooth === "object" && smooth !== null ? smooth : void 0;
75
+ const enabled = smooth !== false && smooth !== null;
76
+ const drainMs = positiveOr(options?.drainMs, DEFAULT_DRAIN_MS);
77
+ const maxCharIntervalMs = positiveOr(options?.maxCharIntervalMs, DEFAULT_MAX_CHAR_INTERVAL_MS);
78
+ const maxCharsPerFrame = positiveOr(options?.maxCharsPerFrame, Infinity);
50
79
  const [displayedText, setDisplayedText] = useState(state.status.type === "running" ? "" : text);
51
80
  const aui = useAui();
52
81
  const part = useAuiState(() => aui.part());
@@ -65,20 +94,30 @@ const useSmooth = (state, smooth = false) => {
65
94
  });
66
95
  useEffect(() => {
67
96
  if (smoothStatusStore) {
68
- const target = smooth && (displayedText !== text || state.status.type === "running") ? SMOOTH_STATUS : state.status;
97
+ const target = enabled && (displayedText !== text || state.status.type === "running") ? SMOOTH_STATUS : state.status;
69
98
  writableStore(smoothStatusStore).setState(target, true);
70
99
  }
71
100
  }, [
72
101
  smoothStatusStore,
73
- smooth,
102
+ enabled,
74
103
  text,
75
104
  displayedText,
76
105
  state.status
77
106
  ]);
78
107
  const [animatorRef] = useState(new TextStreamAnimator(displayedText, setText));
108
+ useEffect(() => {
109
+ animatorRef.drainMs = drainMs;
110
+ animatorRef.maxCharIntervalMs = maxCharIntervalMs;
111
+ animatorRef.maxCharsPerFrame = maxCharsPerFrame;
112
+ }, [
113
+ animatorRef,
114
+ drainMs,
115
+ maxCharIntervalMs,
116
+ maxCharsPerFrame
117
+ ]);
79
118
  const animatorPartRef = useRef(part);
80
119
  useEffect(() => {
81
- if (!smooth) {
120
+ if (!enabled) {
82
121
  animatorRef.stop();
83
122
  return;
84
123
  }
@@ -100,7 +139,7 @@ const useSmooth = (state, smooth = false) => {
100
139
  animatorRef.start();
101
140
  }, [
102
141
  animatorRef,
103
- smooth,
142
+ enabled,
104
143
  text,
105
144
  state.status.type,
106
145
  part
@@ -110,12 +149,12 @@ const useSmooth = (state, smooth = false) => {
110
149
  animatorRef.stop();
111
150
  };
112
151
  }, [animatorRef]);
113
- return useMemo(() => smooth ? {
114
- type: "text",
152
+ return useMemo(() => enabled ? {
153
+ ...state,
115
154
  text: displayedText,
116
155
  status: text === displayedText ? state.status : SMOOTH_STATUS
117
156
  } : state, [
118
- smooth,
157
+ enabled,
119
158
  displayedText,
120
159
  state,
121
160
  text
@@ -1 +1 @@
1
- {"version":3,"file":"useSmooth.js","names":[],"sources":["../../../src/utils/smooth/useSmooth.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport type {\n MessagePartStatus,\n ReasoningMessagePart,\n TextMessagePart,\n MessagePartState,\n} from \"@assistant-ui/core\";\nimport { useCallbackRef } from \"@radix-ui/react-use-callback-ref\";\nimport { useSmoothStatusStore } from \"./SmoothContext\";\nimport { writableStore } from \"../../context/ReadonlyStore\";\n\nclass TextStreamAnimator {\n private animationFrameId: number | null = null;\n private lastUpdateTime: number = Date.now();\n\n public targetText: string = \"\";\n\n constructor(\n public currentText: string,\n private setText: (newText: string) => void,\n ) {}\n\n start() {\n if (this.animationFrameId !== null) return;\n this.lastUpdateTime = Date.now();\n this.animate();\n }\n\n stop() {\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n }\n\n private animate = () => {\n const currentTime = Date.now();\n const deltaTime = currentTime - this.lastUpdateTime;\n let timeToConsume = deltaTime;\n\n const remainingChars = this.targetText.length - this.currentText.length;\n const baseTimePerChar = Math.min(5, 250 / remainingChars);\n\n let charsToAdd = 0;\n while (timeToConsume >= baseTimePerChar && charsToAdd < remainingChars) {\n charsToAdd++;\n timeToConsume -= baseTimePerChar;\n }\n\n if (charsToAdd !== remainingChars) {\n this.animationFrameId = requestAnimationFrame(this.animate);\n } else {\n this.animationFrameId = null;\n }\n if (charsToAdd === 0) return;\n\n this.currentText = this.targetText.slice(\n 0,\n this.currentText.length + charsToAdd,\n );\n this.lastUpdateTime = currentTime - timeToConsume;\n this.setText(this.currentText);\n };\n}\n\nconst SMOOTH_STATUS: MessagePartStatus = Object.freeze({\n type: \"running\",\n});\n\nexport const useSmooth = (\n state: MessagePartState & (TextMessagePart | ReasoningMessagePart),\n smooth: boolean = false,\n): MessagePartState & (TextMessagePart | ReasoningMessagePart) => {\n const { text } = state;\n\n const [displayedText, setDisplayedText] = useState(\n state.status.type === \"running\" ? \"\" : text,\n );\n\n // Render-phase resync on part flip or text discontinuity, so the\n // first paint after a thread switch never shows the previous\n // part's text (#4051). `displayedText` is already a prefix of\n // `text` during normal streaming, so use it as the previous-text\n // reference instead of carrying separate state — avoids the\n // double render per streaming token. Read part identity through\n // `useAuiState` so we actually subscribe to its changes instead\n // of relying on a render-time proxy reference that may be stable\n // across thread swaps.\n const aui = useAui();\n const part = useAuiState(() => aui.part());\n const [prevPart, setPrevPart] = useState(part);\n if (part !== prevPart || !text.startsWith(displayedText)) {\n setPrevPart(part);\n setDisplayedText(state.status.type === \"running\" ? \"\" : text);\n }\n\n const smoothStatusStore = useSmoothStatusStore({ optional: true });\n const setText = useCallbackRef((text: string) => {\n setDisplayedText(text);\n if (smoothStatusStore) {\n const target =\n displayedText !== text || state.status.type === \"running\"\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n });\n\n // TODO this is hacky\n useEffect(() => {\n if (smoothStatusStore) {\n const target =\n smooth && (displayedText !== text || state.status.type === \"running\")\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n }, [smoothStatusStore, smooth, text, displayedText, state.status]);\n\n const [animatorRef] = useState<TextStreamAnimator>(\n new TextStreamAnimator(displayedText, setText),\n );\n\n const animatorPartRef = useRef(part);\n useEffect(() => {\n if (!smooth) {\n animatorRef.stop();\n return;\n }\n\n // Discontinuity: part flipped, or new text breaks continuation\n // of the animator's current target. Either case requires\n // resetting the cursor — without the part check, a new part\n // whose text happens to share a prefix with the previous target\n // would keep the stale cursor and flicker.\n const partChanged = animatorPartRef.current !== part;\n animatorPartRef.current = part;\n if (partChanged || !text.startsWith(animatorRef.targetText)) {\n if (state.status.type === \"running\") {\n animatorRef.currentText = \"\";\n animatorRef.targetText = text;\n animatorRef.start();\n } else {\n animatorRef.currentText = text;\n animatorRef.targetText = text;\n animatorRef.stop();\n }\n return;\n }\n\n animatorRef.targetText = text;\n animatorRef.start();\n }, [animatorRef, smooth, text, state.status.type, part]);\n\n useEffect(() => {\n return () => {\n animatorRef.stop();\n };\n }, [animatorRef]);\n\n return useMemo(\n () =>\n smooth\n ? {\n type: \"text\",\n text: displayedText,\n status: text === displayedText ? state.status : SMOOTH_STATUS,\n }\n : state,\n [smooth, displayedText, state, text],\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useSmooth.js","names":[],"sources":["../../../src/utils/smooth/useSmooth.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport type {\n MessagePartStatus,\n ReasoningMessagePart,\n TextMessagePart,\n MessagePartState,\n} from \"@assistant-ui/core\";\nimport { useCallbackRef } from \"@radix-ui/react-use-callback-ref\";\nimport { useSmoothStatusStore } from \"./SmoothContext\";\nimport { writableStore } from \"../../context/ReadonlyStore\";\n\n/**\n * Tuning options for the smooth text streaming animation.\n */\nexport type SmoothOptions = {\n /**\n * Target time in milliseconds to drain the backlog of unrevealed\n * characters. Larger values reveal long backlogs more gradually.\n * @default 250\n */\n drainMs?: number | undefined;\n /**\n * Maximum time in milliseconds between revealed characters, i.e. the\n * slowest reveal rate when the backlog is short.\n * @default 5\n */\n maxCharIntervalMs?: number | undefined;\n /**\n * Maximum number of characters revealed per animation frame.\n * @default Infinity\n */\n maxCharsPerFrame?: number | undefined;\n};\n\nconst DEFAULT_DRAIN_MS = 250;\nconst DEFAULT_MAX_CHAR_INTERVAL_MS = 5;\n\nclass TextStreamAnimator {\n private animationFrameId: number | null = null;\n private lastUpdateTime: number = Date.now();\n\n public targetText: string = \"\";\n public drainMs: number = DEFAULT_DRAIN_MS;\n public maxCharIntervalMs: number = DEFAULT_MAX_CHAR_INTERVAL_MS;\n public maxCharsPerFrame: number = Infinity;\n\n constructor(\n public currentText: string,\n private setText: (newText: string) => void,\n ) {}\n\n start() {\n if (this.animationFrameId !== null) return;\n this.lastUpdateTime = Date.now();\n this.animate();\n }\n\n stop() {\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n }\n\n private animate = () => {\n const currentTime = Date.now();\n const deltaTime = currentTime - this.lastUpdateTime;\n let timeToConsume = deltaTime;\n\n const remainingChars = this.targetText.length - this.currentText.length;\n const baseTimePerChar = Math.min(\n this.maxCharIntervalMs,\n this.drainMs / remainingChars,\n );\n\n const frameLimit = Math.min(remainingChars, this.maxCharsPerFrame);\n let charsToAdd = 0;\n while (timeToConsume >= baseTimePerChar && charsToAdd < frameLimit) {\n charsToAdd++;\n timeToConsume -= baseTimePerChar;\n }\n // A cap-limited frame must not bank its surplus time, or the next\n // frame would burst past the cap.\n if (charsToAdd === frameLimit && frameLimit === this.maxCharsPerFrame) {\n timeToConsume = 0;\n }\n\n if (charsToAdd !== remainingChars) {\n this.animationFrameId = requestAnimationFrame(this.animate);\n } else {\n this.animationFrameId = null;\n }\n if (charsToAdd === 0) return;\n\n this.currentText = this.targetText.slice(\n 0,\n this.currentText.length + charsToAdd,\n );\n this.lastUpdateTime = currentTime - timeToConsume;\n this.setText(this.currentText);\n };\n}\n\nconst SMOOTH_STATUS: MessagePartStatus = Object.freeze({\n type: \"running\",\n});\n\nconst positiveOr = (value: number | undefined, fallback: number): number =>\n value !== undefined && value > 0 ? value : fallback;\n\n/**\n * Animates streamed message part text with a typewriter-style reveal.\n *\n * Takes the current part state and a `smooth` argument: `false` disables,\n * `true` uses the default rate, and a {@link SmoothOptions} object tunes\n * the reveal. Returns the part state with `text` replaced by the revealed\n * prefix and `status` reporting `running` until the reveal catches up.\n *\n * @example\n * ```tsx\n * const { text, status } = useSmooth(useMessagePartText(), {\n * drainMs: 500,\n * maxCharsPerFrame: 30,\n * });\n * ```\n */\nexport const useSmooth = (\n state: MessagePartState & (TextMessagePart | ReasoningMessagePart),\n smooth: boolean | SmoothOptions = false,\n): MessagePartState & (TextMessagePart | ReasoningMessagePart) => {\n const { text } = state;\n const options =\n typeof smooth === \"object\" && smooth !== null ? smooth : undefined;\n const enabled = smooth !== false && smooth !== null;\n const drainMs = positiveOr(options?.drainMs, DEFAULT_DRAIN_MS);\n const maxCharIntervalMs = positiveOr(\n options?.maxCharIntervalMs,\n DEFAULT_MAX_CHAR_INTERVAL_MS,\n );\n const maxCharsPerFrame = positiveOr(options?.maxCharsPerFrame, Infinity);\n\n const [displayedText, setDisplayedText] = useState(\n state.status.type === \"running\" ? \"\" : text,\n );\n\n // Render-phase resync on part flip or text discontinuity, so the\n // first paint after a thread switch never shows the previous\n // part's text (#4051). `displayedText` is already a prefix of\n // `text` during normal streaming, so use it as the previous-text\n // reference instead of carrying separate state — avoids the\n // double render per streaming token. Read part identity through\n // `useAuiState` so we actually subscribe to its changes instead\n // of relying on a render-time proxy reference that may be stable\n // across thread swaps.\n const aui = useAui();\n const part = useAuiState(() => aui.part());\n const [prevPart, setPrevPart] = useState(part);\n if (part !== prevPart || !text.startsWith(displayedText)) {\n setPrevPart(part);\n setDisplayedText(state.status.type === \"running\" ? \"\" : text);\n }\n\n const smoothStatusStore = useSmoothStatusStore({ optional: true });\n const setText = useCallbackRef((text: string) => {\n setDisplayedText(text);\n if (smoothStatusStore) {\n const target =\n displayedText !== text || state.status.type === \"running\"\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n });\n\n // TODO this is hacky\n useEffect(() => {\n if (smoothStatusStore) {\n const target =\n enabled && (displayedText !== text || state.status.type === \"running\")\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n }, [smoothStatusStore, enabled, text, displayedText, state.status]);\n\n const [animatorRef] = useState<TextStreamAnimator>(\n new TextStreamAnimator(displayedText, setText),\n );\n\n useEffect(() => {\n animatorRef.drainMs = drainMs;\n animatorRef.maxCharIntervalMs = maxCharIntervalMs;\n animatorRef.maxCharsPerFrame = maxCharsPerFrame;\n }, [animatorRef, drainMs, maxCharIntervalMs, maxCharsPerFrame]);\n\n const animatorPartRef = useRef(part);\n useEffect(() => {\n if (!enabled) {\n animatorRef.stop();\n return;\n }\n\n // Discontinuity: part flipped, or new text breaks continuation\n // of the animator's current target. Either case requires\n // resetting the cursor — without the part check, a new part\n // whose text happens to share a prefix with the previous target\n // would keep the stale cursor and flicker.\n const partChanged = animatorPartRef.current !== part;\n animatorPartRef.current = part;\n if (partChanged || !text.startsWith(animatorRef.targetText)) {\n if (state.status.type === \"running\") {\n animatorRef.currentText = \"\";\n animatorRef.targetText = text;\n animatorRef.start();\n } else {\n animatorRef.currentText = text;\n animatorRef.targetText = text;\n animatorRef.stop();\n }\n return;\n }\n\n animatorRef.targetText = text;\n animatorRef.start();\n }, [animatorRef, enabled, text, state.status.type, part]);\n\n useEffect(() => {\n return () => {\n animatorRef.stop();\n };\n }, [animatorRef]);\n\n return useMemo(\n () =>\n enabled\n ? {\n ...state,\n text: displayedText,\n status: text === displayedText ? state.status : SMOOTH_STATUS,\n }\n : state,\n [enabled, displayedText, state, text],\n );\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,33 +1,40 @@
1
1
  {
2
2
  "name": "@assistant-ui/react",
3
- "version": "0.14.15",
3
+ "version": "0.14.18",
4
4
  "description": "Open-source TypeScript/React library for building production-grade AI chat experiences",
5
5
  "keywords": [
6
- "radix-ui",
7
- "nextjs",
8
- "vercel",
9
- "ai-sdk",
10
- "react",
11
- "components",
6
+ "ai",
12
7
  "ui",
13
- "frontend",
14
- "tailwind",
15
- "shadcn",
8
+ "sdk",
9
+ "mcp",
10
+ "agent",
11
+ "agentic",
12
+ "generative",
13
+ "components",
16
14
  "assistant",
17
- "openai",
18
- "ai",
19
15
  "chat",
20
16
  "chatbot",
21
- "copilot",
22
- "ai-chat",
23
- "ai-chatbot",
24
- "ai-assistant",
25
- "ai-copilot",
17
+ "shadcn",
18
+ "radix-ui",
19
+ "tailwind",
20
+ "javascript",
21
+ "openai",
22
+ "anthropic",
23
+ "claude",
24
+ "xai",
26
25
  "chatgpt",
27
- "gpt4",
28
- "gpt-4",
29
- "conversational-ui",
30
- "conversational-ai"
26
+ "gemini",
27
+ "grok",
28
+ "react",
29
+ "framework",
30
+ "nextjs",
31
+ "web",
32
+ "server",
33
+ "node",
34
+ "front-end",
35
+ "backend",
36
+ "cli",
37
+ "vercel"
31
38
  ],
32
39
  "author": "AgentbaseAI Inc.",
33
40
  "license": "MIT",
@@ -48,9 +55,9 @@
48
55
  ],
49
56
  "sideEffects": false,
50
57
  "dependencies": {
51
- "@assistant-ui/core": "^0.2.11",
52
- "@assistant-ui/store": "^0.2.14",
53
- "@assistant-ui/tap": "^0.6.0",
58
+ "@assistant-ui/core": "^0.2.14",
59
+ "@assistant-ui/store": "^0.2.16",
60
+ "@assistant-ui/tap": "^0.7.1",
54
61
  "@radix-ui/primitive": "^1.1.4",
55
62
  "@radix-ui/react-compose-refs": "^1.1.3",
56
63
  "@radix-ui/react-context": "^1.1.4",
@@ -17,14 +17,17 @@ import type {
17
17
  ThreadUserMessagePart,
18
18
  ThreadMessage,
19
19
  ExternalThreadQueueAdapter,
20
+ ExternalThreadBranchAdapter,
20
21
  } from "@assistant-ui/core";
21
22
  import type { QueueItemState } from "@assistant-ui/core/store";
22
23
  import type { ComposerSendOptions } from "@assistant-ui/core/store";
24
+ import { getThreadMessageText } from "@assistant-ui/core/internal";
23
25
  import { ModelContext, Suggestions } from "@assistant-ui/core/store";
24
26
  import { Tools, DataRenderers } from "@assistant-ui/core/react";
25
27
  import { SingleThreadList } from "./SingleThreadList";
26
28
 
27
29
  const EMPTY_QUEUE_ITEMS: readonly QueueItemState[] = [];
30
+ const EMPTY_BRANCH_IDS: readonly string[] = [];
28
31
 
29
32
  export type ExternalThreadMessage = ThreadMessage & {
30
33
  id: string;
@@ -51,6 +54,8 @@ export type ExternalThreadProps = {
51
54
  onCancel?: () => void;
52
55
  /** Queue adapter for runtimes that support message queuing and steering. */
53
56
  queue?: ExternalThreadQueueAdapter;
57
+ /** Branch adapter for runtimes that track sibling variants of messages. */
58
+ branches?: ExternalThreadBranchAdapter;
54
59
  };
55
60
 
56
61
  type MessageClientProps = {
@@ -59,16 +64,18 @@ type MessageClientProps = {
59
64
  onEdit?: (message: AppendMessage) => void;
60
65
  onReload?: () => void;
61
66
  queue?: ExternalThreadQueueAdapter | undefined;
67
+ branches?: ExternalThreadBranchAdapter | undefined;
62
68
  };
63
69
 
64
70
  // Message Client - minimal implementation
65
- const MessageClient = resource(function MessageClient({
71
+ const useMessageClient = ({
66
72
  message,
67
73
  index,
68
74
  onEdit,
69
75
  onReload,
70
76
  queue,
71
- }: MessageClientProps): ClientOutput<"message"> {
77
+ branches,
78
+ }: MessageClientProps): ClientOutput<"message"> => {
72
79
  const [isCopied, setIsCopied] = useState(false);
73
80
  const [isHovering, setIsHovering] = useState(false);
74
81
  const [isEditing, setIsEditing] = useState(false);
@@ -124,14 +131,19 @@ const MessageClient = resource(function MessageClient({
124
131
  }),
125
132
  );
126
133
 
134
+ const branchIds = branches?.getBranches(message.id) ?? EMPTY_BRANCH_IDS;
135
+ const branchIndex = branchIds.indexOf(message.id);
136
+ const branchNumber = branchIndex === -1 ? 1 : branchIndex + 1;
137
+ const branchCount = branchIndex === -1 ? 1 : branchIds.length;
138
+
127
139
  const state = useMemo(() => {
128
140
  return {
129
141
  ...message,
130
142
  attachments: message.attachments ?? [],
131
143
  parentId: null,
132
144
  isLast: false, // Will be set by thread
133
- branchNumber: 1,
134
- branchCount: 1,
145
+ branchNumber,
146
+ branchCount,
135
147
  speech: undefined,
136
148
  parts: partClients.state,
137
149
  isCopied,
@@ -146,20 +158,35 @@ const MessageClient = resource(function MessageClient({
146
158
  index,
147
159
  composerClient.state,
148
160
  partClients.state,
161
+ branchNumber,
162
+ branchCount,
149
163
  ]);
150
164
 
151
165
  return {
152
166
  getState: () => state,
153
167
  composer: () => composerClient.methods,
168
+ delete: () => {},
154
169
  reload: () => {
155
170
  onReload?.();
156
171
  },
157
172
  speak: () => {},
158
173
  stopSpeaking: () => {},
159
174
  submitFeedback: () => {},
160
- switchToBranch: () => {},
161
- getCopyText: () =>
162
- message.content.map((c) => ("text" in c ? c.text : "")).join(""),
175
+ switchToBranch: ({ position, branchId }) => {
176
+ if (!branches) return;
177
+ const target =
178
+ branchId ??
179
+ (branchIndex === -1
180
+ ? undefined
181
+ : position === "previous"
182
+ ? branchIds[branchIndex - 1]
183
+ : position === "next"
184
+ ? branchIds[branchIndex + 1]
185
+ : undefined);
186
+ if (target !== undefined && target !== message.id)
187
+ branches.switchToBranch(target);
188
+ },
189
+ getCopyText: () => getThreadMessageText(message),
163
190
  part: (selector) => {
164
191
  if ("index" in selector) {
165
192
  return partClients.get(selector);
@@ -178,16 +205,16 @@ const MessageClient = resource(function MessageClient({
178
205
  setIsCopied,
179
206
  setIsHovering,
180
207
  };
181
- });
208
+ };
209
+
210
+ const MessageClient = resource(useMessageClient);
182
211
 
183
212
  type PartResourceProps = {
184
213
  part: ThreadAssistantMessagePart | ThreadUserMessagePart;
185
214
  };
186
215
 
187
216
  // Part Client - minimal implementation
188
- const PartResource = resource(function PartResource({
189
- part,
190
- }: PartResourceProps): ClientOutput<"part"> {
217
+ const usePartResource = ({ part }: PartResourceProps): ClientOutput<"part"> => {
191
218
  const state = useMemo(
192
219
  () => ({
193
220
  ...part,
@@ -202,7 +229,9 @@ const PartResource = resource(function PartResource({
202
229
  resumeToolCall: () => {},
203
230
  respondToToolApproval: () => {},
204
231
  };
205
- });
232
+ };
233
+
234
+ const PartResource = resource(usePartResource);
206
235
 
207
236
  type AttachmentResourceProps = {
208
237
  attachment: Attachment;
@@ -210,17 +239,19 @@ type AttachmentResourceProps = {
210
239
  };
211
240
 
212
241
  // Attachment Client - minimal implementation
213
- const AttachmentResource = resource(function AttachmentResource({
242
+ const useAttachmentResource = ({
214
243
  attachment,
215
244
  onRemove,
216
- }: AttachmentResourceProps): ClientOutput<"attachment"> {
245
+ }: AttachmentResourceProps): ClientOutput<"attachment"> => {
217
246
  return {
218
247
  getState: () => attachment,
219
248
  remove: async () => {
220
249
  onRemove?.();
221
250
  },
222
251
  };
223
- });
252
+ };
253
+
254
+ const AttachmentResource = resource(useAttachmentResource);
224
255
 
225
256
  type ComposerClientResourceProps = {
226
257
  type: "thread" | "edit";
@@ -234,7 +265,7 @@ type ComposerClientResourceProps = {
234
265
  queue?: ExternalThreadQueueAdapter | undefined;
235
266
  };
236
267
 
237
- const QueueItemClient = resource(function QueueItemClient({
268
+ const useQueueItemClient = ({
238
269
  item,
239
270
  onSteer,
240
271
  onRemove,
@@ -242,16 +273,18 @@ const QueueItemClient = resource(function QueueItemClient({
242
273
  item: QueueItemState;
243
274
  onSteer: () => void;
244
275
  onRemove: () => void;
245
- }): ClientOutput<"queueItem"> {
276
+ }): ClientOutput<"queueItem"> => {
246
277
  return {
247
278
  getState: () => item,
248
279
  steer: onSteer,
249
280
  remove: onRemove,
250
281
  };
251
- });
282
+ };
283
+
284
+ const QueueItemClient = resource(useQueueItemClient);
252
285
 
253
286
  // Composer Client - minimal implementation
254
- const ComposerClientResource = resource(function ComposerClientResource({
287
+ const useComposerClientResource = ({
255
288
  type,
256
289
  isEditing,
257
290
  canCancel,
@@ -261,7 +294,7 @@ const ComposerClientResource = resource(function ComposerClientResource({
261
294
  onSend,
262
295
  message,
263
296
  queue,
264
- }: ComposerClientResourceProps): ClientOutput<"composer"> {
297
+ }: ComposerClientResourceProps): ClientOutput<"composer"> => {
265
298
  const [text, setText] = useState("");
266
299
  const [role, setRole] = useState<"user" | "assistant" | "system">("user");
267
300
  const [runConfig, setRunConfig] = useState<Record<string, unknown>>({});
@@ -436,10 +469,12 @@ const ComposerClientResource = resource(function ComposerClientResource({
436
469
  return queueItemClients.get(selector);
437
470
  },
438
471
  };
439
- });
472
+ };
473
+
474
+ const ComposerClientResource = resource(useComposerClientResource);
440
475
 
441
476
  // External Thread Client
442
- export const ExternalThread = resource(function ExternalThread({
477
+ const useExternalThread = ({
443
478
  messages,
444
479
  isRunning = false,
445
480
  isSendDisabled = false,
@@ -449,7 +484,8 @@ export const ExternalThread = resource(function ExternalThread({
449
484
  onStartRun,
450
485
  onCancel,
451
486
  queue,
452
- }: ExternalThreadProps): ClientOutput<"thread"> {
487
+ branches,
488
+ }: ExternalThreadProps): ClientOutput<"thread"> => {
453
489
  const handleReload = (messageId: string) => {
454
490
  const messageIndex = messages.findIndex((m) => m.id === messageId);
455
491
  if (messageIndex === -1) return;
@@ -467,11 +503,12 @@ export const ExternalThread = resource(function ExternalThread({
467
503
  index,
468
504
  onReload: () => handleReload(msg.id),
469
505
  queue,
506
+ branches,
470
507
  };
471
508
  if (onEdit) props.onEdit = onEdit;
472
509
  return withKey(msg.id, MessageClient(props));
473
510
  }),
474
- [messages, onEdit, queue],
511
+ [messages, onEdit, queue, branches],
475
512
  );
476
513
 
477
514
  const handleCancelRun = () => {
@@ -496,6 +533,7 @@ export const ExternalThread = resource(function ExternalThread({
496
533
  );
497
534
 
498
535
  const hasQueue = !!queue;
536
+ const hasBranches = !!branches;
499
537
  const state = useMemo(() => {
500
538
  const messageStates = messageClients.state.map((s, idx, arr) => ({
501
539
  ...s,
@@ -509,13 +547,14 @@ export const ExternalThread = resource(function ExternalThread({
509
547
  isRunning,
510
548
  capabilities: {
511
549
  edit: false,
550
+ delete: false,
512
551
  reload: false,
513
552
  cancel: isRunning,
514
553
  speech: false,
515
554
  attachments: false,
516
555
  feedback: false,
517
556
  voice: false,
518
- switchToBranch: false,
557
+ switchToBranch: hasBranches,
519
558
  switchBranchDuringRun: false,
520
559
  unstable_copy: false,
521
560
  dictation: false,
@@ -533,6 +572,7 @@ export const ExternalThread = resource(function ExternalThread({
533
572
  messages,
534
573
  isRunning,
535
574
  hasQueue,
575
+ hasBranches,
536
576
  messageClients.state,
537
577
  composerClient.state,
538
578
  ]);
@@ -570,6 +610,7 @@ export const ExternalThread = resource(function ExternalThread({
570
610
  onNew?.(appendMessage);
571
611
  }
572
612
  },
613
+ deleteMessage: () => {},
573
614
  startRun: () => {
574
615
  onStartRun?.();
575
616
  },
@@ -593,9 +634,11 @@ export const ExternalThread = resource(function ExternalThread({
593
634
  muteVoice: () => {},
594
635
  unmuteVoice: () => {},
595
636
  };
596
- });
637
+ };
638
+
639
+ export const ExternalThread = resource(useExternalThread);
597
640
 
598
- attachTransformScopes(ExternalThread, (scopes, parent) => {
641
+ attachTransformScopes(useExternalThread, (scopes, parent) => {
599
642
  if (!scopes.threads && parent.threads.source === null) {
600
643
  const threadElement = scopes.thread as ClientElement<"thread">;
601
644
  scopes.threads = SingleThreadList({ thread: threadElement });
@@ -27,14 +27,14 @@ type ThreadData = {
27
27
  };
28
28
 
29
29
  // ThreadListItem Client
30
- const ThreadListItemClient = resource(function ThreadListItemClient(props: {
30
+ const useThreadListItemClient = (props: {
31
31
  data: ThreadData;
32
32
  onSwitchTo: () => void;
33
33
  onUpdateCustom: (custom: Record<string, unknown> | undefined) => void;
34
34
  onArchive: () => void;
35
35
  onUnarchive: () => void;
36
36
  onDelete: () => void;
37
- }): ClientOutput<"threadListItem"> {
37
+ }): ClientOutput<"threadListItem"> => {
38
38
  const { data, onSwitchTo, onUpdateCustom, onArchive, onUnarchive, onDelete } =
39
39
  props;
40
40
  const state = useMemo(
@@ -61,12 +61,14 @@ const ThreadListItemClient = resource(function ThreadListItemClient(props: {
61
61
  initialize: async () => ({ remoteId: data.id, externalId: undefined }),
62
62
  detach: () => {},
63
63
  };
64
- });
64
+ };
65
+
66
+ const ThreadListItemClient = resource(useThreadListItemClient);
65
67
 
66
68
  // InMemoryThreadList Client
67
- export const InMemoryThreadList = resource(function InMemoryThreadList(
69
+ const useInMemoryThreadList = (
68
70
  props: InMemoryThreadListProps,
69
- ): ClientOutput<"threads"> {
71
+ ): ClientOutput<"threads"> => {
70
72
  const {
71
73
  thread: threadFactory,
72
74
  onSwitchToThread,
@@ -184,9 +186,11 @@ export const InMemoryThreadList = resource(function InMemoryThreadList(
184
186
  },
185
187
  thread: () => mainThreadClient.methods,
186
188
  };
187
- });
189
+ };
190
+
191
+ export const InMemoryThreadList = resource(useInMemoryThreadList);
188
192
 
189
- attachTransformScopes(InMemoryThreadList, (scopes, parent) => {
193
+ attachTransformScopes(useInMemoryThreadList, (scopes, parent) => {
190
194
  scopes.thread ??= Derived({
191
195
  source: "threads",
192
196
  query: { type: "main" },
@@ -9,31 +9,31 @@ import {
9
9
  const RESOLVED_PROMISE = Promise.resolve();
10
10
  const THREAD_ID = "default";
11
11
 
12
- const SingleThreadListItem = resource(
13
- function SingleThreadListItem(): ClientOutput<"threadListItem"> {
14
- const [custom, setCustom] = useState<Record<string, unknown> | undefined>();
12
+ const useSingleThreadListItem = (): ClientOutput<"threadListItem"> => {
13
+ const [custom, setCustom] = useState<Record<string, unknown> | undefined>();
15
14
 
16
- return {
17
- getState: () => ({
18
- id: THREAD_ID,
19
- remoteId: undefined,
20
- externalId: undefined,
21
- title: undefined,
22
- status: "regular",
23
- custom,
24
- }),
25
- switchTo: () => {},
26
- rename: () => {},
27
- updateCustom: setCustom,
28
- archive: () => {},
29
- unarchive: () => {},
30
- delete: () => {},
31
- generateTitle: () => {},
32
- initialize: async () => ({ remoteId: THREAD_ID, externalId: undefined }),
33
- detach: () => {},
34
- };
35
- },
36
- );
15
+ return {
16
+ getState: () => ({
17
+ id: THREAD_ID,
18
+ remoteId: undefined,
19
+ externalId: undefined,
20
+ title: undefined,
21
+ status: "regular",
22
+ custom,
23
+ }),
24
+ switchTo: () => {},
25
+ rename: () => {},
26
+ updateCustom: setCustom,
27
+ archive: () => {},
28
+ unarchive: () => {},
29
+ delete: () => {},
30
+ generateTitle: () => {},
31
+ initialize: async () => ({ remoteId: THREAD_ID, externalId: undefined }),
32
+ detach: () => {},
33
+ };
34
+ };
35
+
36
+ const SingleThreadListItem = resource(useSingleThreadListItem);
37
37
 
38
38
  type SingleThreadListProps = {
39
39
  thread: ClientElement<"thread">;
@@ -44,9 +44,9 @@ type SingleThreadListProps = {
44
44
  * Automatically provided by ExternalThread when no threads scope exists.
45
45
  * Mounts the provided thread resource element.
46
46
  */
47
- export const SingleThreadList = resource(function SingleThreadList({
47
+ const useSingleThreadList = ({
48
48
  thread,
49
- }: SingleThreadListProps): ClientOutput<"threads"> {
49
+ }: SingleThreadListProps): ClientOutput<"threads"> => {
50
50
  const itemClient = useClientResource(SingleThreadListItem());
51
51
  const threadClient = useClientResource(thread);
52
52
 
@@ -105,4 +105,6 @@ export const SingleThreadList = resource(function SingleThreadList({
105
105
  return threadClient.methods;
106
106
  },
107
107
  };
108
- });
108
+ };
109
+
110
+ export const SingleThreadList = resource(useSingleThreadList);