@assistant-ui/react 0.15.26 → 0.15.27
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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/mcp-apps/app-frame.js +2 -2
- package/dist/mcp-apps/app-frame.js.map +1 -1
- package/dist/mcp-apps/bridge.d.ts.map +1 -1
- package/dist/mcp-apps/bridge.js +28 -3
- package/dist/mcp-apps/bridge.js.map +1 -1
- package/dist/mcp-apps/index.d.ts +1 -1
- package/dist/mcp-apps/index.d.ts.map +1 -1
- package/dist/mcp-apps/types.d.ts +18 -1
- package/dist/mcp-apps/types.d.ts.map +1 -1
- package/dist/mcp-apps/types.js.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.js +14 -12
- package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
- package/dist/unstable/useComposerInput.d.ts +2 -0
- package/dist/unstable/useComposerInput.d.ts.map +1 -1
- package/dist/unstable/useComposerInput.js +2 -0
- package/dist/unstable/useComposerInput.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +1 -0
- package/src/mcp-apps/McpAppRenderer.test.tsx +7 -2
- package/src/mcp-apps/app-frame.test.tsx +64 -6
- package/src/mcp-apps/app-frame.tsx +2 -2
- package/src/mcp-apps/bridge.test.ts +154 -0
- package/src/mcp-apps/bridge.ts +51 -9
- package/src/mcp-apps/index.ts +1 -0
- package/src/mcp-apps/types.ts +15 -1
- package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +32 -1
- package/src/primitives/composer/ComposerAttachmentDropzone.tsx +14 -12
- package/src/primitives/composer/ComposerDisabledThread.test.tsx +121 -0
- package/src/unstable/useComposerInput.ts +2 -0
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { composeEventHandlers } from "radix-ui/internal";
|
|
13
13
|
import { Slot } from "radix-ui";
|
|
14
14
|
import type React from "react";
|
|
15
|
-
import { useAui } from "@assistant-ui/store";
|
|
15
|
+
import { useAui, useAuiState } from "@assistant-ui/store";
|
|
16
16
|
import { renderSlot } from "../../utils/Primitive";
|
|
17
17
|
|
|
18
18
|
export namespace ComposerPrimitiveAttachmentDropzone {
|
|
@@ -30,13 +30,15 @@ export const ComposerPrimitiveAttachmentDropzone = forwardRef<
|
|
|
30
30
|
>(({ disabled, asChild = false, render, children, ...rest }, ref) => {
|
|
31
31
|
const [isDragging, setIsDragging] = useState(false);
|
|
32
32
|
const aui = useAui();
|
|
33
|
+
const disabledByThread = useAuiState((s) => s.thread.isDisabled);
|
|
34
|
+
const isDisabled = disabled || disabledByThread;
|
|
33
35
|
|
|
34
36
|
useEffect(() => {
|
|
35
|
-
if (!
|
|
37
|
+
if (!isDisabled) return;
|
|
36
38
|
// Disabled handlers cannot clear the latch, so reset it when the enabled period ends.
|
|
37
39
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
38
40
|
setIsDragging(false);
|
|
39
|
-
}, [
|
|
41
|
+
}, [isDisabled]);
|
|
40
42
|
|
|
41
43
|
// An unprevented file drop navigates the tab to the file, so file drags are
|
|
42
44
|
// claimed via preventDefault even when the runtime does not support attachments.
|
|
@@ -44,31 +46,31 @@ export const ComposerPrimitiveAttachmentDropzone = forwardRef<
|
|
|
44
46
|
(e: React.DragEvent) => {
|
|
45
47
|
if (!e.dataTransfer.types.includes("Files")) return;
|
|
46
48
|
e.preventDefault();
|
|
47
|
-
if (
|
|
49
|
+
if (isDisabled || !aui.thread.getState().capabilities.attachments) {
|
|
48
50
|
e.dataTransfer.dropEffect = "none";
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
51
53
|
setIsDragging(true);
|
|
52
54
|
},
|
|
53
|
-
[
|
|
55
|
+
[isDisabled, aui],
|
|
54
56
|
);
|
|
55
57
|
|
|
56
58
|
const handleDragOverCapture = useCallback(
|
|
57
59
|
(e: React.DragEvent) => {
|
|
58
60
|
if (!e.dataTransfer.types.includes("Files")) return;
|
|
59
61
|
e.preventDefault();
|
|
60
|
-
if (
|
|
62
|
+
if (isDisabled || !aui.thread.getState().capabilities.attachments) {
|
|
61
63
|
e.dataTransfer.dropEffect = "none";
|
|
62
64
|
return;
|
|
63
65
|
}
|
|
64
66
|
if (!isDragging) setIsDragging(true);
|
|
65
67
|
},
|
|
66
|
-
[
|
|
68
|
+
[isDisabled, isDragging, aui],
|
|
67
69
|
);
|
|
68
70
|
|
|
69
71
|
const handleDragLeaveCapture = useCallback(
|
|
70
72
|
(e: React.DragEvent) => {
|
|
71
|
-
if (
|
|
73
|
+
if (isDisabled) return;
|
|
72
74
|
e.preventDefault();
|
|
73
75
|
const next = e.relatedTarget as Node | null;
|
|
74
76
|
if (next && e.currentTarget.contains(next)) {
|
|
@@ -76,7 +78,7 @@ export const ComposerPrimitiveAttachmentDropzone = forwardRef<
|
|
|
76
78
|
}
|
|
77
79
|
setIsDragging(false);
|
|
78
80
|
},
|
|
79
|
-
[
|
|
81
|
+
[isDisabled],
|
|
80
82
|
);
|
|
81
83
|
|
|
82
84
|
const handleDrop = useCallback(
|
|
@@ -84,7 +86,7 @@ export const ComposerPrimitiveAttachmentDropzone = forwardRef<
|
|
|
84
86
|
setIsDragging(false);
|
|
85
87
|
if (!e.dataTransfer.types.includes("Files")) return;
|
|
86
88
|
e.preventDefault();
|
|
87
|
-
if (
|
|
89
|
+
if (isDisabled) return;
|
|
88
90
|
const files = Array.from(e.dataTransfer.files);
|
|
89
91
|
if (!aui.thread.getState().capabilities.attachments || files.length === 0)
|
|
90
92
|
return;
|
|
@@ -99,11 +101,11 @@ export const ComposerPrimitiveAttachmentDropzone = forwardRef<
|
|
|
99
101
|
}),
|
|
100
102
|
);
|
|
101
103
|
},
|
|
102
|
-
[
|
|
104
|
+
[isDisabled, aui],
|
|
103
105
|
);
|
|
104
106
|
|
|
105
107
|
const mergedProps = {
|
|
106
|
-
...(isDragging && !
|
|
108
|
+
...(isDragging && !isDisabled ? { "data-dragging": "true" } : null),
|
|
107
109
|
...rest,
|
|
108
110
|
onDragEnterCapture: composeEventHandlers(
|
|
109
111
|
rest.onDragEnterCapture,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/** @vitest-environment jsdom */
|
|
2
|
+
import {
|
|
3
|
+
act,
|
|
4
|
+
cleanup,
|
|
5
|
+
fireEvent,
|
|
6
|
+
render,
|
|
7
|
+
screen,
|
|
8
|
+
} from "@testing-library/react";
|
|
9
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
import {
|
|
11
|
+
AssistantRuntimeProvider,
|
|
12
|
+
useExternalStoreRuntime,
|
|
13
|
+
} from "@assistant-ui/core/react";
|
|
14
|
+
import type { AttachmentAdapter, ThreadMessage } from "@assistant-ui/core";
|
|
15
|
+
import { ComposerPrimitiveAddAttachment } from "./ComposerAddAttachment";
|
|
16
|
+
import { ComposerPrimitiveInput } from "./ComposerInput";
|
|
17
|
+
import { ComposerPrimitiveRoot } from "./ComposerRoot";
|
|
18
|
+
import { ComposerPrimitiveSend } from "./ComposerSend";
|
|
19
|
+
import { ThreadPrimitiveViewportProvider } from "../../context/providers/ThreadViewportProvider";
|
|
20
|
+
|
|
21
|
+
const makeAttachmentAdapter = () => {
|
|
22
|
+
const add = vi.fn(async ({ file }: { file: File }) => ({
|
|
23
|
+
id: file.name,
|
|
24
|
+
type: "file" as const,
|
|
25
|
+
name: file.name,
|
|
26
|
+
contentType: file.type,
|
|
27
|
+
file,
|
|
28
|
+
status: {
|
|
29
|
+
type: "requires-action" as const,
|
|
30
|
+
reason: "composer-send" as const,
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
const adapter: AttachmentAdapter = {
|
|
34
|
+
accept: "*",
|
|
35
|
+
add,
|
|
36
|
+
remove: async () => {},
|
|
37
|
+
send: async (attachment) => ({
|
|
38
|
+
...attachment,
|
|
39
|
+
status: { type: "complete" },
|
|
40
|
+
content: [{ type: "text", text: "file body" }],
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
return { adapter, add };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const App = ({
|
|
47
|
+
isDisabled,
|
|
48
|
+
onNew,
|
|
49
|
+
attachments,
|
|
50
|
+
}: {
|
|
51
|
+
isDisabled: boolean;
|
|
52
|
+
onNew: () => Promise<void>;
|
|
53
|
+
attachments: AttachmentAdapter;
|
|
54
|
+
}) => {
|
|
55
|
+
const runtime = useExternalStoreRuntime<ThreadMessage>({
|
|
56
|
+
messages: [],
|
|
57
|
+
isDisabled,
|
|
58
|
+
onNew,
|
|
59
|
+
adapters: { attachments },
|
|
60
|
+
});
|
|
61
|
+
return (
|
|
62
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
63
|
+
<ThreadPrimitiveViewportProvider>
|
|
64
|
+
<ComposerPrimitiveRoot>
|
|
65
|
+
<ComposerPrimitiveInput aria-label="input" />
|
|
66
|
+
<ComposerPrimitiveAddAttachment>
|
|
67
|
+
attach
|
|
68
|
+
</ComposerPrimitiveAddAttachment>
|
|
69
|
+
<ComposerPrimitiveSend>send</ComposerPrimitiveSend>
|
|
70
|
+
</ComposerPrimitiveRoot>
|
|
71
|
+
</ThreadPrimitiveViewportProvider>
|
|
72
|
+
</AssistantRuntimeProvider>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const button = (name: string) =>
|
|
77
|
+
screen.getByRole("button", { name }) as HTMLButtonElement;
|
|
78
|
+
|
|
79
|
+
const settle = () =>
|
|
80
|
+
act(async () => {
|
|
81
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
cleanup();
|
|
86
|
+
document.body.querySelectorAll('input[type="file"]').forEach((input) => {
|
|
87
|
+
input.remove();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("composer on a disabled thread", () => {
|
|
92
|
+
it("blocks sending a draft that was entered before the thread was disabled", async () => {
|
|
93
|
+
const onNew = vi.fn(async () => {});
|
|
94
|
+
const { adapter } = makeAttachmentAdapter();
|
|
95
|
+
const view = render(
|
|
96
|
+
<App isDisabled={false} onNew={onNew} attachments={adapter} />,
|
|
97
|
+
);
|
|
98
|
+
fireEvent.change(screen.getByRole("textbox", { name: "input" }), {
|
|
99
|
+
target: { value: "hello" },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
view.rerender(<App isDisabled onNew={onNew} attachments={adapter} />);
|
|
103
|
+
await settle();
|
|
104
|
+
|
|
105
|
+
expect(button("send").disabled).toBe(true);
|
|
106
|
+
fireEvent.click(button("send"));
|
|
107
|
+
await settle();
|
|
108
|
+
expect(onNew).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("disables attachment selection", () => {
|
|
112
|
+
const onNew = vi.fn(async () => {});
|
|
113
|
+
const { adapter, add } = makeAttachmentAdapter();
|
|
114
|
+
render(<App isDisabled onNew={onNew} attachments={adapter} />);
|
|
115
|
+
|
|
116
|
+
expect(button("attach").disabled).toBe(true);
|
|
117
|
+
fireEvent.click(button("attach"));
|
|
118
|
+
expect(document.body.querySelector('input[type="file"]')).toBeNull();
|
|
119
|
+
expect(add).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -68,6 +68,8 @@ export type Unstable_ComposerInput = {
|
|
|
68
68
|
* disabled={isDisabled}
|
|
69
69
|
* onChange={(e) => setText(e.target.value)}
|
|
70
70
|
* onKeyDown={(e) => {
|
|
71
|
+
* if (e.nativeEvent.isComposing || e.keyCode === 229) return;
|
|
72
|
+
*
|
|
71
73
|
* if (e.key === "Enter" && !e.shiftKey && canSend) {
|
|
72
74
|
* e.preventDefault();
|
|
73
75
|
* send();
|