@assistant-ui/react-native 0.1.45 → 0.1.47
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/index.d.ts +13 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/primitives/actionBar/ActionBarCopy.d.ts +2 -2
- package/dist/primitives/actionBar/ActionBarCopy.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarCopy.js +2 -1
- package/dist/primitives/actionBar/ActionBarCopy.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.d.ts +2 -2
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +2 -1
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.d.ts +2 -2
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +2 -1
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.d.ts +1 -1
- package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +55 -38
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/message/MessageContent.d.ts.map +1 -1
- package/dist/primitives/message/MessageContent.js +13 -10
- package/dist/primitives/message/MessageContent.js.map +1 -1
- package/dist/primitives/message/MessageParts.d.ts +1 -0
- package/dist/primitives/message/MessageParts.d.ts.map +1 -1
- package/dist/primitives/message/MessageParts.js +22 -3
- package/dist/primitives/message/MessageParts.js.map +1 -1
- package/dist/primitives/message.d.ts +1 -0
- package/dist/primitives/message.d.ts.map +1 -1
- package/dist/primitives/message.js +4 -3
- package/dist/primitives/message.js.map +1 -1
- package/dist/primitives/messagePart.js +1 -1
- package/dist/primitives/thread/ThreadMessages.d.ts +7 -5
- package/dist/primitives/thread/ThreadMessages.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadMessages.js +109 -376
- package/dist/primitives/thread/ThreadMessages.js.map +1 -1
- package/dist/primitives/thread/ThreadRowsFlatList.d.ts +29 -0
- package/dist/primitives/thread/ThreadRowsFlatList.d.ts.map +1 -0
- package/dist/primitives/thread/ThreadRowsFlatList.js +226 -0
- package/dist/primitives/thread/ThreadRowsFlatList.js.map +1 -0
- package/dist/primitives/thread/flatListScroll.d.ts +52 -0
- package/dist/primitives/thread/flatListScroll.d.ts.map +1 -0
- package/dist/primitives/thread/flatListScroll.js +380 -0
- package/dist/primitives/thread/flatListScroll.js.map +1 -0
- package/dist/primitives/thread.d.ts +2 -0
- package/dist/primitives/thread.d.ts.map +1 -1
- package/dist/primitives/thread.js +5 -2
- package/dist/primitives/thread.js.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemTitle.d.ts +10 -0
- package/dist/primitives/threadListItem/ThreadListItemTitle.d.ts.map +1 -0
- package/dist/primitives/threadListItem/ThreadListItemTitle.js +47 -0
- package/dist/primitives/threadListItem/ThreadListItemTitle.js.map +1 -0
- package/dist/primitives/threadListItem.d.ts +1 -1
- package/dist/primitives/threadListItem.d.ts.map +1 -1
- package/dist/primitives/threadListItem.js +3 -3
- package/dist/primitives/threadListItem.js.map +1 -1
- package/package.json +8 -7
- package/src/index.ts +22 -1
- package/src/primitives/actionBar/ActionBar.test.tsx +45 -0
- package/src/primitives/actionBar/ActionBarCopy.tsx +12 -3
- package/src/primitives/actionBar/ActionBarFeedbackNegative.tsx +13 -3
- package/src/primitives/actionBar/ActionBarFeedbackPositive.tsx +13 -3
- package/src/primitives/composer/ComposerInput.test.tsx +64 -4
- package/src/primitives/composer/ComposerInput.tsx +13 -2
- package/src/primitives/message/MessageContent.runtime.test.tsx +35 -0
- package/src/primitives/message/MessageContent.test.tsx +68 -4
- package/src/primitives/message/MessageContent.tsx +19 -9
- package/src/primitives/message/MessageParts.test.tsx +68 -1
- package/src/primitives/message/MessageParts.tsx +17 -1
- package/src/primitives/message.test.tsx +74 -0
- package/src/primitives/message.ts +1 -0
- package/src/primitives/thread/ThreadMessages.test.tsx +28 -1
- package/src/primitives/thread/ThreadMessages.tsx +49 -343
- package/src/primitives/thread/ThreadRowsFlatList.test.tsx +253 -0
- package/src/primitives/thread/ThreadRowsFlatList.tsx +159 -0
- package/src/primitives/thread/flatListScroll.ts +467 -0
- package/src/primitives/thread.ts +5 -0
- package/src/primitives/threadListItem/ThreadListItemTitle.test.tsx +164 -0
- package/src/primitives/threadListItem/ThreadListItemTitle.tsx +23 -0
- package/src/primitives/threadListItem.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-native",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.47",
|
|
4
4
|
"description": "React Native bindings for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"assistant",
|
|
@@ -31,11 +31,11 @@
|
|
|
31
31
|
],
|
|
32
32
|
"sideEffects": false,
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@assistant-ui/core": "^0.3.
|
|
35
|
-
"@assistant-ui/store": "^0.3.
|
|
36
|
-
"@assistant-ui/tap": "^0.9.
|
|
37
|
-
"assistant-cloud": "^0.2.
|
|
38
|
-
"assistant-stream": "^0.3.
|
|
34
|
+
"@assistant-ui/core": "^0.3.25",
|
|
35
|
+
"@assistant-ui/store": "^0.3.18",
|
|
36
|
+
"@assistant-ui/tap": "^0.9.22",
|
|
37
|
+
"assistant-cloud": "^0.2.6",
|
|
38
|
+
"assistant-stream": "^0.3.49"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@types/react": "*",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@babel/core": "^7.29.7",
|
|
54
54
|
"@types/react": "^19.3.0",
|
|
55
55
|
"@types/react-dom": "^19.3.0",
|
|
56
|
-
"jsdom": "^30.
|
|
56
|
+
"jsdom": "^30.1.2",
|
|
57
57
|
"react": "^19.3.0",
|
|
58
58
|
"react-dom": "^19.3.0",
|
|
59
59
|
"react-native": "^0.87.1",
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"scripts": {
|
|
77
77
|
"build": "aui-build",
|
|
78
78
|
"test": "vitest run",
|
|
79
|
+
"test:coverage": "vitest run --coverage --coverage.reporter=text --coverage.reporter=html --coverage.reporter=json-summary",
|
|
79
80
|
"typecheck": "tsc --noEmit",
|
|
80
81
|
"test:peer-react18": "vitest run --config vitest.peer-react18.config.ts",
|
|
81
82
|
"test:types:peer-react18": "aui-react18-types",
|
package/src/index.ts
CHANGED
|
@@ -20,9 +20,13 @@ export type {
|
|
|
20
20
|
ToolApprovalDisplay,
|
|
21
21
|
ToolApprovalOption,
|
|
22
22
|
ToolApprovalOptionKind,
|
|
23
|
+
ToolApprovalAnswer,
|
|
24
|
+
ToolApprovalQuestion,
|
|
25
|
+
ToolApprovalQuestionOption,
|
|
23
26
|
ToolApprovalResponse,
|
|
24
27
|
ToolCallMessagePart,
|
|
25
28
|
ToolCallMessagePartStatus,
|
|
29
|
+
MessagePartTiming,
|
|
26
30
|
ToolCallTiming,
|
|
27
31
|
ToolModelContentPart,
|
|
28
32
|
ImageMessagePart,
|
|
@@ -156,9 +160,13 @@ export {
|
|
|
156
160
|
useExternalStoreRuntime,
|
|
157
161
|
useExternalStoreSharedOptions,
|
|
158
162
|
useExternalMessageConverter,
|
|
163
|
+
/** @deprecated Experimental since 2025-01-26. Not scheduled for removal; the API may change in any release. */
|
|
159
164
|
convertExternalMessages as unstable_convertExternalMessages,
|
|
165
|
+
/** @deprecated Experimental since 2026-09-15. Not scheduled for removal; the API may change in any release. */
|
|
160
166
|
createExternalMessageConversionCache as unstable_createExternalMessageConversionCache,
|
|
167
|
+
/** @deprecated Experimental since 2025-02-02. Not scheduled for removal; the API may change in any release. */
|
|
161
168
|
createMessageConverter as unstable_createMessageConverter,
|
|
169
|
+
/** @deprecated Experimental since 2026-09-15. Not scheduled for removal; the API may change in any release. */
|
|
162
170
|
type ExternalMessageConversionCache as Unstable_ExternalMessageConversionCache,
|
|
163
171
|
type JoinStrategy,
|
|
164
172
|
} from "@assistant-ui/core/react";
|
|
@@ -179,6 +187,11 @@ export * as SuggestionPrimitive from "./primitives/suggestion";
|
|
|
179
187
|
export * as ErrorPrimitive from "./primitives/error";
|
|
180
188
|
|
|
181
189
|
export { groupPartByType, type GroupByContext } from "@assistant-ui/core/react";
|
|
190
|
+
export {
|
|
191
|
+
createThreadRowsSelector,
|
|
192
|
+
type ThreadRow,
|
|
193
|
+
type ThreadRowsOptions,
|
|
194
|
+
} from "@assistant-ui/core/react";
|
|
182
195
|
export { unstable_useThreadMessageIds } from "@assistant-ui/core/react";
|
|
183
196
|
|
|
184
197
|
// Re-export shared providers from core/react
|
|
@@ -404,9 +417,17 @@ export type {
|
|
|
404
417
|
ThreadSuggestion,
|
|
405
418
|
ToolCallMessagePartMcpMetadata,
|
|
406
419
|
ToolExecutionStatus,
|
|
420
|
+
DirectiveFormatter,
|
|
421
|
+
DirectiveSegment,
|
|
422
|
+
TriggerAdapter,
|
|
423
|
+
TriggerCategory,
|
|
424
|
+
TriggerItem,
|
|
407
425
|
Unstable_DirectiveFormatter,
|
|
408
426
|
Unstable_DirectiveSegment,
|
|
409
427
|
Unstable_TriggerItem,
|
|
410
428
|
VoiceSessionState,
|
|
411
429
|
} from "@assistant-ui/core";
|
|
412
|
-
export {
|
|
430
|
+
export {
|
|
431
|
+
defaultDirectiveFormatter,
|
|
432
|
+
unstable_defaultDirectiveFormatter,
|
|
433
|
+
} from "@assistant-ui/core";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { act, type ReactNode } from "react";
|
|
2
2
|
import { createRoot, type Root } from "react-dom/client";
|
|
3
|
+
import { Text } from "react-native";
|
|
3
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
5
|
import { ActionBarCopy } from "./ActionBarCopy";
|
|
5
6
|
import { ActionBarEdit } from "./ActionBarEdit";
|
|
@@ -222,6 +223,50 @@ describe("ActionBar", () => {
|
|
|
222
223
|
return container.querySelector('[data-testid="t"]') as HTMLElement;
|
|
223
224
|
};
|
|
224
225
|
|
|
226
|
+
const assertRenderState = async <TState extends object>(
|
|
227
|
+
render: (children: (state: TState) => ReactNode) => ReactNode,
|
|
228
|
+
expectedState: TState,
|
|
229
|
+
) => {
|
|
230
|
+
let renderState: unknown;
|
|
231
|
+
await mount(
|
|
232
|
+
render((state) => {
|
|
233
|
+
renderState = state;
|
|
234
|
+
return <Text>label</Text>;
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(renderState).toMatchObject(expectedState);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
it("ActionBarCopy passes Pressable state alongside isCopied", async () => {
|
|
242
|
+
await assertRenderState(
|
|
243
|
+
(children) => <ActionBarCopy testID="t">{children}</ActionBarCopy>,
|
|
244
|
+
{ pressed: false, disabled: false, isCopied: false },
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("ActionBarFeedbackPositive passes Pressable state alongside isSubmitted", async () => {
|
|
249
|
+
await assertRenderState(
|
|
250
|
+
(children) => (
|
|
251
|
+
<ActionBarFeedbackPositive testID="t">
|
|
252
|
+
{children}
|
|
253
|
+
</ActionBarFeedbackPositive>
|
|
254
|
+
),
|
|
255
|
+
{ pressed: false, disabled: false, isSubmitted: false },
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("ActionBarFeedbackNegative passes Pressable state alongside isSubmitted", async () => {
|
|
260
|
+
await assertRenderState(
|
|
261
|
+
(children) => (
|
|
262
|
+
<ActionBarFeedbackNegative testID="t">
|
|
263
|
+
{children}
|
|
264
|
+
</ActionBarFeedbackNegative>
|
|
265
|
+
),
|
|
266
|
+
{ pressed: false, disabled: false, isSubmitted: false },
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
225
270
|
for (const actionBar of actionBars) {
|
|
226
271
|
describe(actionBar.name, () => {
|
|
227
272
|
it("passes the effective disabled state to render children", async () => {
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
type PressableProps,
|
|
5
|
+
type PressableStateCallbackType,
|
|
6
|
+
} from "react-native";
|
|
3
7
|
import {
|
|
4
8
|
useActionBarCopy,
|
|
5
9
|
type UseActionBarCopyOptions,
|
|
@@ -9,7 +13,12 @@ export type ActionBarCopyProps = Omit<PressableProps, "onPress" | "children"> &
|
|
|
9
13
|
UseActionBarCopyOptions & {
|
|
10
14
|
children:
|
|
11
15
|
| ReactNode
|
|
12
|
-
| ((
|
|
16
|
+
| ((
|
|
17
|
+
props: PressableStateCallbackType & {
|
|
18
|
+
isCopied: boolean;
|
|
19
|
+
disabled: boolean;
|
|
20
|
+
},
|
|
21
|
+
) => ReactNode);
|
|
13
22
|
};
|
|
14
23
|
|
|
15
24
|
export const ActionBarCopy = ({
|
|
@@ -33,7 +42,7 @@ export const ActionBarCopy = ({
|
|
|
33
42
|
{...pressableProps}
|
|
34
43
|
>
|
|
35
44
|
{typeof children === "function"
|
|
36
|
-
? children({ isCopied, disabled: isDisabled })
|
|
45
|
+
? (state) => children({ ...state, isCopied, disabled: isDisabled })
|
|
37
46
|
: children}
|
|
38
47
|
</Pressable>
|
|
39
48
|
);
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
Pressable,
|
|
5
|
+
type PressableProps,
|
|
6
|
+
type PressableStateCallbackType,
|
|
7
|
+
} from "react-native";
|
|
3
8
|
import { useActionBarFeedbackNegative } from "@assistant-ui/core/react";
|
|
4
9
|
|
|
5
10
|
export type ActionBarFeedbackNegativeProps = Omit<
|
|
@@ -8,7 +13,12 @@ export type ActionBarFeedbackNegativeProps = Omit<
|
|
|
8
13
|
> & {
|
|
9
14
|
children:
|
|
10
15
|
| ReactNode
|
|
11
|
-
| ((
|
|
16
|
+
| ((
|
|
17
|
+
props: PressableStateCallbackType & {
|
|
18
|
+
isSubmitted: boolean;
|
|
19
|
+
disabled: boolean;
|
|
20
|
+
},
|
|
21
|
+
) => ReactNode);
|
|
12
22
|
};
|
|
13
23
|
|
|
14
24
|
export const ActionBarFeedbackNegative = ({
|
|
@@ -30,7 +40,7 @@ export const ActionBarFeedbackNegative = ({
|
|
|
30
40
|
{...pressableProps}
|
|
31
41
|
>
|
|
32
42
|
{typeof children === "function"
|
|
33
|
-
? children({ isSubmitted, disabled })
|
|
43
|
+
? (state) => children({ ...state, isSubmitted, disabled })
|
|
34
44
|
: children}
|
|
35
45
|
</Pressable>
|
|
36
46
|
);
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
Pressable,
|
|
5
|
+
type PressableProps,
|
|
6
|
+
type PressableStateCallbackType,
|
|
7
|
+
} from "react-native";
|
|
3
8
|
import { useActionBarFeedbackPositive } from "@assistant-ui/core/react";
|
|
4
9
|
|
|
5
10
|
export type ActionBarFeedbackPositiveProps = Omit<
|
|
@@ -8,7 +13,12 @@ export type ActionBarFeedbackPositiveProps = Omit<
|
|
|
8
13
|
> & {
|
|
9
14
|
children:
|
|
10
15
|
| ReactNode
|
|
11
|
-
| ((
|
|
16
|
+
| ((
|
|
17
|
+
props: PressableStateCallbackType & {
|
|
18
|
+
isSubmitted: boolean;
|
|
19
|
+
disabled: boolean;
|
|
20
|
+
},
|
|
21
|
+
) => ReactNode);
|
|
12
22
|
};
|
|
13
23
|
|
|
14
24
|
export const ActionBarFeedbackPositive = ({
|
|
@@ -30,7 +40,7 @@ export const ActionBarFeedbackPositive = ({
|
|
|
30
40
|
{...pressableProps}
|
|
31
41
|
>
|
|
32
42
|
{typeof children === "function"
|
|
33
|
-
? children({ isSubmitted, disabled })
|
|
43
|
+
? (state) => children({ ...state, isSubmitted, disabled })
|
|
34
44
|
: children}
|
|
35
45
|
</Pressable>
|
|
36
46
|
);
|
|
@@ -8,7 +8,12 @@ const h = vi.hoisted(() => ({
|
|
|
8
8
|
sendSpy: vi.fn<() => void>(),
|
|
9
9
|
flushTapSyncSpy: vi.fn(<T,>(fn: () => T) => fn()),
|
|
10
10
|
composerState: { text: "" },
|
|
11
|
-
threadState: {
|
|
11
|
+
threadState: {
|
|
12
|
+
isRunning: false,
|
|
13
|
+
queue: false,
|
|
14
|
+
voice: false,
|
|
15
|
+
isDisabled: false,
|
|
16
|
+
},
|
|
12
17
|
platform: { os: "web" as "web" | "ios" | "android" },
|
|
13
18
|
}));
|
|
14
19
|
|
|
@@ -28,12 +33,20 @@ vi.mock("@assistant-ui/store", () => {
|
|
|
28
33
|
return {
|
|
29
34
|
useAui: () => aui,
|
|
30
35
|
useAuiState: <T,>(
|
|
31
|
-
selector: (s: {
|
|
32
|
-
|
|
36
|
+
selector: (s: {
|
|
37
|
+
composer: typeof h.composerState;
|
|
38
|
+
thread: { isDisabled: boolean };
|
|
39
|
+
}) => T,
|
|
40
|
+
) =>
|
|
41
|
+
selector({
|
|
42
|
+
composer: h.composerState,
|
|
43
|
+
thread: { isDisabled: h.threadState.isDisabled },
|
|
44
|
+
}),
|
|
33
45
|
};
|
|
34
46
|
});
|
|
35
47
|
|
|
36
|
-
vi.mock("@assistant-ui/tap", () => ({
|
|
48
|
+
vi.mock("@assistant-ui/tap", async (importOriginal) => ({
|
|
49
|
+
...(await importOriginal<typeof import("@assistant-ui/tap")>()),
|
|
37
50
|
flushTapSync: h.flushTapSyncSpy,
|
|
38
51
|
}));
|
|
39
52
|
|
|
@@ -111,6 +124,7 @@ describe("ComposerInput", () => {
|
|
|
111
124
|
h.threadState.isRunning = false;
|
|
112
125
|
h.threadState.queue = false;
|
|
113
126
|
h.threadState.voice = false;
|
|
127
|
+
h.threadState.isDisabled = false;
|
|
114
128
|
h.platform.os = "web";
|
|
115
129
|
|
|
116
130
|
container = document.createElement("div");
|
|
@@ -421,4 +435,50 @@ describe("ComposerInput", () => {
|
|
|
421
435
|
expect(onKeyPress).toHaveBeenCalledTimes(1);
|
|
422
436
|
});
|
|
423
437
|
});
|
|
438
|
+
|
|
439
|
+
describe("disabled thread", () => {
|
|
440
|
+
beforeEach(() => {
|
|
441
|
+
h.threadState.isDisabled = true;
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("makes the input non-editable", async () => {
|
|
445
|
+
const input = await mount();
|
|
446
|
+
expect(input.disabled || input.readOnly).toBe(true);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("does not let editable props override the disabled thread", async () => {
|
|
450
|
+
const input = await mount({ editable: true, readOnly: false });
|
|
451
|
+
expect(input.readOnly).toBe(true);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("ignores text changes", async () => {
|
|
455
|
+
const input = await mount();
|
|
456
|
+
await act(async () => fireInput(input, "hello"));
|
|
457
|
+
expect(h.setText).not.toHaveBeenCalled();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("does not submit on Enter", async () => {
|
|
461
|
+
const input = await mount();
|
|
462
|
+
await act(async () => fireKeyDown(input, { key: "Enter" }));
|
|
463
|
+
expect(h.sendSpy).not.toHaveBeenCalled();
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("forwards caller key handlers while blocking submission", async () => {
|
|
467
|
+
const onKeyPress = vi.fn();
|
|
468
|
+
const input = await mount({ onKeyPress });
|
|
469
|
+
|
|
470
|
+
await act(async () => {
|
|
471
|
+
fireKeyDown(input, { key: "Escape" });
|
|
472
|
+
fireKeyDown(input, { key: "Enter" });
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
expect(onKeyPress).toHaveBeenCalledTimes(2);
|
|
476
|
+
expect(h.sendSpy).not.toHaveBeenCalled();
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("preserves a caller-provided non-editable setting", async () => {
|
|
481
|
+
const input = await mount({ editable: false });
|
|
482
|
+
expect(input.readOnly).toBe(true);
|
|
483
|
+
});
|
|
424
484
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import { Platform, TextInput, type TextInputProps } from "react-native";
|
|
3
|
+
import { composerInputDisabled } from "@assistant-ui/core/store/internal";
|
|
3
4
|
import { useAui, useAuiState } from "@assistant-ui/store";
|
|
4
5
|
import { flushTapSync } from "@assistant-ui/tap";
|
|
5
6
|
|
|
@@ -59,14 +60,19 @@ export const ComposerInput = ({
|
|
|
59
60
|
onKeyPress: onKeyPressProp,
|
|
60
61
|
numberOfLines,
|
|
61
62
|
style,
|
|
63
|
+
editable,
|
|
64
|
+
readOnly,
|
|
62
65
|
...props
|
|
63
66
|
}: ComposerInputProps) => {
|
|
64
67
|
const aui = useAui();
|
|
65
68
|
const text = useAuiState((s) => s.composer.text);
|
|
69
|
+
const isInputDisabled = useAuiState(composerInputDisabled);
|
|
66
70
|
const inputRef = useRef<TextInputInstance>(null);
|
|
67
71
|
|
|
68
72
|
const onChangeText = useCallback(
|
|
69
73
|
(value: string) => {
|
|
74
|
+
if (isInputDisabled) return;
|
|
75
|
+
|
|
70
76
|
if (Platform.OS === "web") {
|
|
71
77
|
// Keep the controlled value in sync mid-IME so react-dom does not reset the textarea to a stale value
|
|
72
78
|
flushTapSync(() => {
|
|
@@ -76,7 +82,7 @@ export const ComposerInput = ({
|
|
|
76
82
|
}
|
|
77
83
|
aui.composer.setText(value);
|
|
78
84
|
},
|
|
79
|
-
[aui],
|
|
85
|
+
[aui, isInputDisabled],
|
|
80
86
|
);
|
|
81
87
|
|
|
82
88
|
// Auto-resize textarea on web when text changes
|
|
@@ -89,6 +95,7 @@ export const ComposerInput = ({
|
|
|
89
95
|
(e: TextInputKeyPressEvent) => {
|
|
90
96
|
onKeyPressProp?.(e);
|
|
91
97
|
if (e.isDefaultPrevented()) return;
|
|
98
|
+
if (isInputDisabled) return;
|
|
92
99
|
|
|
93
100
|
if (Platform.OS !== "web") return;
|
|
94
101
|
if (submitMode !== "enter") return;
|
|
@@ -113,9 +120,11 @@ export const ComposerInput = ({
|
|
|
113
120
|
aui.composer.send();
|
|
114
121
|
}
|
|
115
122
|
},
|
|
116
|
-
[aui, submitMode, onKeyPressProp],
|
|
123
|
+
[aui, isInputDisabled, submitMode, onKeyPressProp],
|
|
117
124
|
);
|
|
118
125
|
|
|
126
|
+
const isReadOnly = isInputDisabled || editable === false || readOnly === true;
|
|
127
|
+
|
|
119
128
|
return (
|
|
120
129
|
<TextInput
|
|
121
130
|
ref={inputRef}
|
|
@@ -123,6 +132,8 @@ export const ComposerInput = ({
|
|
|
123
132
|
onChangeText={onChangeText}
|
|
124
133
|
onKeyPress={onKeyPress}
|
|
125
134
|
numberOfLines={numberOfLines ?? (Platform.OS === "web" ? 1 : undefined)}
|
|
135
|
+
editable={editable}
|
|
136
|
+
readOnly={isReadOnly}
|
|
126
137
|
style={style}
|
|
127
138
|
{...props}
|
|
128
139
|
/>
|
|
@@ -14,6 +14,41 @@ import { MessageContent } from "./MessageContent";
|
|
|
14
14
|
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
15
15
|
|
|
16
16
|
describe("MessageContent with a runtime", () => {
|
|
17
|
+
it("renders a default image after a text part", async () => {
|
|
18
|
+
const image = "data:image/png;base64,iVBORw0KGgo=";
|
|
19
|
+
const App = () => {
|
|
20
|
+
const runtime = useExternalStoreRuntime({
|
|
21
|
+
messages: [
|
|
22
|
+
{
|
|
23
|
+
role: "assistant",
|
|
24
|
+
content: [
|
|
25
|
+
{ type: "text", text: "lead" },
|
|
26
|
+
{ type: "image", image },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
] satisfies ThreadMessageLike[],
|
|
30
|
+
convertMessage: (value) => value,
|
|
31
|
+
onNew: async () => {},
|
|
32
|
+
});
|
|
33
|
+
return (
|
|
34
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
35
|
+
<MessageByIndexProvider index={0}>
|
|
36
|
+
<MessageContent />
|
|
37
|
+
</MessageByIndexProvider>
|
|
38
|
+
</AssistantRuntimeProvider>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
const container = document.createElement("div");
|
|
42
|
+
const root = createRoot(container);
|
|
43
|
+
try {
|
|
44
|
+
await act(async () => root.render(<App />));
|
|
45
|
+
expect(container.textContent).toContain("lead");
|
|
46
|
+
expect(container.querySelector("img")?.getAttribute("src")).toBe(image);
|
|
47
|
+
} finally {
|
|
48
|
+
await act(async () => root.unmount());
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
17
52
|
it.each(["tool-call", "data"] as const)(
|
|
18
53
|
"renders derived status for registered %s UIs",
|
|
19
54
|
async (type) => {
|
|
@@ -17,7 +17,10 @@ const h = vi.hoisted(() => ({
|
|
|
17
17
|
return this.content;
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
|
-
tools: {
|
|
20
|
+
tools: {
|
|
21
|
+
toolUIs: {} as Record<string, unknown>,
|
|
22
|
+
mcpApp: undefined as { render: unknown } | undefined,
|
|
23
|
+
},
|
|
21
24
|
dataRenderers: {
|
|
22
25
|
renderers: {} as Record<string, unknown>,
|
|
23
26
|
fallbacks: [] as unknown[],
|
|
@@ -25,7 +28,8 @@ const h = vi.hoisted(() => ({
|
|
|
25
28
|
},
|
|
26
29
|
}));
|
|
27
30
|
|
|
28
|
-
vi.mock("@assistant-ui/store", () => {
|
|
31
|
+
vi.mock("@assistant-ui/store", async (importOriginal) => {
|
|
32
|
+
const actual = await importOriginal<typeof import("@assistant-ui/store")>();
|
|
29
33
|
const message = Object.assign(() => message, {
|
|
30
34
|
part: ({ index }: { index: number }) => ({
|
|
31
35
|
addToolResult: (...args: unknown[]) => h.addToolResult(index, ...args),
|
|
@@ -38,6 +42,7 @@ vi.mock("@assistant-ui/store", () => {
|
|
|
38
42
|
});
|
|
39
43
|
const aui = { message };
|
|
40
44
|
return {
|
|
45
|
+
...actual,
|
|
41
46
|
useAui: () => aui,
|
|
42
47
|
useAuiState: <T,>(selector: (s: typeof h.state) => T) => selector(h.state),
|
|
43
48
|
};
|
|
@@ -56,6 +61,7 @@ describe("MessageContent", () => {
|
|
|
56
61
|
h.unstable_recordInteraction.mockReset();
|
|
57
62
|
h.state.message.content = [];
|
|
58
63
|
h.state.tools.toolUIs = {};
|
|
64
|
+
h.state.tools.mcpApp = undefined;
|
|
59
65
|
h.state.dataRenderers.renderers = {};
|
|
60
66
|
h.state.dataRenderers.fallbacks = [];
|
|
61
67
|
|
|
@@ -153,9 +159,8 @@ describe("MessageContent", () => {
|
|
|
153
159
|
expect(container.textContent).toBe("");
|
|
154
160
|
});
|
|
155
161
|
|
|
156
|
-
it("renders null for
|
|
162
|
+
it("renders null for reasoning, source, and file without renderers", async () => {
|
|
157
163
|
h.state.message.content = [
|
|
158
|
-
{ type: "image", image: "x" },
|
|
159
164
|
{ type: "reasoning", text: "r" },
|
|
160
165
|
{ type: "source", sourceType: "url", id: "1", url: "u" },
|
|
161
166
|
{ type: "file", filename: "f" },
|
|
@@ -186,6 +191,7 @@ describe("MessageContent", () => {
|
|
|
186
191
|
await mount({ renderImage, renderReasoning, renderSource, renderFile });
|
|
187
192
|
|
|
188
193
|
expect(container.textContent).toBe("image-0reasoning-1source-2file-3");
|
|
194
|
+
expect(container.querySelector("img")).toBeNull();
|
|
189
195
|
expect(renderImage).toHaveBeenCalledWith({
|
|
190
196
|
part: h.state.message.content[0],
|
|
191
197
|
index: 0,
|
|
@@ -262,6 +268,64 @@ describe("MessageContent", () => {
|
|
|
262
268
|
});
|
|
263
269
|
});
|
|
264
270
|
|
|
271
|
+
it("renders tools.mcpApp for a tool call with a ui:// resource", async () => {
|
|
272
|
+
const Mcp = vi.fn(() => <span data-testid="mcp">mcp</span>);
|
|
273
|
+
h.state.message.content = [
|
|
274
|
+
{
|
|
275
|
+
type: "tool-call",
|
|
276
|
+
toolName: "show_chart",
|
|
277
|
+
toolCallId: "c1",
|
|
278
|
+
mcp: { app: { resourceUri: "ui://chart" } },
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
h.state.tools.mcpApp = { render: Mcp };
|
|
282
|
+
await mount();
|
|
283
|
+
expect(container.querySelector('[data-testid="mcp"]')?.textContent).toBe(
|
|
284
|
+
"mcp",
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("prefers a named tool UI over tools.mcpApp", async () => {
|
|
289
|
+
const NamedTool = vi.fn(() => <span data-testid="named">named</span>);
|
|
290
|
+
const Mcp = vi.fn(() => <span data-testid="mcp">mcp</span>);
|
|
291
|
+
h.state.message.content = [
|
|
292
|
+
{
|
|
293
|
+
type: "tool-call",
|
|
294
|
+
toolName: "show_chart",
|
|
295
|
+
toolCallId: "c1",
|
|
296
|
+
mcp: { app: { resourceUri: "ui://chart" } },
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
h.state.tools.toolUIs = { show_chart: [{ render: NamedTool }] };
|
|
300
|
+
h.state.tools.mcpApp = { render: Mcp };
|
|
301
|
+
await mount();
|
|
302
|
+
expect(container.querySelector('[data-testid="named"]')).not.toBeNull();
|
|
303
|
+
expect(Mcp).not.toHaveBeenCalled();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("does not use tools.mcpApp when the resource URI is not ui://", async () => {
|
|
307
|
+
const Mcp = vi.fn(() => <span data-testid="mcp">mcp</span>);
|
|
308
|
+
h.state.message.content = [
|
|
309
|
+
{
|
|
310
|
+
type: "tool-call",
|
|
311
|
+
toolName: "show_chart",
|
|
312
|
+
toolCallId: "c1",
|
|
313
|
+
mcp: { app: { resourceUri: "https://example.com/chart" } },
|
|
314
|
+
},
|
|
315
|
+
];
|
|
316
|
+
h.state.tools.mcpApp = { render: Mcp };
|
|
317
|
+
const renderToolCall = vi.fn(({ part, index }): ReactElement => (
|
|
318
|
+
<span data-testid="fallback">
|
|
319
|
+
fallback:{String(part.toolName)}:{index}
|
|
320
|
+
</span>
|
|
321
|
+
));
|
|
322
|
+
await mount({ renderToolCall });
|
|
323
|
+
expect(
|
|
324
|
+
container.querySelector('[data-testid="fallback"]'),
|
|
325
|
+
).not.toBeNull();
|
|
326
|
+
expect(Mcp).not.toHaveBeenCalled();
|
|
327
|
+
});
|
|
328
|
+
|
|
265
329
|
it("renders null when no renderer is registered and no fallback is given", async () => {
|
|
266
330
|
h.state.message.content = [
|
|
267
331
|
{ type: "tool-call", toolName: "search", toolCallId: "c1" },
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { type ReactElement, Fragment, useMemo } from "react";
|
|
2
2
|
import { Text } from "react-native";
|
|
3
|
-
import { getMessagePartKeys } from "@assistant-ui/core/internal";
|
|
4
3
|
import type {
|
|
5
4
|
ThreadUserMessagePart,
|
|
6
5
|
ThreadAssistantMessagePart,
|
|
7
6
|
MessagePartState,
|
|
8
7
|
} from "@assistant-ui/core";
|
|
9
8
|
import { useAui, useAuiState } from "@assistant-ui/store";
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
import {
|
|
10
|
+
PartByIndexProvider,
|
|
11
|
+
type ToolCallMessagePartProps,
|
|
12
|
+
type DataMessagePartProps,
|
|
13
13
|
} from "@assistant-ui/core/react";
|
|
14
|
+
import { DefaultMessageImage } from "./MessageParts";
|
|
15
|
+
import {
|
|
16
|
+
getMessagePartKeys,
|
|
17
|
+
resolveToolRender,
|
|
18
|
+
} from "@assistant-ui/core/internal";
|
|
14
19
|
|
|
15
20
|
type MessageContentPart = ThreadUserMessagePart | ThreadAssistantMessagePart;
|
|
16
21
|
type MessageContentStatePart = MessagePartState;
|
|
@@ -69,9 +74,7 @@ const ToolUIDisplay = ({
|
|
|
69
74
|
index: number;
|
|
70
75
|
}) => {
|
|
71
76
|
const aui = useAui();
|
|
72
|
-
const Render = useAuiState(
|
|
73
|
-
(s) => s.tools.toolUIs[part.toolName]?.[0]?.render,
|
|
74
|
-
);
|
|
77
|
+
const Render = useAuiState((s) => resolveToolRender(s.tools, part));
|
|
75
78
|
|
|
76
79
|
const partMethods = useMemo(() => aui.message.part({ index }), [aui, index]);
|
|
77
80
|
|
|
@@ -153,9 +156,16 @@ export const MessageContent = ({
|
|
|
153
156
|
</Fragment>
|
|
154
157
|
);
|
|
155
158
|
case "image":
|
|
156
|
-
if (!renderImage) return null;
|
|
157
159
|
return (
|
|
158
|
-
<Fragment key={key}>
|
|
160
|
+
<Fragment key={key}>
|
|
161
|
+
{renderImage ? (
|
|
162
|
+
renderImage({ part, index })
|
|
163
|
+
) : (
|
|
164
|
+
<PartByIndexProvider index={index}>
|
|
165
|
+
<DefaultMessageImage />
|
|
166
|
+
</PartByIndexProvider>
|
|
167
|
+
)}
|
|
168
|
+
</Fragment>
|
|
159
169
|
);
|
|
160
170
|
case "reasoning":
|
|
161
171
|
if (!renderReasoning) return null;
|