@assistant-ui/react 0.14.16 → 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.
- package/dist/client/ExternalThread.d.ts +4 -3
- package/dist/client/ExternalThread.d.ts.map +1 -1
- package/dist/client/ExternalThread.js +46 -21
- package/dist/client/ExternalThread.js.map +1 -1
- package/dist/client/InMemoryThreadList.d.ts +1 -1
- package/dist/client/InMemoryThreadList.d.ts.map +1 -1
- package/dist/client/InMemoryThreadList.js +7 -5
- package/dist/client/InMemoryThreadList.js.map +1 -1
- package/dist/client/SingleThreadList.d.ts +1 -6
- package/dist/client/SingleThreadList.d.ts.map +1 -1
- package/dist/client/SingleThreadList.js +6 -4
- package/dist/client/SingleThreadList.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/mcp-apps/McpAppRenderer.d.ts +2 -10
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +3 -2
- package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/mcp-apps/McpAppsRemoteHost.d.ts +1 -8
- package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppsRemoteHost.js +3 -2
- package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +3 -3
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts +2 -10
- package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverResource.js +3 -2
- package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts +2 -6
- package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerDetectionResource.js +3 -2
- package/dist/primitives/composer/trigger/triggerDetectionResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts +2 -17
- package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerKeyboardResource.js +3 -2
- package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts +2 -10
- package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.js +3 -2
- package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts +2 -10
- package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerSelectionResource.js +3 -2
- package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
- package/dist/primitives/messagePart/MessagePartText.d.ts +5 -2
- package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -1
- package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
- package/dist/primitives/reasoning/useScrollLock.js +11 -2
- package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +5 -0
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/unstable/useComposerInputHistory.d.ts +30 -0
- package/dist/unstable/useComposerInputHistory.d.ts.map +1 -0
- package/dist/unstable/useComposerInputHistory.js +117 -0
- package/dist/unstable/useComposerInputHistory.js.map +1 -0
- package/dist/utils/smooth/useSmooth.d.ts +40 -2
- package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
- package/dist/utils/smooth/useSmooth.js +48 -9
- package/dist/utils/smooth/useSmooth.js.map +1 -1
- package/package.json +4 -4
- package/src/client/ExternalThread.ts +70 -27
- package/src/client/InMemoryThreadList.ts +11 -7
- package/src/client/SingleThreadList.ts +29 -27
- package/src/index.ts +8 -0
- package/src/mcp-apps/McpAppRenderer.tsx +5 -3
- package/src/mcp-apps/McpAppsRemoteHost.ts +5 -3
- package/src/primitives/composer/ComposerInput.test.tsx +1 -1
- package/src/primitives/composer/ComposerInput.tsx +3 -3
- package/src/primitives/composer/trigger/TriggerPopoverResource.ts +5 -3
- package/src/primitives/composer/trigger/triggerDetectionResource.ts +21 -21
- package/src/primitives/composer/trigger/triggerKeyboardResource.test.ts +5 -4
- package/src/primitives/composer/trigger/triggerKeyboardResource.ts +99 -101
- package/src/primitives/composer/trigger/triggerNavigationResource.ts +92 -98
- package/src/primitives/composer/trigger/triggerSelectionResource.ts +76 -76
- package/src/primitives/messagePart/MessagePartText.tsx +3 -2
- package/src/primitives/reasoning/useScrollLock.ts +25 -2
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +8 -0
- package/src/tests/external-thread-branches.test.tsx +160 -0
- package/src/tests/shouldContinue.test.ts +33 -0
- package/src/unstable/useComposerInputHistory.test.tsx +201 -0
- package/src/unstable/useComposerInputHistory.ts +160 -0
- package/src/utils/smooth/useSmooth.test.tsx +95 -0
- 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(
|
|
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 <
|
|
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 =
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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(() =>
|
|
114
|
-
|
|
152
|
+
return useMemo(() => enabled ? {
|
|
153
|
+
...state,
|
|
115
154
|
text: displayedText,
|
|
116
155
|
status: text === displayedText ? state.status : SMOOTH_STATUS
|
|
117
156
|
} : state, [
|
|
118
|
-
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.18",
|
|
4
4
|
"description": "Open-source TypeScript/React library for building production-grade AI chat experiences",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
],
|
|
56
56
|
"sideEffects": false,
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@assistant-ui/core": "^0.2.
|
|
59
|
-
"@assistant-ui/store": "^0.2.
|
|
60
|
-
"@assistant-ui/tap": "^0.
|
|
58
|
+
"@assistant-ui/core": "^0.2.14",
|
|
59
|
+
"@assistant-ui/store": "^0.2.16",
|
|
60
|
+
"@assistant-ui/tap": "^0.7.1",
|
|
61
61
|
"@radix-ui/primitive": "^1.1.4",
|
|
62
62
|
"@radix-ui/react-compose-refs": "^1.1.3",
|
|
63
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
|
|
71
|
+
const useMessageClient = ({
|
|
66
72
|
message,
|
|
67
73
|
index,
|
|
68
74
|
onEdit,
|
|
69
75
|
onReload,
|
|
70
76
|
queue,
|
|
71
|
-
|
|
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
|
|
134
|
-
branchCount
|
|
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
|
-
|
|
162
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
13
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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);
|
package/src/index.ts
CHANGED
|
@@ -141,6 +141,7 @@ export {
|
|
|
141
141
|
type MessageQueueDriver,
|
|
142
142
|
type MessageQueueController,
|
|
143
143
|
type ExternalThreadQueueAdapter,
|
|
144
|
+
type ExternalThreadBranchAdapter,
|
|
144
145
|
} from "@assistant-ui/core";
|
|
145
146
|
export { useExternalStoreRuntime } from "./legacy-runtime/runtime-cores/external-store/useExternalStoreRuntime";
|
|
146
147
|
export { useExternalStoreSharedOptions } from "@assistant-ui/core/react";
|
|
@@ -288,6 +289,7 @@ export { useThreadViewportAutoScroll } from "./primitives/thread/useThreadViewpo
|
|
|
288
289
|
export { useScrollLock } from "./primitives/reasoning/useScrollLock";
|
|
289
290
|
export { useMessageQuote } from "./hooks/useMessageQuote";
|
|
290
291
|
export { useMessageTiming } from "./hooks/useMessageTiming";
|
|
292
|
+
export { useSmooth, type SmoothOptions } from "./utils/smooth/useSmooth";
|
|
291
293
|
|
|
292
294
|
// Re-export core types from @assistant-ui/core
|
|
293
295
|
export type {
|
|
@@ -421,6 +423,12 @@ export type {
|
|
|
421
423
|
} from "@assistant-ui/core";
|
|
422
424
|
export { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
|
|
423
425
|
|
|
426
|
+
// Unstable - composer input history (terminal-style ArrowUp/ArrowDown recall)
|
|
427
|
+
export {
|
|
428
|
+
unstable_useComposerInputHistory,
|
|
429
|
+
type Unstable_ComposerInputHistory,
|
|
430
|
+
} from "./unstable/useComposerInputHistory";
|
|
431
|
+
|
|
424
432
|
export type { Assistant } from "./augmentations";
|
|
425
433
|
|
|
426
434
|
// --- mcp-apps ---
|
|
@@ -195,9 +195,9 @@ function InlineRenderer({
|
|
|
195
195
|
* renderer loads that resource from the configured host and displays it in a
|
|
196
196
|
* sandboxed frame.
|
|
197
197
|
*/
|
|
198
|
-
|
|
198
|
+
const useMcpAppRenderer = (
|
|
199
199
|
options: McpAppRendererOptions,
|
|
200
|
-
): { readonly render: ToolCallMessagePartComponent } {
|
|
200
|
+
): { readonly render: ToolCallMessagePartComponent } => {
|
|
201
201
|
const host = useResource(options.host);
|
|
202
202
|
|
|
203
203
|
const optionsRef = useRef<McpAppRendererOptions>(options);
|
|
@@ -219,4 +219,6 @@ export const McpAppRenderer = resource(function McpAppRenderer(
|
|
|
219
219
|
}, []);
|
|
220
220
|
|
|
221
221
|
return { render };
|
|
222
|
-
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const McpAppRenderer = resource(useMcpAppRenderer);
|
|
@@ -34,9 +34,9 @@ async function postToHost(
|
|
|
34
34
|
* params }`, using the method names expected by the assistant-ui MCP Apps
|
|
35
35
|
* guide.
|
|
36
36
|
*/
|
|
37
|
-
|
|
37
|
+
const useMcpAppsRemoteHost = (
|
|
38
38
|
options: McpAppsRemoteHostOptions,
|
|
39
|
-
): McpAppsHost {
|
|
39
|
+
): McpAppsHost => {
|
|
40
40
|
const optionsRef = useRef(options);
|
|
41
41
|
optionsRef.current = options;
|
|
42
42
|
|
|
@@ -57,4 +57,6 @@ export const McpAppsRemoteHost = resource(function McpAppsRemoteHost(
|
|
|
57
57
|
}),
|
|
58
58
|
[],
|
|
59
59
|
);
|
|
60
|
-
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const McpAppsRemoteHost = resource(useMcpAppsRemoteHost);
|