@assistant-ui/react 0.15.17 → 0.15.19
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/README.md +8 -2
- package/dist/context/providers/ThreadViewportProvider.js +2 -2
- package/dist/context/providers/ThreadViewportProvider.js.map +1 -1
- package/dist/hooks/useToolCallElapsed.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +5 -3
- package/dist/mcp-apps/McpAppRenderer.d.ts +30 -8
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +91 -47
- package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/mcp-apps/app-frame.d.ts.map +1 -1
- package/dist/mcp-apps/app-frame.js +57 -33
- package/dist/mcp-apps/app-frame.js.map +1 -1
- package/dist/mcp-apps/bridge.js +9 -4
- package/dist/mcp-apps/bridge.js.map +1 -1
- package/dist/mcp-apps/index.d.ts +2 -2
- package/dist/primitives/assistantModal/AssistantModalRoot.d.ts.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.js +47 -79
- package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.js +61 -138
- package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +3 -8
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerInputPluginContext.d.ts +1 -2
- package/dist/primitives/composer/ComposerInputPluginContext.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerInputPluginContext.js +1 -6
- package/dist/primitives/composer/ComposerInputPluginContext.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js +46 -15
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +40 -10
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts +6 -0
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts.map +1 -0
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js +9 -0
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js.map +1 -0
- package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerKeyboardResource.js +53 -61
- package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.js +129 -141
- package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
- package/dist/primitives/message/MessageParts.js +2 -14
- package/dist/primitives/message/MessageParts.js.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.d.ts +3 -3
- package/dist/primitives/message/MessagePartsGrouped.js +2 -2
- package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
- package/dist/primitives/messagePart/MessagePartText.d.ts +2 -0
- package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -1
- package/dist/primitives/messagePart/MessagePartText.js +13 -36
- package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
- package/dist/primitives/reasoning/useScrollLock.js +3 -1
- package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +3 -3
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
- package/dist/sandbox-host/SandboxHost.js +36 -9
- package/dist/sandbox-host/SandboxHost.js.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.js +24 -2
- package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
- package/dist/unstable/useMentionAdapter.d.ts +9 -3
- package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
- package/dist/unstable/useMentionAdapter.js +50 -43
- package/dist/unstable/useMentionAdapter.js.map +1 -1
- package/dist/unstable/useMessageStallDetection.d.ts +4 -5
- package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
- package/dist/unstable/useMessageStallDetection.js +16 -15
- package/dist/unstable/useMessageStallDetection.js.map +1 -1
- package/dist/unstable/useModelContextSnapshot.d.ts +34 -0
- package/dist/unstable/useModelContextSnapshot.d.ts.map +1 -0
- package/dist/unstable/useModelContextSnapshot.js +61 -0
- package/dist/unstable/useModelContextSnapshot.js.map +1 -0
- package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.js +2 -8
- package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
- package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts +14 -0
- package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts.map +1 -0
- package/dist/unstable/webmcp/WebMcpRegistrationResource.js +85 -0
- package/dist/unstable/webmcp/WebMcpRegistrationResource.js.map +1 -0
- package/dist/unstable/webmcp/convertTools.d.ts +21 -0
- package/dist/unstable/webmcp/convertTools.d.ts.map +1 -0
- package/dist/unstable/webmcp/convertTools.js +161 -0
- package/dist/unstable/webmcp/convertTools.js.map +1 -0
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts +21 -0
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -0
- package/dist/unstable/webmcp/useWebMcpProvider.js +106 -0
- package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -0
- package/dist/unstable/webmcp/webmcp-host.d.ts +39 -0
- package/dist/unstable/webmcp/webmcp-host.d.ts.map +1 -0
- package/dist/unstable/webmcp/webmcp-host.js +29 -0
- package/dist/unstable/webmcp/webmcp-host.js.map +1 -0
- package/dist/utils/Primitive.d.ts +10 -2
- package/dist/utils/Primitive.d.ts.map +1 -1
- package/dist/utils/Primitive.js +25 -6
- package/dist/utils/Primitive.js.map +1 -1
- package/dist/utils/getSelectionMessageId.d.ts.map +1 -1
- package/dist/utils/getSelectionMessageId.js +17 -3
- package/dist/utils/getSelectionMessageId.js.map +1 -1
- package/package.json +13 -13
- package/src/context/providers/ThreadViewportProvider.tsx +2 -2
- package/src/hooks/useToolCallElapsed.ts +3 -0
- package/src/index.ts +14 -1
- package/src/mcp-apps/McpAppRenderer.test.tsx +253 -5
- package/src/mcp-apps/McpAppRenderer.tsx +147 -52
- package/src/mcp-apps/app-frame.test.tsx +437 -1
- package/src/mcp-apps/app-frame.tsx +76 -21
- package/src/mcp-apps/bridge.test.ts +98 -0
- package/src/mcp-apps/bridge.ts +14 -3
- package/src/mcp-apps/index.ts +5 -1
- package/src/primitives/assistantModal/AssistantModalRoot.test.tsx +123 -0
- package/src/primitives/assistantModal/AssistantModalRoot.tsx +11 -31
- package/src/primitives/assistantModal/AssistantModalTrigger.test.tsx +1 -16
- package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +43 -0
- package/src/primitives/composer/ComposerAttachmentDropzone.tsx +2 -10
- package/src/primitives/composer/ComposerInput.test.tsx +22 -0
- package/src/primitives/composer/ComposerInput.tsx +5 -9
- package/src/primitives/composer/ComposerInputPluginContext.tsx +0 -10
- package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +4 -2
- package/src/primitives/composer/trigger/TriggerPopoverBehavior.test.tsx +109 -0
- package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +4 -2
- package/src/primitives/composer/trigger/matchesTriggerItemQuery.test.ts +52 -0
- package/src/primitives/composer/trigger/matchesTriggerItemQuery.ts +13 -0
- package/src/primitives/composer/trigger/triggerKeyboardResource.ts +13 -7
- package/src/primitives/composer/trigger/triggerNavigationResource.ts +8 -13
- package/src/primitives/message/MessageParts.tsx +1 -21
- package/src/primitives/message/MessagePartsGrouped.test.tsx +93 -0
- package/src/primitives/message/MessagePartsGrouped.tsx +6 -6
- package/src/primitives/messagePart/MessagePartText.test.tsx +69 -0
- package/src/primitives/messagePart/MessagePartText.tsx +22 -9
- package/src/primitives/reasoning/useScrollLock.test.tsx +129 -0
- package/src/primitives/reasoning/useScrollLock.ts +15 -1
- package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +38 -1
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +7 -6
- package/src/sandbox-host/SandboxHost.test.tsx +160 -2
- package/src/sandbox-host/SandboxHost.tsx +50 -10
- package/src/tests/MessageParts.rendererOptions.test.tsx +117 -0
- package/src/tests/toolCallTiming.test.tsx +55 -1
- package/src/unstable/useComposerInputHistory.test.tsx +2 -1
- package/src/unstable/useLiveCompletionAdapter.test.tsx +202 -1
- package/src/unstable/useLiveCompletionAdapter.ts +38 -8
- package/src/unstable/useMentionAdapter.test.tsx +383 -0
- package/src/unstable/useMentionAdapter.ts +94 -57
- package/src/unstable/useMessageStallDetection.ts +26 -19
- package/src/unstable/useModelContextSnapshot.test.tsx +132 -0
- package/src/unstable/useModelContextSnapshot.ts +67 -0
- package/src/unstable/useSlashCommandAdapter.ts +2 -9
- package/src/unstable/webmcp/WebMcpRegistrationResource.ts +92 -0
- package/src/unstable/webmcp/__tests__/webmcp.fake.tsx +168 -0
- package/src/unstable/webmcp/convertTools.test.ts +553 -0
- package/src/unstable/webmcp/convertTools.ts +260 -0
- package/src/unstable/webmcp/useWebMcpProvider.test.tsx +466 -0
- package/src/unstable/webmcp/useWebMcpProvider.ts +148 -0
- package/src/unstable/webmcp/webmcp-host.test.ts +142 -0
- package/src/unstable/webmcp/webmcp-host.ts +83 -0
- package/src/utils/Primitive.test.tsx +46 -1
- package/src/utils/Primitive.tsx +37 -7
- package/src/utils/getSelectionMessageId.test.ts +176 -1
- package/src/utils/getSelectionMessageId.ts +21 -3
- package/dist/utils/invokeCallbackSafely.d.ts +0 -5
- package/dist/utils/invokeCallbackSafely.d.ts.map +0 -1
- package/dist/utils/invokeCallbackSafely.js +0 -15
- package/dist/utils/invokeCallbackSafely.js.map +0 -1
- package/dist/utils/useToolArgsFieldStatus.d.ts +0 -19
- package/dist/utils/useToolArgsFieldStatus.d.ts.map +0 -1
- package/dist/utils/useToolArgsFieldStatus.js +0 -23
- package/dist/utils/useToolArgsFieldStatus.js.map +0 -1
- package/src/utils/invokeCallbackSafely.ts +0 -11
- package/src/utils/useToolArgsFieldStatus.ts +0 -18
|
@@ -37,6 +37,104 @@ async function flush() {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
describe("createMcpAppBridge", () => {
|
|
40
|
+
it("ignores requests and notifications after disposal", async () => {
|
|
41
|
+
const { frame, captured } = makeFrame();
|
|
42
|
+
const callTool = vi.fn().mockResolvedValue({ ok: true });
|
|
43
|
+
const onInitialized = vi.fn();
|
|
44
|
+
const bridge = createMcpAppBridge({
|
|
45
|
+
frame,
|
|
46
|
+
handlers: { callTool, onInitialized },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
bridge.dispose();
|
|
50
|
+
expect(() => bridge.dispose()).not.toThrow();
|
|
51
|
+
|
|
52
|
+
deliver(bridge, {
|
|
53
|
+
jsonrpc: "2.0",
|
|
54
|
+
id: 1,
|
|
55
|
+
method: "tools/call",
|
|
56
|
+
params: { name: "search" },
|
|
57
|
+
});
|
|
58
|
+
deliver(bridge, {
|
|
59
|
+
jsonrpc: "2.0",
|
|
60
|
+
method: "notifications/initialized",
|
|
61
|
+
});
|
|
62
|
+
await flush();
|
|
63
|
+
|
|
64
|
+
expect(callTool).not.toHaveBeenCalled();
|
|
65
|
+
expect(onInitialized).not.toHaveBeenCalled();
|
|
66
|
+
expect(captured).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("does not send a response after disposal", async () => {
|
|
70
|
+
const { frame, captured } = makeFrame();
|
|
71
|
+
let resolveCall!: (value: unknown) => void;
|
|
72
|
+
const callTool = vi.fn(
|
|
73
|
+
() =>
|
|
74
|
+
new Promise((resolve) => {
|
|
75
|
+
resolveCall = resolve;
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
const bridge = createMcpAppBridge({ frame, handlers: { callTool } });
|
|
79
|
+
|
|
80
|
+
deliver(bridge, {
|
|
81
|
+
jsonrpc: "2.0",
|
|
82
|
+
id: 1,
|
|
83
|
+
method: "tools/call",
|
|
84
|
+
params: { name: "search" },
|
|
85
|
+
});
|
|
86
|
+
expect(callTool).toHaveBeenCalledOnce();
|
|
87
|
+
|
|
88
|
+
bridge.dispose();
|
|
89
|
+
resolveCall({ ok: true });
|
|
90
|
+
await flush();
|
|
91
|
+
|
|
92
|
+
expect(captured).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("does not report an async error after disposal", async () => {
|
|
96
|
+
const { frame, captured } = makeFrame();
|
|
97
|
+
let rejectCall!: (reason?: unknown) => void;
|
|
98
|
+
const callTool = vi.fn(
|
|
99
|
+
() =>
|
|
100
|
+
new Promise((_resolve, reject) => {
|
|
101
|
+
rejectCall = reject;
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
const onError = vi.fn();
|
|
105
|
+
const bridge = createMcpAppBridge({
|
|
106
|
+
frame,
|
|
107
|
+
handlers: { callTool, onError },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
deliver(bridge, {
|
|
111
|
+
jsonrpc: "2.0",
|
|
112
|
+
id: 1,
|
|
113
|
+
method: "tools/call",
|
|
114
|
+
params: { name: "search" },
|
|
115
|
+
});
|
|
116
|
+
expect(callTool).toHaveBeenCalledOnce();
|
|
117
|
+
|
|
118
|
+
bridge.dispose();
|
|
119
|
+
rejectCall(new Error("tool failed"));
|
|
120
|
+
await flush();
|
|
121
|
+
|
|
122
|
+
expect(onError).not.toHaveBeenCalled();
|
|
123
|
+
expect(captured).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("does not send host notifications after disposal", () => {
|
|
127
|
+
const { frame, captured } = makeFrame();
|
|
128
|
+
const bridge = createMcpAppBridge({ frame });
|
|
129
|
+
|
|
130
|
+
bridge.dispose();
|
|
131
|
+
bridge.notifyToolInput({ query: "hello" });
|
|
132
|
+
bridge.notifyToolResult({ answer: "world" });
|
|
133
|
+
bridge.notifyHostContextChanged({ theme: "dark" });
|
|
134
|
+
|
|
135
|
+
expect(captured).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
40
138
|
it("responds to ui/initialize with host info, version, and capabilities", async () => {
|
|
41
139
|
const { frame, captured } = makeFrame();
|
|
42
140
|
const bridge = createMcpAppBridge({
|
package/src/mcp-apps/bridge.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SandboxHostFrame } from "../sandbox-host/SandboxHost";
|
|
2
|
-
import {
|
|
2
|
+
import { invokeUserCallback } from "@assistant-ui/core/internal";
|
|
3
3
|
import {
|
|
4
4
|
MCP_APP_PROTOCOL_VERSION,
|
|
5
5
|
type McpAppBridgeHandlers,
|
|
@@ -90,8 +90,10 @@ export function createMcpAppBridge(
|
|
|
90
90
|
hostInfo = DEFAULT_HOST_INFO,
|
|
91
91
|
hostContext = {},
|
|
92
92
|
} = opts;
|
|
93
|
+
let disposed = false;
|
|
93
94
|
|
|
94
95
|
const post = (msg: McpAppJsonRpcMessage) => {
|
|
96
|
+
if (disposed) return;
|
|
95
97
|
frame.sendMessage(msg);
|
|
96
98
|
};
|
|
97
99
|
|
|
@@ -125,7 +127,13 @@ export function createMcpAppBridge(
|
|
|
125
127
|
};
|
|
126
128
|
|
|
127
129
|
const reportError = (error: Error) => {
|
|
128
|
-
|
|
130
|
+
if (disposed) return;
|
|
131
|
+
invokeUserCallback(
|
|
132
|
+
"assistant-ui",
|
|
133
|
+
"MCP App onError",
|
|
134
|
+
handlers.onError?.bind(handlers),
|
|
135
|
+
error,
|
|
136
|
+
);
|
|
129
137
|
};
|
|
130
138
|
|
|
131
139
|
const handleRequest = async (req: McpAppJsonRpcRequest) => {
|
|
@@ -430,6 +438,7 @@ export function createMcpAppBridge(
|
|
|
430
438
|
// The host applies the cross-origin guard before delegating; this only
|
|
431
439
|
// validates the JSON-RPC envelope.
|
|
432
440
|
const onMessage = (event: MessageEvent) => {
|
|
441
|
+
if (disposed) return;
|
|
433
442
|
if (!isJsonRpcMessage(event.data)) return;
|
|
434
443
|
|
|
435
444
|
const msg = event.data;
|
|
@@ -442,7 +451,9 @@ export function createMcpAppBridge(
|
|
|
442
451
|
|
|
443
452
|
return {
|
|
444
453
|
onMessage,
|
|
445
|
-
dispose: () => {
|
|
454
|
+
dispose: () => {
|
|
455
|
+
disposed = true;
|
|
456
|
+
},
|
|
446
457
|
notifyToolInput: (input: unknown) => {
|
|
447
458
|
post({
|
|
448
459
|
jsonrpc: "2.0",
|
package/src/mcp-apps/index.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
McpAppRenderer,
|
|
3
|
+
type McpAppPartOptions,
|
|
4
|
+
type McpAppRendererOptions,
|
|
5
|
+
} from "./McpAppRenderer";
|
|
2
6
|
export { McpAppsRemoteHost } from "./McpAppsRemoteHost";
|
|
3
7
|
export { getMcpAppFromToolPart } from "./utils";
|
|
4
8
|
export type {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
act,
|
|
5
|
+
cleanup,
|
|
6
|
+
fireEvent,
|
|
7
|
+
render,
|
|
8
|
+
screen,
|
|
9
|
+
} from "@testing-library/react";
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import { AssistantRuntimeProvider } from "../../context";
|
|
13
|
+
import { useLocalRuntime } from "../../legacy-runtime/runtime-cores/local/useLocalRuntime";
|
|
14
|
+
import { AssistantModalPrimitiveContent } from "./AssistantModalContent";
|
|
15
|
+
import { AssistantModalPrimitiveRoot } from "./AssistantModalRoot";
|
|
16
|
+
import { AssistantModalPrimitiveTrigger } from "./AssistantModalTrigger";
|
|
17
|
+
|
|
18
|
+
const adapter = {
|
|
19
|
+
async *run() {
|
|
20
|
+
yield { content: [{ type: "text" as const, text: "Hello" }] };
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const Modal = ({
|
|
25
|
+
controlled = true,
|
|
26
|
+
onOpenChange,
|
|
27
|
+
...props
|
|
28
|
+
}: AssistantModalPrimitiveRoot.Props & { controlled?: boolean }) => {
|
|
29
|
+
const runtime = useLocalRuntime(adapter);
|
|
30
|
+
const [open, setOpen] = useState(false);
|
|
31
|
+
return (
|
|
32
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
33
|
+
<button onClick={() => runtime.thread.append("Hello")}>Start run</button>
|
|
34
|
+
<AssistantModalPrimitiveRoot
|
|
35
|
+
{...(controlled ? { open } : {})}
|
|
36
|
+
onOpenChange={(value) => {
|
|
37
|
+
setOpen(value);
|
|
38
|
+
onOpenChange?.(value);
|
|
39
|
+
}}
|
|
40
|
+
{...props}
|
|
41
|
+
>
|
|
42
|
+
<AssistantModalPrimitiveTrigger>
|
|
43
|
+
Toggle chat
|
|
44
|
+
</AssistantModalPrimitiveTrigger>
|
|
45
|
+
<AssistantModalPrimitiveContent aria-label="Chat">
|
|
46
|
+
Response
|
|
47
|
+
</AssistantModalPrimitiveContent>
|
|
48
|
+
</AssistantModalPrimitiveRoot>
|
|
49
|
+
</AssistantRuntimeProvider>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
afterEach(cleanup);
|
|
54
|
+
|
|
55
|
+
describe("AssistantModalPrimitiveRoot run start", () => {
|
|
56
|
+
it.each([true, false])(
|
|
57
|
+
"opens on run start and closes through the trigger (controlled: %s)",
|
|
58
|
+
async (controlled) => {
|
|
59
|
+
const onOpenChange = vi.fn();
|
|
60
|
+
render(<Modal controlled={controlled} onOpenChange={onOpenChange} />);
|
|
61
|
+
expect(screen.queryByRole("dialog")).toBeNull();
|
|
62
|
+
|
|
63
|
+
await act(async () => {
|
|
64
|
+
fireEvent.click(screen.getByRole("button", { name: "Start run" }));
|
|
65
|
+
});
|
|
66
|
+
expect(await screen.findByRole("dialog", { name: "Chat" })).toBeDefined();
|
|
67
|
+
expect(onOpenChange).toHaveBeenCalledExactlyOnceWith(true);
|
|
68
|
+
|
|
69
|
+
await act(async () => {
|
|
70
|
+
fireEvent.click(screen.getByRole("button", { name: "Start run" }));
|
|
71
|
+
});
|
|
72
|
+
expect(onOpenChange).toHaveBeenCalledExactlyOnceWith(true);
|
|
73
|
+
|
|
74
|
+
fireEvent.click(screen.getByRole("button", { name: "Toggle chat" }));
|
|
75
|
+
expect(screen.queryByRole("dialog")).toBeNull();
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
it.each([{ open: true }, { controlled: false, defaultOpen: true }])(
|
|
80
|
+
"does not request opening when already open (%o)",
|
|
81
|
+
async (props) => {
|
|
82
|
+
const onOpenChange = vi.fn();
|
|
83
|
+
render(<Modal {...props} onOpenChange={onOpenChange} />);
|
|
84
|
+
expect(screen.getByRole("dialog", { name: "Chat" })).toBeDefined();
|
|
85
|
+
|
|
86
|
+
await act(async () => {
|
|
87
|
+
fireEvent.click(screen.getByRole("button", { name: "Start run" }));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
91
|
+
expect(screen.getByRole("dialog", { name: "Chat" })).toBeDefined();
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
it("notifies the current owner without overriding its controlled value", async () => {
|
|
96
|
+
const previous = vi.fn();
|
|
97
|
+
const current = vi.fn();
|
|
98
|
+
const view = render(<Modal open={false} onOpenChange={previous} />);
|
|
99
|
+
view.rerender(<Modal open={false} onOpenChange={current} />);
|
|
100
|
+
|
|
101
|
+
await act(async () => {
|
|
102
|
+
fireEvent.click(screen.getByRole("button", { name: "Start run" }));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(current).toHaveBeenCalledExactlyOnceWith(true);
|
|
106
|
+
expect(previous).not.toHaveBeenCalled();
|
|
107
|
+
expect(screen.queryByRole("dialog")).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("does not request opening when auto-open is disabled", async () => {
|
|
111
|
+
const onOpenChange = vi.fn();
|
|
112
|
+
render(
|
|
113
|
+
<Modal unstable_openOnRunStart={false} onOpenChange={onOpenChange} />,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
await act(async () => {
|
|
117
|
+
fireEvent.click(screen.getByRole("button", { name: "Start run" }));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
121
|
+
expect(screen.queryByRole("dialog")).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { type FC,
|
|
3
|
+
import { type FC, useState } from "react";
|
|
4
4
|
import { Popover as PopoverPrimitive } from "radix-ui";
|
|
5
5
|
import { type ScopedProps, usePopoverScope } from "./scope";
|
|
6
|
-
import {
|
|
6
|
+
import { useAuiEvent } from "@assistant-ui/store";
|
|
7
7
|
|
|
8
8
|
export namespace AssistantModalPrimitiveRoot {
|
|
9
9
|
export type Props = PopoverPrimitive.PopoverProps & {
|
|
@@ -11,54 +11,34 @@ export namespace AssistantModalPrimitiveRoot {
|
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const useAssistantModalOpenState = ({
|
|
15
|
-
defaultOpen = false,
|
|
16
|
-
unstable_openOnRunStart = true,
|
|
17
|
-
}: {
|
|
18
|
-
defaultOpen?: boolean | undefined;
|
|
19
|
-
unstable_openOnRunStart?: boolean | undefined;
|
|
20
|
-
}) => {
|
|
21
|
-
const state = useState(defaultOpen);
|
|
22
|
-
|
|
23
|
-
const [, setOpen] = state;
|
|
24
|
-
const aui = useAui();
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (!unstable_openOnRunStart) return undefined;
|
|
27
|
-
|
|
28
|
-
return aui.on("thread.runStart", () => {
|
|
29
|
-
setOpen(true);
|
|
30
|
-
});
|
|
31
|
-
}, [unstable_openOnRunStart, aui, setOpen]);
|
|
32
|
-
|
|
33
|
-
return state;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
14
|
export const AssistantModalPrimitiveRoot: FC<
|
|
37
15
|
AssistantModalPrimitiveRoot.Props
|
|
38
16
|
> = ({
|
|
39
17
|
__scopeAssistantModal,
|
|
40
|
-
defaultOpen,
|
|
41
|
-
unstable_openOnRunStart,
|
|
18
|
+
defaultOpen = false,
|
|
19
|
+
unstable_openOnRunStart = true,
|
|
42
20
|
open,
|
|
43
21
|
onOpenChange,
|
|
44
22
|
...rest
|
|
45
23
|
}: ScopedProps<AssistantModalPrimitiveRoot.Props>) => {
|
|
46
24
|
const scope = usePopoverScope(__scopeAssistantModal);
|
|
47
25
|
|
|
48
|
-
const [modalOpen, setOpen] =
|
|
49
|
-
|
|
50
|
-
unstable_openOnRunStart,
|
|
51
|
-
});
|
|
26
|
+
const [modalOpen, setOpen] = useState(defaultOpen);
|
|
27
|
+
const isOpen = open ?? modalOpen;
|
|
52
28
|
|
|
53
29
|
const openChangeHandler = (open: boolean) => {
|
|
54
30
|
onOpenChange?.(open);
|
|
55
31
|
setOpen(open);
|
|
56
32
|
};
|
|
57
33
|
|
|
34
|
+
useAuiEvent("thread.runStart", () => {
|
|
35
|
+
if (unstable_openOnRunStart && !isOpen) openChangeHandler(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
58
38
|
return (
|
|
59
39
|
<PopoverPrimitive.Root
|
|
60
40
|
{...scope}
|
|
61
|
-
open={
|
|
41
|
+
open={isOpen}
|
|
62
42
|
onOpenChange={openChangeHandler}
|
|
63
43
|
{...rest}
|
|
64
44
|
/>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { AssistantModalPrimitiveAnchor } from "./AssistantModalAnchor";
|
|
4
|
-
import { AssistantModalPrimitiveContent } from "./AssistantModalContent";
|
|
5
4
|
import { AssistantModalPrimitiveRoot } from "./AssistantModalRoot";
|
|
6
5
|
import { AssistantModalPrimitiveTrigger } from "./AssistantModalTrigger";
|
|
7
6
|
|
|
@@ -9,9 +8,7 @@ vi.mock("@assistant-ui/store", async (importOriginal) => {
|
|
|
9
8
|
const actual = await importOriginal<typeof import("@assistant-ui/store")>();
|
|
10
9
|
return {
|
|
11
10
|
...actual,
|
|
12
|
-
|
|
13
|
-
on: () => () => {},
|
|
14
|
-
}),
|
|
11
|
+
useAuiEvent: () => {},
|
|
15
12
|
};
|
|
16
13
|
});
|
|
17
14
|
|
|
@@ -65,16 +62,4 @@ describe("AssistantModalPrimitive render props", () => {
|
|
|
65
62
|
expect(renderHtml).toContain('class="anchor"');
|
|
66
63
|
expect(renderHtml).toContain("Anchor");
|
|
67
64
|
});
|
|
68
|
-
|
|
69
|
-
it("accepts render on Trigger, Anchor, and Content", () => {
|
|
70
|
-
const tree = (
|
|
71
|
-
<AssistantModalPrimitiveRoot open>
|
|
72
|
-
<AssistantModalPrimitiveTrigger render={<button type="button" />} />
|
|
73
|
-
<AssistantModalPrimitiveAnchor render={<div />} />
|
|
74
|
-
<AssistantModalPrimitiveContent render={<div />} />
|
|
75
|
-
</AssistantModalPrimitiveRoot>
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
expect(tree).toBeTruthy();
|
|
79
|
-
});
|
|
80
65
|
});
|
|
@@ -74,6 +74,49 @@ describe("ComposerPrimitiveAttachmentDropzone", () => {
|
|
|
74
74
|
vi.restoreAllMocks();
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
it("composes a render element and keeps the drag props attached", async () => {
|
|
78
|
+
await act(async () => {
|
|
79
|
+
root.render(
|
|
80
|
+
<ComposerPrimitiveAttachmentDropzone
|
|
81
|
+
data-testid="dropzone"
|
|
82
|
+
render={<section className="child" />}
|
|
83
|
+
className="parent"
|
|
84
|
+
>
|
|
85
|
+
<div>outer</div>
|
|
86
|
+
</ComposerPrimitiveAttachmentDropzone>,
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const dropzone = container.querySelector(
|
|
91
|
+
"section[data-testid='dropzone']",
|
|
92
|
+
) as HTMLElement;
|
|
93
|
+
expect(dropzone).not.toBeNull();
|
|
94
|
+
expect(dropzone.textContent).toBe("outer");
|
|
95
|
+
expect(dropzone.className).toContain("parent");
|
|
96
|
+
expect(dropzone.className).toContain("child");
|
|
97
|
+
|
|
98
|
+
await act(async () => {
|
|
99
|
+
dropzone.dispatchEvent(createDragEvent("dragenter", ["Files"]));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(dropzone.getAttribute("data-dragging")).toBe("true");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("falls back to the render element's own children", async () => {
|
|
106
|
+
await act(async () => {
|
|
107
|
+
root.render(
|
|
108
|
+
<ComposerPrimitiveAttachmentDropzone
|
|
109
|
+
data-testid="dropzone"
|
|
110
|
+
render={<section>fallback</section>}
|
|
111
|
+
/>,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(
|
|
116
|
+
container.querySelector("section[data-testid='dropzone']")?.textContent,
|
|
117
|
+
).toBe("fallback");
|
|
118
|
+
});
|
|
119
|
+
|
|
77
120
|
it("starts all dropped attachments before awaiting completion", async () => {
|
|
78
121
|
const resolvers: Array<() => void> = [];
|
|
79
122
|
addAttachment.mockImplementation(
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
useCallback,
|
|
6
6
|
useState,
|
|
7
7
|
type ReactElement,
|
|
8
|
-
cloneElement,
|
|
9
8
|
isValidElement,
|
|
10
9
|
} from "react";
|
|
11
10
|
|
|
@@ -13,6 +12,7 @@ import { composeEventHandlers } from "radix-ui/internal";
|
|
|
13
12
|
import { Slot } from "radix-ui";
|
|
14
13
|
import type React from "react";
|
|
15
14
|
import { useAui } from "@assistant-ui/store";
|
|
15
|
+
import { renderSlot } from "../../utils/Primitive";
|
|
16
16
|
|
|
17
17
|
export namespace ComposerPrimitiveAttachmentDropzone {
|
|
18
18
|
export type Element = HTMLDivElement;
|
|
@@ -116,15 +116,7 @@ export const ComposerPrimitiveAttachmentDropzone = forwardRef<
|
|
|
116
116
|
};
|
|
117
117
|
|
|
118
118
|
if (render && isValidElement(render)) {
|
|
119
|
-
|
|
120
|
-
children !== undefined
|
|
121
|
-
? children
|
|
122
|
-
: (render.props as Record<string, unknown>).children;
|
|
123
|
-
return (
|
|
124
|
-
<Slot.Root {...mergedProps}>
|
|
125
|
-
{cloneElement(render, undefined, renderChildren as React.ReactNode)}
|
|
126
|
-
</Slot.Root>
|
|
127
|
-
);
|
|
119
|
+
return renderSlot(render, children, mergedProps);
|
|
128
120
|
}
|
|
129
121
|
|
|
130
122
|
const Comp = asChild ? Slot.Root : "div";
|
|
@@ -240,6 +240,28 @@ describe("ComposerPrimitiveInput", () => {
|
|
|
240
240
|
return textarea;
|
|
241
241
|
};
|
|
242
242
|
|
|
243
|
+
it("composes a render element with the computed input props", async () => {
|
|
244
|
+
await act(async () => {
|
|
245
|
+
root.render(
|
|
246
|
+
<form>
|
|
247
|
+
<ComposerPrimitiveInput
|
|
248
|
+
render={<textarea data-testid="custom" className="child" />}
|
|
249
|
+
className="parent"
|
|
250
|
+
/>
|
|
251
|
+
</form>,
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const textarea = container.querySelector(
|
|
256
|
+
"textarea[data-testid='custom']",
|
|
257
|
+
) as HTMLTextAreaElement;
|
|
258
|
+
expect(textarea).not.toBeNull();
|
|
259
|
+
expect(textarea.className).toContain("parent");
|
|
260
|
+
expect(textarea.className).toContain("child");
|
|
261
|
+
expect(textarea.name).toBe("input");
|
|
262
|
+
expect(textarea.hasAttribute("render")).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
|
|
243
265
|
it("syncs setText during active composition so React 19 cannot reset the textarea", async () => {
|
|
244
266
|
const textarea = await mount();
|
|
245
267
|
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
useCallback,
|
|
13
13
|
useEffect,
|
|
14
14
|
useRef,
|
|
15
|
-
cloneElement,
|
|
16
15
|
isValidElement,
|
|
17
16
|
} from "react";
|
|
18
17
|
import TextareaAutosize, {
|
|
@@ -22,6 +21,7 @@ import TextareaAutosize, {
|
|
|
22
21
|
import { useEscapeKeydown } from "radix-ui/internal";
|
|
23
22
|
import { useOnScrollToBottom } from "../../utils/hooks/useOnScrollToBottom";
|
|
24
23
|
import { useMediaQuery } from "../../utils/hooks/useMediaQuery";
|
|
24
|
+
import { renderSlot } from "../../utils/Primitive";
|
|
25
25
|
import { useAui } from "@assistant-ui/store";
|
|
26
26
|
import { flushTapSync } from "@assistant-ui/tap";
|
|
27
27
|
import { useComposerInputPluginRegistryOptional } from "./ComposerInputPluginContext";
|
|
@@ -434,14 +434,10 @@ export const ComposerPrimitiveInput = forwardRef<
|
|
|
434
434
|
};
|
|
435
435
|
|
|
436
436
|
if (render && isValidElement(render)) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return (
|
|
442
|
-
<Slot.Root {...inputProps}>
|
|
443
|
-
{cloneElement(render, undefined, renderChildren)}
|
|
444
|
-
</Slot.Root>
|
|
437
|
+
return renderSlot(
|
|
438
|
+
render,
|
|
439
|
+
(rest as { children?: ReactNode }).children,
|
|
440
|
+
inputProps,
|
|
445
441
|
);
|
|
446
442
|
}
|
|
447
443
|
|
|
@@ -51,16 +51,6 @@ export type ComposerInputPluginRegistry = {
|
|
|
51
51
|
const ComposerInputPluginRegistryContext =
|
|
52
52
|
createContext<ComposerInputPluginRegistry | null>(null);
|
|
53
53
|
|
|
54
|
-
export const useComposerInputPluginRegistry =
|
|
55
|
-
(): ComposerInputPluginRegistry => {
|
|
56
|
-
const ctx = useContext(ComposerInputPluginRegistryContext);
|
|
57
|
-
if (!ctx)
|
|
58
|
-
throw new Error(
|
|
59
|
-
"useComposerInputPluginRegistry must be used within a ComposerInputPluginProvider",
|
|
60
|
-
);
|
|
61
|
-
return ctx;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
54
|
export const useComposerInputPluginRegistryOptional =
|
|
65
55
|
(): ComposerInputPluginRegistry | null => {
|
|
66
56
|
return useContext(ComposerInputPluginRegistryContext);
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
Unstable_TriggerItem,
|
|
6
6
|
} from "@assistant-ui/core";
|
|
7
7
|
import { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
|
|
8
|
-
import { useEffect, useRef, type FC } from "react";
|
|
8
|
+
import { useEffect, useInsertionEffect, useRef, type FC } from "react";
|
|
9
9
|
import { useTriggerBehaviorRegistration } from "./TriggerPopover";
|
|
10
10
|
import type { TriggerBehavior } from "./triggerSelectionResource";
|
|
11
11
|
|
|
@@ -42,7 +42,9 @@ export const ComposerPrimitiveTriggerPopoverAction: FC<
|
|
|
42
42
|
> = ({ formatter, onExecute, removeOnExecute }) => {
|
|
43
43
|
const { register } = useTriggerBehaviorRegistration();
|
|
44
44
|
const onExecuteRef = useRef(onExecute);
|
|
45
|
-
|
|
45
|
+
useInsertionEffect(() => {
|
|
46
|
+
onExecuteRef.current = onExecute;
|
|
47
|
+
}, [onExecute]);
|
|
46
48
|
|
|
47
49
|
useEffect(() => {
|
|
48
50
|
const behavior: TriggerBehavior = {
|