@assistant-ui/react 0.15.14 → 0.15.16
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/assistant-transport.d.ts +32 -0
- package/dist/assistant-transport.d.ts.map +1 -0
- package/dist/assistant-transport.js +27 -0
- package/dist/assistant-transport.js.map +1 -0
- package/dist/context/providers/MessageProvider.js +1 -1
- package/dist/devtools/DevToolsHooks.d.ts +0 -2
- package/dist/devtools/DevToolsHooks.d.ts.map +1 -1
- package/dist/devtools/DevToolsHooks.js +10 -12
- package/dist/devtools/DevToolsHooks.js.map +1 -1
- package/dist/hooks/useMessageQuote.js +1 -1
- package/dist/hooks/useToolCallElapsed.js +1 -1
- package/dist/index.d.ts +4 -7
- package/dist/index.js +6 -8
- package/dist/legacy-runtime/AssistantRuntimeProvider.js +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +1 -1
- package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppsRemoteHost.js +18 -2
- package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
- package/dist/mcp-apps/bridge.d.ts.map +1 -1
- package/dist/mcp-apps/bridge.js +33 -24
- package/dist/mcp-apps/bridge.js.map +1 -1
- package/dist/model-context/makeAssistantVisible.js +1 -1
- package/dist/primitives/actionBar/ActionBarCopy.js +1 -1
- package/dist/primitives/actionBar/ActionBarExportMarkdown.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarExportMarkdown.js +5 -2
- package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +1 -1
- package/dist/primitives/actionBar/ActionBarStopSpeaking.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarStopSpeaking.js +24 -37
- package/dist/primitives/actionBar/ActionBarStopSpeaking.js.map +1 -1
- package/dist/primitives/actionBar/useActionBarFloatStatus.js +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.js +1 -1
- package/dist/primitives/attachment/AttachmentName.js +1 -1
- package/dist/primitives/attachment/AttachmentRemove.js +1 -1
- package/dist/primitives/attachment/AttachmentThumb.js +1 -1
- package/dist/primitives/branchPicker/BranchPickerCount.js +1 -1
- package/dist/primitives/branchPicker/BranchPickerNumber.js +1 -1
- package/dist/primitives/branchPicker/BranchPickerRoot.d.ts.map +1 -1
- package/dist/primitives/branchPicker/BranchPickerRoot.js +20 -15
- package/dist/primitives/branchPicker/BranchPickerRoot.js.map +1 -1
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +1 -1
- package/dist/primitives/composer/ComposerAddAttachment.js +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -1
- package/dist/primitives/composer/ComposerDictationTranscript.d.ts +2 -2
- package/dist/primitives/composer/ComposerDictationTranscript.js +3 -3
- package/dist/primitives/composer/ComposerDictationTranscript.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +2 -2
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerQuote.js +1 -1
- package/dist/primitives/composer/ComposerRoot.js +1 -1
- package/dist/primitives/composer/ComposerStopDictation.js +1 -1
- package/dist/primitives/composer/trigger/TriggerPopover.js +1 -1
- package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerSelectionResource.js +24 -20
- package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
- package/dist/primitives/composer/useComposerInputState.js +1 -1
- package/dist/primitives/message/MessageIf.js +1 -1
- package/dist/primitives/message/MessagePartsGrouped.js +1 -1
- package/dist/primitives/message/MessageRoot.js +1 -1
- package/dist/primitives/message.js +1 -1
- package/dist/primitives/queueItem/QueueItemRemove.js +1 -1
- package/dist/primitives/queueItem/QueueItemSteer.js +1 -1
- package/dist/primitives/queueItem/QueueItemText.js +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +1 -1
- package/dist/primitives/suggestion/SuggestionDescription.js +1 -1
- package/dist/primitives/suggestion/SuggestionTitle.js +1 -1
- package/dist/primitives/suggestion/SuggestionTrigger.js +1 -1
- package/dist/primitives/thread/ThreadIf.js +1 -1
- package/dist/primitives/thread/ThreadRoot.d.ts +3 -0
- package/dist/primitives/thread/ThreadRoot.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadRoot.js +44 -9
- package/dist/primitives/thread/ThreadRoot.js.map +1 -1
- package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadViewport.js +2 -13
- package/dist/primitives/thread/ThreadViewport.js.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +23 -8
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/primitives/threadList/ThreadListNew.js +1 -1
- package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
- package/dist/sandbox-host/SandboxHost.js +6 -1
- package/dist/sandbox-host/SandboxHost.js.map +1 -1
- package/dist/unstable/useComposerInput.d.ts +2 -2
- package/dist/unstable/useComposerInput.js +1 -1
- package/dist/unstable/useComposerInput.js.map +1 -1
- package/dist/unstable/useComposerInputHistory.js +2 -2
- package/dist/unstable/useComposerInputHistory.js.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.d.ts +5 -0
- package/dist/unstable/useLiveCompletionAdapter.d.ts.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.js +16 -3
- package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
- package/dist/unstable/useMentionAdapter.d.ts +1 -1
- package/dist/utils/invokeCallbackSafely.d.ts +5 -0
- package/dist/utils/invokeCallbackSafely.d.ts.map +1 -0
- package/dist/utils/invokeCallbackSafely.js +15 -0
- package/dist/utils/invokeCallbackSafely.js.map +1 -0
- package/dist/utils/smooth/SmoothContext.d.ts +6 -6
- package/dist/utils/smooth/SmoothContext.js +1 -1
- package/dist/utils/smooth/useSmooth.d.ts +3 -0
- package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
- package/dist/utils/smooth/useSmooth.js +16 -1
- package/dist/utils/smooth/useSmooth.js.map +1 -1
- package/dist/utils/useToolArgsFieldStatus.js +1 -1
- package/package.json +10 -10
- package/src/assistant-transport.ts +94 -0
- package/src/devtools/DevToolsHooks.test.ts +40 -0
- package/src/devtools/DevToolsHooks.ts +10 -15
- package/src/index.ts +8 -4
- package/src/mcp-apps/McpAppsRemoteHost.test.ts +29 -3
- package/src/mcp-apps/McpAppsRemoteHost.ts +42 -7
- package/src/mcp-apps/bridge.test.ts +153 -0
- package/src/mcp-apps/bridge.ts +42 -30
- package/src/model-context/frame/SPEC_AssistantFrame.md +1 -1
- package/src/primitives/actionBar/ActionBarExportMarkdown.test.tsx +67 -0
- package/src/primitives/actionBar/ActionBarExportMarkdown.tsx +3 -1
- package/src/primitives/actionBar/ActionBarStopSpeaking.tsx +0 -9
- package/src/primitives/branchPicker/BranchPickerRoot.tsx +5 -3
- package/src/primitives/composer/ComposerDictationTranscript.tsx +2 -2
- package/src/primitives/composer/ComposerInput.tsx +1 -1
- package/src/primitives/composer/trigger/triggerSelectionResource.test.ts +135 -0
- package/src/primitives/composer/trigger/triggerSelectionResource.ts +2 -0
- package/src/primitives/thread/ThreadRoot.test.tsx +251 -0
- package/src/primitives/thread/ThreadRoot.tsx +33 -0
- package/src/primitives/thread/ThreadViewport.tsx +1 -8
- package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +152 -0
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +26 -19
- package/src/sandbox-host/SandboxHost.test.tsx +90 -0
- package/src/sandbox-host/SandboxHost.tsx +8 -2
- package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +24 -3
- package/src/tests/RemoteThreadListRuntime.reloadMainThread.test.tsx +1 -2
- package/src/tests/augmentations.test.ts +49 -0
- package/src/tests/in-memory-thread-list.test.tsx +1 -1
- package/src/tests/threadMessageById.test.tsx +61 -1
- package/src/unstable/useComposerInput.ts +2 -2
- package/src/unstable/useComposerInputHistory.test.tsx +4 -4
- package/src/unstable/useComposerInputHistory.ts +1 -1
- package/src/unstable/useLiveCompletionAdapter.test.tsx +157 -0
- package/src/unstable/useLiveCompletionAdapter.ts +41 -17
- package/src/utils/invokeCallbackSafely.ts +11 -0
- package/src/utils/smooth/useSmooth.test.tsx +110 -0
- package/src/utils/smooth/useSmooth.ts +20 -6
- package/dist/client/InMemoryThreadList.d.ts +0 -12
- package/dist/client/InMemoryThreadList.d.ts.map +0 -1
- package/dist/client/InMemoryThreadList.js +0 -374
- package/dist/client/InMemoryThreadList.js.map +0 -1
- package/dist/context/react/utils/ensureBinding.d.ts +0 -5
- package/dist/context/react/utils/ensureBinding.d.ts.map +0 -1
- package/dist/context/react/utils/ensureBinding.js +0 -24
- package/dist/context/react/utils/ensureBinding.js.map +0 -1
- package/dist/context/react/utils/useRuntimeState.d.ts +0 -13
- package/dist/context/react/utils/useRuntimeState.d.ts.map +0 -1
- package/dist/context/react/utils/useRuntimeState.js +0 -33
- package/dist/context/react/utils/useRuntimeState.js.map +0 -1
- package/dist/legacy-runtime/cloud/auiV0.d.ts +0 -119
- package/dist/legacy-runtime/cloud/auiV0.d.ts.map +0 -1
- package/dist/legacy-runtime/cloud/auiV0.js +0 -137
- package/dist/legacy-runtime/cloud/auiV0.js.map +0 -1
- package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.d.ts +0 -16
- package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.d.ts.map +0 -1
- package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.js +0 -36
- package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.js.map +0 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts +0 -17
- package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts.map +0 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js +0 -55
- package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js.map +0 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts +0 -11
- package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts.map +0 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js +0 -165
- package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js.map +0 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts +0 -15
- package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts.map +0 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js +0 -81
- package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js.map +0 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts +0 -126
- package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts.map +0 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/types.js +0 -0
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts +0 -14
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +0 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +0 -290
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +0 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.d.ts +0 -7
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.d.ts.map +0 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js +0 -25
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js.map +0 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.d.ts +0 -7
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.d.ts.map +0 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.js +0 -27
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.js.map +0 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.d.ts +0 -2
- package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.js +0 -2
- package/src/client/InMemoryThreadList.ts +0 -247
- package/src/context/react/utils/ensureBinding.ts +0 -46
- package/src/context/react/utils/useRuntimeState.ts +0 -46
- package/src/legacy-runtime/cloud/auiV0.ts +0 -331
- package/src/legacy-runtime/cloud/useCloudThreadListRuntime.ts +0 -31
- package/src/legacy-runtime/runtime-cores/assistant-transport/commandQueue.ts +0 -68
- package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.test.ts +0 -455
- package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.ts +0 -172
- package/src/legacy-runtime/runtime-cores/assistant-transport/runManager.ts +0 -113
- package/src/legacy-runtime/runtime-cores/assistant-transport/transport-delivery.test.tsx +0 -79
- package/src/legacy-runtime/runtime-cores/assistant-transport/transport-parentid.test.tsx +0 -97
- package/src/legacy-runtime/runtime-cores/assistant-transport/transport-scheduling.test.ts +0 -307
- package/src/legacy-runtime/runtime-cores/assistant-transport/types.ts +0 -170
- package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransport.spec.md +0 -141
- package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.test.tsx +0 -585
- package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +0 -476
- package/src/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.ts +0 -20
- package/src/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.ts +0 -9
- package/src/legacy-runtime/runtime-cores/assistant-transport/utils.ts +0 -1
|
@@ -14,7 +14,7 @@ declare const useSmoothStatus: {
|
|
|
14
14
|
readonly error?: unknown;
|
|
15
15
|
} | {
|
|
16
16
|
readonly type: "requires-action";
|
|
17
|
-
readonly reason: "interrupt";
|
|
17
|
+
readonly reason: "tool-calls" | "interrupt";
|
|
18
18
|
};
|
|
19
19
|
<TSelected>(selector: (state: {
|
|
20
20
|
readonly type: "running";
|
|
@@ -26,7 +26,7 @@ declare const useSmoothStatus: {
|
|
|
26
26
|
readonly error?: unknown;
|
|
27
27
|
} | {
|
|
28
28
|
readonly type: "requires-action";
|
|
29
|
-
readonly reason: "interrupt";
|
|
29
|
+
readonly reason: "tool-calls" | "interrupt";
|
|
30
30
|
}) => TSelected): TSelected;
|
|
31
31
|
(options: {
|
|
32
32
|
optional: true;
|
|
@@ -40,7 +40,7 @@ declare const useSmoothStatus: {
|
|
|
40
40
|
readonly error?: unknown;
|
|
41
41
|
} | {
|
|
42
42
|
readonly type: "requires-action";
|
|
43
|
-
readonly reason: "interrupt";
|
|
43
|
+
readonly reason: "tool-calls" | "interrupt";
|
|
44
44
|
} | null;
|
|
45
45
|
<TSelected>(options: {
|
|
46
46
|
optional: true;
|
|
@@ -54,7 +54,7 @@ declare const useSmoothStatus: {
|
|
|
54
54
|
readonly error?: unknown;
|
|
55
55
|
} | {
|
|
56
56
|
readonly type: "requires-action";
|
|
57
|
-
readonly reason: "interrupt";
|
|
57
|
+
readonly reason: "tool-calls" | "interrupt";
|
|
58
58
|
}) => TSelected;
|
|
59
59
|
}): TSelected | null;
|
|
60
60
|
}, useSmoothStatusStore: {
|
|
@@ -68,7 +68,7 @@ declare const useSmoothStatus: {
|
|
|
68
68
|
readonly error?: unknown;
|
|
69
69
|
} | {
|
|
70
70
|
readonly type: "requires-action";
|
|
71
|
-
readonly reason: "interrupt";
|
|
71
|
+
readonly reason: "tool-calls" | "interrupt";
|
|
72
72
|
}>;
|
|
73
73
|
(options: {
|
|
74
74
|
optional: true;
|
|
@@ -82,7 +82,7 @@ declare const useSmoothStatus: {
|
|
|
82
82
|
readonly error?: unknown;
|
|
83
83
|
} | {
|
|
84
84
|
readonly type: "requires-action";
|
|
85
|
-
readonly reason: "interrupt";
|
|
85
|
+
readonly reason: "tool-calls" | "interrupt";
|
|
86
86
|
}> | null;
|
|
87
87
|
};
|
|
88
88
|
//#endregion
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { createContextStoreHook } from "../../context/react/utils/createContextStoreHook.js";
|
|
3
|
-
import { useAui } from "@assistant-ui/store";
|
|
4
3
|
import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
|
|
4
|
+
import { useAui } from "@assistant-ui/store";
|
|
5
5
|
import { createContext, forwardRef, useContext, useState } from "@assistant-ui/tap/react-shim";
|
|
6
6
|
import { create } from "zustand";
|
|
7
7
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -37,6 +37,9 @@ type SmoothOptions = {
|
|
|
37
37
|
* `true` uses the default rate, and a {@link SmoothOptions} object tunes
|
|
38
38
|
* the reveal. Returns the part state with `text` replaced by the revealed
|
|
39
39
|
* prefix and `status` reporting `running` until the reveal catches up.
|
|
40
|
+
* If the source settles before any character has been revealed, the text
|
|
41
|
+
* is committed immediately so a missed animation frame cannot leave an
|
|
42
|
+
* empty bubble.
|
|
40
43
|
*
|
|
41
44
|
* The reveal auto-disables under `prefers-reduced-motion: reduce`,
|
|
42
45
|
* committing the full text immediately; this takes precedence over an
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSmooth.d.ts","names":[],"sources":["../../../src/utils/smooth/useSmooth.ts"],"mappings":";;;;;KAkBY;;;;;;EAMV;;;;;;EAMA;;;;;EAKA;;;;;;;;EAQA
|
|
1
|
+
{"version":3,"file":"useSmooth.d.ts","names":[],"sources":["../../../src/utils/smooth/useSmooth.ts"],"mappings":";;;;;KAkBY;;;;;;EAMV;;;;;;EAMA;;;;;EAKA;;;;;;;;EAQA;;;;;;;;;;;;;;;;;;;;;;;;;cAgHW,YACX,OAAO,oBAAoB,kBAAkB,uBAC7C,mBAAkB,kBACjB,oBAAoB,kBAAkB"}
|
|
@@ -66,6 +66,9 @@ const positiveOr = (value, fallback) => value !== void 0 && value > 0 ? value :
|
|
|
66
66
|
* `true` uses the default rate, and a {@link SmoothOptions} object tunes
|
|
67
67
|
* the reveal. Returns the part state with `text` replaced by the revealed
|
|
68
68
|
* prefix and `status` reporting `running` until the reveal catches up.
|
|
69
|
+
* If the source settles before any character has been revealed, the text
|
|
70
|
+
* is committed immediately so a missed animation frame cannot leave an
|
|
71
|
+
* empty bubble.
|
|
69
72
|
*
|
|
70
73
|
* The reveal auto-disables under `prefers-reduced-motion: reduce`,
|
|
71
74
|
* committing the full text immediately; this takes precedence over an
|
|
@@ -147,17 +150,29 @@ const useSmooth = (state, smooth = false) => {
|
|
|
147
150
|
animatorRef.currentText = text;
|
|
148
151
|
animatorRef.targetText = text;
|
|
149
152
|
animatorRef.stop();
|
|
153
|
+
setText(text);
|
|
150
154
|
}
|
|
151
155
|
return;
|
|
152
156
|
}
|
|
153
157
|
animatorRef.targetText = text;
|
|
158
|
+
if (state.status.type !== "running") {
|
|
159
|
+
if (animatorRef.currentText === "") {
|
|
160
|
+
animatorRef.currentText = text;
|
|
161
|
+
animatorRef.stop();
|
|
162
|
+
setText(text);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
animatorRef.start();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
154
168
|
animatorRef.start();
|
|
155
169
|
}, [
|
|
156
170
|
animatorRef,
|
|
157
171
|
enabled,
|
|
158
172
|
text,
|
|
159
173
|
state.status.type,
|
|
160
|
-
part
|
|
174
|
+
part,
|
|
175
|
+
setText
|
|
161
176
|
]);
|
|
162
177
|
useEffect(() => {
|
|
163
178
|
return () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSmooth.js","names":["useEffect","useMemo","useRef","useState","useAui","useAuiState","MessagePartStatus","ReasoningMessagePart","TextMessagePart","MessagePartState","useCallbackRef","useMediaQuery","useSmoothStatusStore","writableStore","SmoothOptions","drainMs","maxCharIntervalMs","maxCharsPerFrame","minCommitMs","DEFAULT_DRAIN_MS","DEFAULT_MAX_CHAR_INTERVAL_MS","TextStreamAnimator","animationFrameId","lastUpdateTime","Date","now","lastCommitTime","targetText","Infinity","currentText","setText","newText","constructor","start","animate","stop","cancelAnimationFrame","currentTime","deltaTime","timeToConsume","remainingChars","length","baseTimePerChar","Math","min","frameLimit","charsToAdd","requestAnimationFrame","slice","isComplete","SMOOTH_STATUS","Object","freeze","type","positiveOr","value","fallback","undefined","useSmooth","state","smooth","text","reduceMotion","options","enabled","displayedText","setDisplayedText","status","aui","part","prevPart","setPrevPart","startsWith","smoothStatusStore","optional","target","setState","animatorRef","animatorPartRef","partChanged","current"],"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 { useMediaQuery } from \"../hooks/useMediaQuery\";\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 * Minimum time in milliseconds between committed updates. The reveal keeps\n * advancing every frame, but the visible text (and the downstream re-render\n * and markdown re-parse it triggers) is committed at most once per interval.\n * The final frame always commits. `0` commits every frame.\n * @default 0\n */\n minCommitMs?: 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 public lastCommitTime: number = 0;\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 public minCommitMs: number = 0;\n\n public currentText: string;\n private setText: (newText: string) => void;\n\n constructor(currentText: string, setText: (newText: string) => void) {\n this.currentText = currentText;\n this.setText = setText;\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\n const isComplete = charsToAdd === remainingChars;\n if (isComplete || currentTime - this.lastCommitTime >= this.minCommitMs) {\n this.lastCommitTime = currentTime;\n this.setText(this.currentText);\n }\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 * The reveal auto-disables under `prefers-reduced-motion: reduce`,\n * committing the full text immediately; this takes precedence over an\n * explicit `smooth={true}`.\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 reduceMotion = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n const options =\n typeof smooth === \"object\" && smooth !== null ? smooth : undefined;\n const enabled = smooth !== false && smooth !== null && !reduceMotion;\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 const minCommitMs = positiveOr(options?.minCommitMs, 0);\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.minCommitMs = minCommitMs;\n }, [animatorRef, drainMs, maxCharIntervalMs, maxCharsPerFrame, minCommitMs]);\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.lastCommitTime = 0;\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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"}
|
|
1
|
+
{"version":3,"file":"useSmooth.js","names":["useEffect","useMemo","useRef","useState","useAui","useAuiState","MessagePartStatus","ReasoningMessagePart","TextMessagePart","MessagePartState","useCallbackRef","useMediaQuery","useSmoothStatusStore","writableStore","SmoothOptions","drainMs","maxCharIntervalMs","maxCharsPerFrame","minCommitMs","DEFAULT_DRAIN_MS","DEFAULT_MAX_CHAR_INTERVAL_MS","TextStreamAnimator","animationFrameId","lastUpdateTime","Date","now","lastCommitTime","targetText","Infinity","currentText","setText","newText","constructor","start","animate","stop","cancelAnimationFrame","currentTime","deltaTime","timeToConsume","remainingChars","length","baseTimePerChar","Math","min","frameLimit","charsToAdd","requestAnimationFrame","slice","isComplete","SMOOTH_STATUS","Object","freeze","type","positiveOr","value","fallback","undefined","useSmooth","state","smooth","text","reduceMotion","options","enabled","displayedText","setDisplayedText","status","aui","part","prevPart","setPrevPart","startsWith","smoothStatusStore","optional","target","setState","animatorRef","animatorPartRef","partChanged","current"],"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 { useMediaQuery } from \"../hooks/useMediaQuery\";\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 * Minimum time in milliseconds between committed updates. The reveal keeps\n * advancing every frame, but the visible text (and the downstream re-render\n * and markdown re-parse it triggers) is committed at most once per interval.\n * The final frame always commits. `0` commits every frame.\n * @default 0\n */\n minCommitMs?: 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 public lastCommitTime: number = 0;\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 public minCommitMs: number = 0;\n\n public currentText: string;\n private setText: (newText: string) => void;\n\n constructor(currentText: string, setText: (newText: string) => void) {\n this.currentText = currentText;\n this.setText = setText;\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\n const isComplete = charsToAdd === remainingChars;\n if (isComplete || currentTime - this.lastCommitTime >= this.minCommitMs) {\n this.lastCommitTime = currentTime;\n this.setText(this.currentText);\n }\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 * If the source settles before any character has been revealed, the text\n * is committed immediately so a missed animation frame cannot leave an\n * empty bubble.\n *\n * The reveal auto-disables under `prefers-reduced-motion: reduce`,\n * committing the full text immediately; this takes precedence over an\n * explicit `smooth={true}`.\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 reduceMotion = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n const options =\n typeof smooth === \"object\" && smooth !== null ? smooth : undefined;\n const enabled = smooth !== false && smooth !== null && !reduceMotion;\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 const minCommitMs = positiveOr(options?.minCommitMs, 0);\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.minCommitMs = minCommitMs;\n }, [animatorRef, drainMs, maxCharIntervalMs, maxCharsPerFrame, minCommitMs]);\n\n const animatorPartRef = useRef(part);\n useEffect(() => {\n if (!enabled) {\n animatorRef.stop();\n return;\n }\n\n const partChanged = animatorPartRef.current !== part;\n animatorPartRef.current = part;\n // A new part whose text shares a prefix with the previous target would\n // keep the stale cursor and flicker without this reset.\n if (partChanged || !text.startsWith(animatorRef.targetText)) {\n if (state.status.type === \"running\") {\n animatorRef.currentText = \"\";\n animatorRef.targetText = text;\n animatorRef.lastCommitTime = 0;\n animatorRef.start();\n } else {\n animatorRef.currentText = text;\n animatorRef.targetText = text;\n animatorRef.stop();\n setText(text);\n }\n return;\n }\n\n animatorRef.targetText = text;\n if (state.status.type !== \"running\") {\n // No character has painted. A pending frame that never runs would\n // leave an empty bubble after settle.\n if (animatorRef.currentText === \"\") {\n animatorRef.currentText = text;\n animatorRef.stop();\n setText(text);\n return;\n }\n animatorRef.start();\n return;\n }\n\n animatorRef.start();\n }, [animatorRef, enabled, text, state.status.type, part, setText]);\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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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useAuiState } from "@assistant-ui/store";
|
|
2
1
|
import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
|
|
2
|
+
import { useAuiState } from "@assistant-ui/store";
|
|
3
3
|
import { getPartialJsonObjectFieldState } from "assistant-stream/utils";
|
|
4
4
|
//#region src/utils/useToolArgsFieldStatus.ts
|
|
5
5
|
const COMPLETE_STATUS = { type: "complete" };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.16",
|
|
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.3.
|
|
59
|
-
"@assistant-ui/store": "^0.3.
|
|
60
|
-
"@assistant-ui/tap": "^0.9.
|
|
58
|
+
"@assistant-ui/core": "^0.3.15",
|
|
59
|
+
"@assistant-ui/store": "^0.3.10",
|
|
60
|
+
"@assistant-ui/tap": "^0.9.14",
|
|
61
61
|
"@radix-ui/primitive": "^1.1.7",
|
|
62
62
|
"@radix-ui/react-collection": "^1.1.15",
|
|
63
63
|
"@radix-ui/react-compose-refs": "^1.1.5",
|
|
@@ -66,13 +66,13 @@
|
|
|
66
66
|
"@radix-ui/react-use-callback-ref": "^1.1.4",
|
|
67
67
|
"@radix-ui/react-use-controllable-state": "^1.2.6",
|
|
68
68
|
"@radix-ui/react-use-escape-keydown": "^1.1.5",
|
|
69
|
-
"assistant-cloud": "^0.1.
|
|
70
|
-
"assistant-stream": "^0.3.
|
|
69
|
+
"assistant-cloud": "^0.1.41",
|
|
70
|
+
"assistant-stream": "^0.3.39",
|
|
71
71
|
"radix-ui": "^1.6.7",
|
|
72
72
|
"react-textarea-autosize": "^8.5.9",
|
|
73
73
|
"safe-content-frame": "^0.0.27",
|
|
74
74
|
"zod": "^4.4.3",
|
|
75
|
-
"zustand": "^5.0.
|
|
75
|
+
"zustand": "^5.0.15"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
78
|
"@types/react": "*",
|
|
@@ -91,15 +91,15 @@
|
|
|
91
91
|
"devDependencies": {
|
|
92
92
|
"@testing-library/react": "^16.3.2",
|
|
93
93
|
"@types/json-schema": "^7.0.15",
|
|
94
|
-
"@types/node": "^26.
|
|
94
|
+
"@types/node": "^26.2.0",
|
|
95
95
|
"@types/react": "^19.2.18",
|
|
96
96
|
"@types/react-dom": "^19.2.4",
|
|
97
97
|
"jsdom": "^30.0.1",
|
|
98
98
|
"react": "^19.2.8",
|
|
99
99
|
"react-dom": "^19.2.8",
|
|
100
100
|
"vitest": "^4.1.10",
|
|
101
|
-
"@assistant-ui/vite": "0.0.
|
|
102
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
101
|
+
"@assistant-ui/vite": "0.0.14",
|
|
102
|
+
"@assistant-ui/x-buildutils": "0.0.23"
|
|
103
103
|
},
|
|
104
104
|
"publishConfig": {
|
|
105
105
|
"access": "public",
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
AssistantRuntime,
|
|
5
|
+
UserCommands as CoreUserCommands,
|
|
6
|
+
UserExternalState as CoreUserExternalState,
|
|
7
|
+
} from "@assistant-ui/core";
|
|
8
|
+
import {
|
|
9
|
+
useAssistantTransportRuntime as useCoreAssistantTransportRuntime,
|
|
10
|
+
useAssistantTransportSendCommand as useCoreAssistantTransportSendCommand,
|
|
11
|
+
useAssistantTransportState as useCoreAssistantTransportState,
|
|
12
|
+
} from "@assistant-ui/core/react";
|
|
13
|
+
import type {
|
|
14
|
+
AssistantTransportCommand as CoreAssistantTransportCommand,
|
|
15
|
+
AssistantTransportConnectionMetadata as CoreAssistantTransportConnectionMetadata,
|
|
16
|
+
AssistantTransportOptions as CoreAssistantTransportOptions,
|
|
17
|
+
AssistantTransportProtocol,
|
|
18
|
+
SendCommandsRequestBody as CoreSendCommandsRequestBody,
|
|
19
|
+
} from "@assistant-ui/core/react";
|
|
20
|
+
import type { UserCommands, UserExternalState } from "./augmentations";
|
|
21
|
+
|
|
22
|
+
export type { AssistantTransportProtocol };
|
|
23
|
+
|
|
24
|
+
export type SendCommandsRequestBody = Omit<
|
|
25
|
+
CoreSendCommandsRequestBody,
|
|
26
|
+
"commands"
|
|
27
|
+
> & {
|
|
28
|
+
commands: AssistantTransportCommand[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type AssistantTransportCommand =
|
|
32
|
+
| Exclude<CoreAssistantTransportCommand, CoreUserCommands>
|
|
33
|
+
| UserCommands;
|
|
34
|
+
|
|
35
|
+
export type AssistantTransportConnectionMetadata = Omit<
|
|
36
|
+
CoreAssistantTransportConnectionMetadata,
|
|
37
|
+
"pendingCommands"
|
|
38
|
+
> & {
|
|
39
|
+
pendingCommands: AssistantTransportCommand[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type AssistantTransportStateConverter<T> = (
|
|
43
|
+
state: T,
|
|
44
|
+
connectionMetadata: AssistantTransportConnectionMetadata,
|
|
45
|
+
) => ReturnType<CoreAssistantTransportOptions<T>["converter"]>;
|
|
46
|
+
|
|
47
|
+
export type AssistantTransportOptions<T> = Omit<
|
|
48
|
+
CoreAssistantTransportOptions<T>,
|
|
49
|
+
"converter" | "prepareSendCommandsRequest" | "onError" | "onCancel"
|
|
50
|
+
> & {
|
|
51
|
+
converter: AssistantTransportStateConverter<T>;
|
|
52
|
+
prepareSendCommandsRequest?: (
|
|
53
|
+
body: SendCommandsRequestBody,
|
|
54
|
+
) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
55
|
+
onError?: (
|
|
56
|
+
error: Error,
|
|
57
|
+
params: {
|
|
58
|
+
commands: AssistantTransportCommand[];
|
|
59
|
+
updateState: (updater: (state: T) => T) => void;
|
|
60
|
+
},
|
|
61
|
+
) => void | Promise<void>;
|
|
62
|
+
onCancel?: (params: {
|
|
63
|
+
commands: AssistantTransportCommand[];
|
|
64
|
+
updateState: (updater: (state: T) => T) => void;
|
|
65
|
+
error?: Error;
|
|
66
|
+
}) => void;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const useAssistantTransportRuntime = <T>(
|
|
70
|
+
options: AssistantTransportOptions<T>,
|
|
71
|
+
): AssistantRuntime => {
|
|
72
|
+
return useCoreAssistantTransportRuntime(
|
|
73
|
+
options as CoreAssistantTransportOptions<T>,
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const useAssistantTransportSendCommand = () => {
|
|
78
|
+
const send = useCoreAssistantTransportSendCommand();
|
|
79
|
+
return (command: AssistantTransportCommand) => {
|
|
80
|
+
send(command as CoreAssistantTransportCommand);
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export function useAssistantTransportState(): UserExternalState;
|
|
85
|
+
export function useAssistantTransportState<T>(
|
|
86
|
+
selector: (state: UserExternalState) => T,
|
|
87
|
+
): T;
|
|
88
|
+
export function useAssistantTransportState<T>(
|
|
89
|
+
selector: (state: UserExternalState) => T = (state) => state as T,
|
|
90
|
+
): T | UserExternalState {
|
|
91
|
+
return useCoreAssistantTransportState(
|
|
92
|
+
selector as (state: CoreUserExternalState) => T,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { DevToolsHooks, DevToolsProviderApi } from "./DevToolsHooks";
|
|
3
|
+
|
|
4
|
+
describe("DevToolsHooks", () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
vi.restoreAllMocks();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("notifies every listener when one throws", () => {
|
|
10
|
+
const error = new Error("listener failed");
|
|
11
|
+
const consoleError = vi
|
|
12
|
+
.spyOn(console, "error")
|
|
13
|
+
.mockImplementation(() => {});
|
|
14
|
+
const laterListener = vi.fn();
|
|
15
|
+
const unsubscribeThrowing = DevToolsHooks.subscribe(() => {
|
|
16
|
+
throw error;
|
|
17
|
+
});
|
|
18
|
+
const unsubscribeLater = DevToolsHooks.subscribe(laterListener);
|
|
19
|
+
let unregister = () => {};
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
unregister = DevToolsProviderApi.register({});
|
|
23
|
+
const apiId = [...DevToolsHooks.getApis().keys()].at(-1);
|
|
24
|
+
expect(apiId).toBeDefined();
|
|
25
|
+
|
|
26
|
+
DevToolsHooks.clearEventLogs(apiId!);
|
|
27
|
+
|
|
28
|
+
expect(laterListener).toHaveBeenCalledTimes(2);
|
|
29
|
+
expect(consoleError).toHaveBeenCalledTimes(2);
|
|
30
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
31
|
+
"[assistant-ui] DevTools listener threw an error",
|
|
32
|
+
error,
|
|
33
|
+
);
|
|
34
|
+
} finally {
|
|
35
|
+
unsubscribeThrowing();
|
|
36
|
+
unsubscribeLater();
|
|
37
|
+
unregister();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Unsubscribe } from "@assistant-ui/core";
|
|
2
|
+
import { notifyEventListeners } from "@assistant-ui/core/internal";
|
|
2
3
|
import type { AssistantClient } from "@assistant-ui/store";
|
|
3
4
|
|
|
4
5
|
export interface EventLog {
|
|
@@ -54,6 +55,10 @@ const getHook = (): DevToolsHook => {
|
|
|
54
55
|
return newHook;
|
|
55
56
|
};
|
|
56
57
|
|
|
58
|
+
const notifyListeners = (apiId: number): void => {
|
|
59
|
+
notifyEventListeners(getHook().listeners, apiId, "DevTools");
|
|
60
|
+
};
|
|
61
|
+
|
|
57
62
|
export class DevToolsHooks {
|
|
58
63
|
static subscribe(listener: () => void): Unsubscribe {
|
|
59
64
|
const hook = getHook();
|
|
@@ -69,17 +74,12 @@ export class DevToolsHooks {
|
|
|
69
74
|
if (!entry) return;
|
|
70
75
|
|
|
71
76
|
entry.logs = [];
|
|
72
|
-
|
|
77
|
+
notifyListeners(apiId);
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
static getApis(): Map<number, DevToolsApiEntry> {
|
|
76
81
|
return getHook().apis;
|
|
77
82
|
}
|
|
78
|
-
|
|
79
|
-
private static notifyListeners(apiId: number): void {
|
|
80
|
-
const hook = getHook();
|
|
81
|
-
hook.listeners.forEach((listener) => listener(apiId));
|
|
82
|
-
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export class DevToolsProviderApi {
|
|
@@ -116,15 +116,15 @@ export class DevToolsProviderApi {
|
|
|
116
116
|
);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
notifyListeners(apiId);
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
const stateUnsubscribe = aui.subscribe?.(() => {
|
|
123
|
-
|
|
123
|
+
notifyListeners(apiId);
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
hook.apis.set(apiId, entry);
|
|
127
|
-
|
|
127
|
+
notifyListeners(apiId);
|
|
128
128
|
|
|
129
129
|
return () => {
|
|
130
130
|
const hook = getHook();
|
|
@@ -136,12 +136,7 @@ export class DevToolsProviderApi {
|
|
|
136
136
|
|
|
137
137
|
hook.apis.delete(apiId);
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
notifyListeners(apiId);
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
|
-
|
|
143
|
-
private static notifyListeners(apiId: number): void {
|
|
144
|
-
const hook = getHook();
|
|
145
|
-
hook.listeners.forEach((listener) => listener(apiId));
|
|
146
|
-
}
|
|
147
142
|
}
|
package/src/index.ts
CHANGED
|
@@ -43,7 +43,7 @@ export type {
|
|
|
43
43
|
ThreadListItemState,
|
|
44
44
|
} from "@assistant-ui/core";
|
|
45
45
|
|
|
46
|
-
export { useCloudThreadListRuntime } from "
|
|
46
|
+
export { useCloudThreadListRuntime } from "@assistant-ui/core/react";
|
|
47
47
|
export { AssistantCloud } from "assistant-cloud";
|
|
48
48
|
|
|
49
49
|
// --- adapters/attachment ---
|
|
@@ -107,13 +107,13 @@ export {
|
|
|
107
107
|
useAssistantTransportRuntime,
|
|
108
108
|
useAssistantTransportSendCommand,
|
|
109
109
|
useAssistantTransportState,
|
|
110
|
-
} from "./
|
|
110
|
+
} from "./assistant-transport";
|
|
111
111
|
export type {
|
|
112
112
|
AssistantTransportConnectionMetadata,
|
|
113
113
|
AssistantTransportCommand,
|
|
114
114
|
AssistantTransportProtocol,
|
|
115
115
|
SendCommandsRequestBody,
|
|
116
|
-
} from "./
|
|
116
|
+
} from "./assistant-transport";
|
|
117
117
|
|
|
118
118
|
// --- core ---
|
|
119
119
|
export type {
|
|
@@ -430,7 +430,11 @@ export {
|
|
|
430
430
|
export {
|
|
431
431
|
InMemoryThreadList,
|
|
432
432
|
type InMemoryThreadListProps,
|
|
433
|
-
} from "
|
|
433
|
+
} from "@assistant-ui/core/store";
|
|
434
|
+
export {
|
|
435
|
+
RemoteThreadList,
|
|
436
|
+
type RemoteThreadListProps,
|
|
437
|
+
} from "@assistant-ui/core/store";
|
|
434
438
|
export { SingleThreadList } from "@assistant-ui/core/store";
|
|
435
439
|
|
|
436
440
|
export * as INTERNAL from "./internal";
|
|
@@ -41,9 +41,15 @@ describe("McpAppsRemoteHost", () => {
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
it("posts serverId params verbatim for tool calls and resource loads", async () => {
|
|
44
|
-
const fetch = vi.fn(async () =>
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const fetch = vi.fn(async (_input, init) => {
|
|
45
|
+
const request = JSON.parse(String(init?.body)) as { method: string };
|
|
46
|
+
return request.method === "mcp-apps/read-resource"
|
|
47
|
+
? Response.json({
|
|
48
|
+
uri: "ui://example/search",
|
|
49
|
+
html: "<main>Search</main>",
|
|
50
|
+
})
|
|
51
|
+
: Response.json({ content: [{ type: "text", text: "ok" }] });
|
|
52
|
+
}) as unknown as typeof globalThis.fetch;
|
|
47
53
|
const root = mount(fetch);
|
|
48
54
|
|
|
49
55
|
try {
|
|
@@ -123,4 +129,24 @@ describe("McpAppsRemoteHost", () => {
|
|
|
123
129
|
root.unmount();
|
|
124
130
|
}
|
|
125
131
|
});
|
|
132
|
+
|
|
133
|
+
it.each([{}, { uri: " ", html: "" }])(
|
|
134
|
+
"rejects malformed successful resource responses",
|
|
135
|
+
async (response) => {
|
|
136
|
+
const fetch = vi.fn(async () =>
|
|
137
|
+
Response.json(response),
|
|
138
|
+
) as unknown as typeof globalThis.fetch;
|
|
139
|
+
const root = mount(fetch);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await expect(
|
|
143
|
+
root.getValue().loadResource({ uri: "ui://example/search" }),
|
|
144
|
+
).rejects.toThrow(
|
|
145
|
+
/Invalid MCP App host response "mcp-apps\/read-resource" from "\/api\/mcp-apps"/,
|
|
146
|
+
);
|
|
147
|
+
} finally {
|
|
148
|
+
root.unmount();
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
);
|
|
126
152
|
});
|
|
@@ -39,6 +39,33 @@ const readErrorBody = async (res: Response): Promise<string | undefined> => {
|
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
const invalidMcpAppResource = (options: McpAppsRemoteHostOptions): never => {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Invalid MCP App host response "mcp-apps/read-resource" from "${options.url}": expected a resource with non-empty string "uri" and "html" fields`,
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const parseMcpAppResource = (
|
|
49
|
+
value: unknown,
|
|
50
|
+
options: McpAppsRemoteHostOptions,
|
|
51
|
+
): McpAppResource => {
|
|
52
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
53
|
+
return invalidMcpAppResource(options);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const resource = value as Record<string, unknown>;
|
|
57
|
+
if (
|
|
58
|
+
typeof resource.uri !== "string" ||
|
|
59
|
+
resource.uri.trim() === "" ||
|
|
60
|
+
typeof resource.html !== "string" ||
|
|
61
|
+
resource.html.trim() === ""
|
|
62
|
+
) {
|
|
63
|
+
return invalidMcpAppResource(options);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return value as McpAppResource;
|
|
67
|
+
};
|
|
68
|
+
|
|
42
69
|
async function postToHost(
|
|
43
70
|
options: McpAppsRemoteHostOptions,
|
|
44
71
|
method: string,
|
|
@@ -63,7 +90,14 @@ async function postToHost(
|
|
|
63
90
|
`MCP App host request "${method}" to "${options.url}" failed with ${status}${body ? `: ${body}` : ""}`,
|
|
64
91
|
);
|
|
65
92
|
}
|
|
66
|
-
|
|
93
|
+
try {
|
|
94
|
+
return await res.json();
|
|
95
|
+
} catch (cause) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Invalid MCP App host response "${method}" from "${options.url}": expected valid JSON`,
|
|
98
|
+
{ cause },
|
|
99
|
+
);
|
|
100
|
+
}
|
|
67
101
|
}
|
|
68
102
|
|
|
69
103
|
/**
|
|
@@ -91,12 +125,13 @@ const useMcpAppsRemoteHost = (
|
|
|
91
125
|
};
|
|
92
126
|
};
|
|
93
127
|
return {
|
|
94
|
-
loadResource: (params) =>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"mcp-apps/read-resource",
|
|
98
|
-
|
|
99
|
-
)
|
|
128
|
+
loadResource: async (params) => {
|
|
129
|
+
const options = getCurrentOptions();
|
|
130
|
+
return parseMcpAppResource(
|
|
131
|
+
await postToHost(options, "mcp-apps/read-resource", params),
|
|
132
|
+
options,
|
|
133
|
+
);
|
|
134
|
+
},
|
|
100
135
|
callTool: (params) =>
|
|
101
136
|
postToHost(getCurrentOptions(), "tools/call", params),
|
|
102
137
|
readResource: (params) =>
|