@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.
Files changed (32) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/mcp-apps/app-frame.js +2 -2
  4. package/dist/mcp-apps/app-frame.js.map +1 -1
  5. package/dist/mcp-apps/bridge.d.ts.map +1 -1
  6. package/dist/mcp-apps/bridge.js +28 -3
  7. package/dist/mcp-apps/bridge.js.map +1 -1
  8. package/dist/mcp-apps/index.d.ts +1 -1
  9. package/dist/mcp-apps/index.d.ts.map +1 -1
  10. package/dist/mcp-apps/types.d.ts +18 -1
  11. package/dist/mcp-apps/types.d.ts.map +1 -1
  12. package/dist/mcp-apps/types.js.map +1 -1
  13. package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
  14. package/dist/primitives/composer/ComposerAttachmentDropzone.js +14 -12
  15. package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
  16. package/dist/unstable/useComposerInput.d.ts +2 -0
  17. package/dist/unstable/useComposerInput.d.ts.map +1 -1
  18. package/dist/unstable/useComposerInput.js +2 -0
  19. package/dist/unstable/useComposerInput.js.map +1 -1
  20. package/package.json +4 -4
  21. package/src/index.ts +1 -0
  22. package/src/mcp-apps/McpAppRenderer.test.tsx +7 -2
  23. package/src/mcp-apps/app-frame.test.tsx +64 -6
  24. package/src/mcp-apps/app-frame.tsx +2 -2
  25. package/src/mcp-apps/bridge.test.ts +154 -0
  26. package/src/mcp-apps/bridge.ts +51 -9
  27. package/src/mcp-apps/index.ts +1 -0
  28. package/src/mcp-apps/types.ts +15 -1
  29. package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +32 -1
  30. package/src/primitives/composer/ComposerAttachmentDropzone.tsx +14 -12
  31. package/src/primitives/composer/ComposerDisabledThread.test.tsx +121 -0
  32. 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 (!disabled) return;
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
- }, [disabled]);
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 (disabled || !aui.thread.getState().capabilities.attachments) {
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
- [disabled, aui],
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 (disabled || !aui.thread.getState().capabilities.attachments) {
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
- [disabled, isDragging, aui],
68
+ [isDisabled, isDragging, aui],
67
69
  );
68
70
 
69
71
  const handleDragLeaveCapture = useCallback(
70
72
  (e: React.DragEvent) => {
71
- if (disabled) return;
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
- [disabled],
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 (disabled) return;
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
- [disabled, aui],
104
+ [isDisabled, aui],
103
105
  );
104
106
 
105
107
  const mergedProps = {
106
- ...(isDragging && !disabled ? { "data-dragging": "true" } : null),
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();