@assistant-ui/react 0.15.25 → 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/hooks/useToolCallElapsed.d.ts.map +1 -1
- package/dist/hooks/useToolCallElapsed.js +3 -2
- package/dist/hooks/useToolCallElapsed.js.map +1 -1
- package/dist/index.d.ts +31 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +3 -2
- 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 +58 -48
- 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/csp.d.ts +4 -0
- package/dist/mcp-apps/csp.d.ts.map +1 -0
- package/dist/mcp-apps/csp.js +93 -0
- package/dist/mcp-apps/csp.js.map +1 -0
- 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 +19 -1
- package/dist/mcp-apps/types.d.ts.map +1 -1
- package/dist/mcp-apps/types.js.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.d.ts +1 -0
- package/dist/primitives/assistantModal/AssistantModalRoot.d.ts.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
- package/dist/primitives/attachment.d.ts +3 -1
- package/dist/primitives/attachment.d.ts.map +1 -1
- package/dist/primitives/attachment.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/primitives/composer/ComposerCancel.d.ts +5 -1
- package/dist/primitives/composer/ComposerCancel.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerCancel.js +14 -1
- package/dist/primitives/composer/ComposerCancel.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.d.ts +14 -0
- package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +4 -3
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerRoot.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerRoot.js +24 -19
- package/dist/primitives/composer/ComposerRoot.js.map +1 -1
- package/dist/primitives/composer/composerInputSelector.d.ts +3 -0
- package/dist/primitives/composer/composerInputSelector.d.ts.map +1 -0
- package/dist/primitives/composer/composerInputSelector.js +7 -0
- package/dist/primitives/composer/composerInputSelector.js.map +1 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.d.ts +2 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.d.ts.map +1 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.js +42 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.js.map +1 -0
- package/dist/primitives/composer.d.ts +35 -14
- package/dist/primitives/composer.d.ts.map +1 -1
- package/dist/primitives/composer.js.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.d.ts +8 -1
- package/dist/primitives/message/MessagePartsGrouped.d.ts.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.js +58 -278
- package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.d.ts.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +5 -2
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
- package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadViewport.js +4 -3
- package/dist/primitives/thread/ThreadViewport.js.map +1 -1
- package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js +2 -2
- package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +90 -21
- 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 +3 -2
- package/dist/sandbox-host/SandboxHost.js.map +1 -1
- package/dist/unstable/useComposerInput.d.ts +9 -4
- package/dist/unstable/useComposerInput.d.ts.map +1 -1
- package/dist/unstable/useComposerInput.js +6 -4
- package/dist/unstable/useComposerInput.js.map +1 -1
- package/dist/unstable/useComposerInputHistory.d.ts +3 -2
- package/dist/unstable/useComposerInputHistory.d.ts.map +1 -1
- package/dist/unstable/useComposerInputHistory.js +2 -2
- package/dist/unstable/useComposerInputHistory.js.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.d.ts +3 -2
- package/dist/unstable/useLiveCompletionAdapter.d.ts.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.js +8 -7
- package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
- package/dist/unstable/useMentionAdapter.d.ts +12 -3
- package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
- package/dist/unstable/useMentionAdapter.js +2 -2
- package/dist/unstable/useMentionAdapter.js.map +1 -1
- package/dist/unstable/useMessageStallDetection.d.ts +4 -2
- package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
- package/dist/unstable/useMessageStallDetection.js +2 -2
- package/dist/unstable/useMessageStallDetection.js.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.d.ts +5 -2
- package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.js +5 -4
- package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts +4 -0
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -1
- package/dist/unstable/webmcp/useWebMcpProvider.js +2 -0
- package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -1
- package/dist/utils/getSelectionMessageId.d.ts.map +1 -1
- package/dist/utils/getSelectionMessageId.js +31 -8
- package/dist/utils/getSelectionMessageId.js.map +1 -1
- package/dist/utils/hooks/useOnResizeContent.d.ts.map +1 -1
- package/dist/utils/hooks/useOnResizeContent.js +2 -22
- package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
- package/dist/utils/useIsomorphicLayoutEffect.d.ts +3 -0
- package/dist/utils/useIsomorphicLayoutEffect.d.ts.map +1 -0
- package/dist/utils/useIsomorphicLayoutEffect.js +6 -0
- package/dist/utils/useIsomorphicLayoutEffect.js.map +1 -0
- package/package.json +10 -10
- package/src/hooks/useToolCallElapsed.test.tsx +4 -2
- package/src/hooks/useToolCallElapsed.ts +3 -2
- package/src/index.ts +20 -2
- package/src/mcp-apps/McpAppRenderer.test.tsx +7 -2
- package/src/mcp-apps/McpAppRenderer.tsx +3 -9
- package/src/mcp-apps/app-frame.test.tsx +102 -6
- package/src/mcp-apps/app-frame.tsx +7 -10
- package/src/mcp-apps/bridge.test.ts +154 -0
- package/src/mcp-apps/bridge.ts +51 -9
- package/src/mcp-apps/csp.test.ts +112 -0
- package/src/mcp-apps/csp.ts +124 -0
- package/src/mcp-apps/index.ts +1 -0
- package/src/mcp-apps/types.ts +16 -1
- package/src/model-context/frame/AssistantFrame.test.ts +45 -0
- package/src/model-context/frame/SPEC_AssistantFrame.md +1 -0
- package/src/primitives/assistantModal/AssistantModalRoot.tsx +1 -0
- package/src/primitives/attachment.ts +4 -1
- package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +32 -1
- package/src/primitives/composer/ComposerAttachmentDropzone.tsx +14 -12
- package/src/primitives/composer/ComposerCancel.focus.test.tsx +307 -0
- package/src/primitives/composer/ComposerCancel.ts +8 -1
- package/src/primitives/composer/ComposerDisabledThread.test.tsx +121 -0
- package/src/primitives/composer/ComposerInput.tsx +19 -1
- package/src/primitives/composer/ComposerRoot.tsx +6 -5
- package/src/primitives/composer/composerInputSelector.ts +4 -0
- package/src/primitives/composer/useComposerCancelWithFocus.ts +57 -0
- package/src/primitives/composer.ts +56 -21
- package/src/primitives/message/MessagePartsGrouped.test.tsx +106 -2
- package/src/primitives/message/MessagePartsGrouped.tsx +36 -134
- package/src/primitives/selectionToolbar/SelectionToolbarRoot.test.tsx +20 -12
- package/src/primitives/selectionToolbar/SelectionToolbarRoot.tsx +6 -2
- package/src/primitives/thread/ThreadViewport.tsx +3 -3
- package/src/primitives/thread/topAnchor/useTopAnchorReserve.ts +2 -2
- package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +666 -63
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +201 -73
- package/src/sandbox-host/SandboxHost.tsx +3 -8
- package/src/unstable/useComposerInput.ts +9 -4
- package/src/unstable/useComposerInputHistory.ts +3 -2
- package/src/unstable/useLiveCompletionAdapter.ts +9 -14
- package/src/unstable/useMentionAdapter.ts +12 -3
- package/src/unstable/useMessageStallDetection.ts +4 -2
- package/src/unstable/useSlashCommandAdapter.ts +8 -4
- package/src/unstable/webmcp/useWebMcpProvider.ts +4 -0
- package/src/utils/getSelectionMessageId.test.ts +231 -0
- package/src/utils/getSelectionMessageId.ts +45 -7
- package/src/utils/hooks/useOnResizeContent.ts +2 -29
- package/src/utils/useIsomorphicLayoutEffect.test.tsx +21 -0
- package/src/utils/useIsomorphicLayoutEffect.ts +9 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { McpAppResourceCSP } from "./types";
|
|
2
|
+
|
|
3
|
+
const isAllowedOrigin = (value: string, allowWebSocket: boolean): boolean => {
|
|
4
|
+
const scheme = allowWebSocket ? /^(?:https?|wss?):\/\//i : /^https?:\/\//i;
|
|
5
|
+
if (!scheme.test(value) || /[\s;'"`,]/.test(value)) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const url = new URL(value.replace("://*.", "://wildcard."));
|
|
11
|
+
return (
|
|
12
|
+
url.username === "" &&
|
|
13
|
+
url.password === "" &&
|
|
14
|
+
url.pathname === "/" &&
|
|
15
|
+
url.search === "" &&
|
|
16
|
+
url.hash === ""
|
|
17
|
+
);
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getAllowedOrigins = (
|
|
24
|
+
values: unknown,
|
|
25
|
+
allowWebSocket = false,
|
|
26
|
+
): string[] =>
|
|
27
|
+
Array.isArray(values)
|
|
28
|
+
? [
|
|
29
|
+
...new Set(
|
|
30
|
+
values.filter(
|
|
31
|
+
(value): value is string =>
|
|
32
|
+
typeof value === "string" &&
|
|
33
|
+
isAllowedOrigin(value, allowWebSocket),
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
]
|
|
37
|
+
: [];
|
|
38
|
+
|
|
39
|
+
const directive = (name: string, values: readonly string[]) =>
|
|
40
|
+
`${name} ${values.join(" ")}`;
|
|
41
|
+
|
|
42
|
+
export const buildMcpAppCsp = (csp?: McpAppResourceCSP): string => {
|
|
43
|
+
const connectDomains = getAllowedOrigins(csp?.connectDomains, true);
|
|
44
|
+
const resourceDomains = getAllowedOrigins(csp?.resourceDomains);
|
|
45
|
+
const frameDomains = getAllowedOrigins(csp?.frameDomains);
|
|
46
|
+
const baseUriDomains = getAllowedOrigins(csp?.baseUriDomains);
|
|
47
|
+
|
|
48
|
+
return [
|
|
49
|
+
"default-src 'none'",
|
|
50
|
+
directive("script-src", ["'self'", "'unsafe-inline'", ...resourceDomains]),
|
|
51
|
+
directive("style-src", ["'self'", "'unsafe-inline'", ...resourceDomains]),
|
|
52
|
+
directive(
|
|
53
|
+
"connect-src",
|
|
54
|
+
connectDomains.length > 0 ? ["'self'", ...connectDomains] : ["'none'"],
|
|
55
|
+
),
|
|
56
|
+
directive("img-src", ["'self'", "data:", ...resourceDomains]),
|
|
57
|
+
directive("font-src", ["'self'", ...resourceDomains]),
|
|
58
|
+
directive("media-src", ["'self'", "data:", ...resourceDomains]),
|
|
59
|
+
directive("frame-src", frameDomains.length > 0 ? frameDomains : ["'none'"]),
|
|
60
|
+
directive(
|
|
61
|
+
"base-uri",
|
|
62
|
+
baseUriDomains.length > 0 ? baseUriDomains : ["'self'"],
|
|
63
|
+
),
|
|
64
|
+
"object-src 'none'",
|
|
65
|
+
"form-action 'none'",
|
|
66
|
+
].join("; ");
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const findDoctypeEnd = (html: string): number => {
|
|
70
|
+
let offset = 0;
|
|
71
|
+
|
|
72
|
+
while (offset < html.length) {
|
|
73
|
+
const whitespace = html.slice(offset).match(/^[\t\n\f\r ]+/)?.[0];
|
|
74
|
+
if (whitespace) {
|
|
75
|
+
offset += whitespace.length;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (html.startsWith("<!--", offset)) {
|
|
79
|
+
if (html.startsWith("<!-->", offset)) {
|
|
80
|
+
offset += 5;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (html.startsWith("<!--->", offset)) {
|
|
84
|
+
offset += 6;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const commentEnd = html.indexOf("-->", offset + 4);
|
|
88
|
+
const bangCommentEnd = html.indexOf("--!>", offset + 4);
|
|
89
|
+
if (commentEnd === -1 && bangCommentEnd === -1) return 0;
|
|
90
|
+
offset =
|
|
91
|
+
bangCommentEnd !== -1 &&
|
|
92
|
+
(commentEnd === -1 || bangCommentEnd < commentEnd)
|
|
93
|
+
? bangCommentEnd + 4
|
|
94
|
+
: commentEnd + 3;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (html.slice(offset, offset + 9).toLowerCase() === "<!doctype") {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
if (html.startsWith("<?", offset) || html.startsWith("<!", offset)) {
|
|
101
|
+
const bogusCommentEnd = html.indexOf(">", offset + 2);
|
|
102
|
+
if (bogusCommentEnd === -1) return 0;
|
|
103
|
+
offset = bogusCommentEnd + 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (html.slice(offset, offset + 9).toLowerCase() !== "<!doctype") {
|
|
110
|
+
return offset;
|
|
111
|
+
}
|
|
112
|
+
const end = html.indexOf(">", offset + 9);
|
|
113
|
+
return end === -1 ? 0 : end + 1;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const applyMcpAppCsp = (
|
|
117
|
+
html: string,
|
|
118
|
+
csp?: McpAppResourceCSP,
|
|
119
|
+
): string => {
|
|
120
|
+
const normalizedHtml = html.charCodeAt(0) === 0xfeff ? html.slice(1) : html;
|
|
121
|
+
const meta = `<meta http-equiv="Content-Security-Policy" content="${buildMcpAppCsp(csp)}">`;
|
|
122
|
+
const insertionPoint = findDoctypeEnd(normalizedHtml);
|
|
123
|
+
return `${normalizedHtml.slice(0, insertionPoint)}${meta}${normalizedHtml.slice(insertionPoint)}`;
|
|
124
|
+
};
|
package/src/mcp-apps/index.ts
CHANGED
package/src/mcp-apps/types.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type McpAppResourceCSP = {
|
|
|
14
14
|
connectDomains?: string[];
|
|
15
15
|
resourceDomains?: string[];
|
|
16
16
|
frameDomains?: string[];
|
|
17
|
+
baseUriDomains?: string[];
|
|
17
18
|
[k: string]: unknown;
|
|
18
19
|
};
|
|
19
20
|
|
|
@@ -86,6 +87,20 @@ export type McpAppToolCallParams = {
|
|
|
86
87
|
arguments?: Record<string, unknown>;
|
|
87
88
|
};
|
|
88
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The params of the app's `ui/initialize` request, as `onInitialized`
|
|
92
|
+
* receives them. A field that fails validation is omitted, and
|
|
93
|
+
* `availableDisplayModes` keeps only the modes the bridge recognizes.
|
|
94
|
+
*/
|
|
95
|
+
export type McpAppInitializeParams = {
|
|
96
|
+
appInfo?: { name: string; version: string; [k: string]: unknown };
|
|
97
|
+
appCapabilities?: {
|
|
98
|
+
availableDisplayModes?: McpAppDisplayMode[];
|
|
99
|
+
[k: string]: unknown;
|
|
100
|
+
};
|
|
101
|
+
protocolVersion?: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
89
104
|
export type McpAppBridgeHandlers = {
|
|
90
105
|
allowedTools?: readonly string[];
|
|
91
106
|
callTool?: (params: McpAppToolCallParams) => Promise<unknown> | unknown;
|
|
@@ -98,7 +113,7 @@ export type McpAppBridgeHandlers = {
|
|
|
98
113
|
mode: McpAppDisplayMode;
|
|
99
114
|
}) => Promise<{ mode: McpAppDisplayMode }> | { mode: McpAppDisplayMode };
|
|
100
115
|
onSizeChange?: (params: { width?: number; height?: number }) => void;
|
|
101
|
-
onInitialized?: () => void;
|
|
116
|
+
onInitialized?: (params: McpAppInitializeParams) => void;
|
|
102
117
|
onRequestTeardown?: (params: unknown) => void;
|
|
103
118
|
onLog?: (params: unknown) => void;
|
|
104
119
|
onError?: (error: Error) => void;
|
|
@@ -237,6 +237,51 @@ describe("AssistantFrame Integration", () => {
|
|
|
237
237
|
unsubscribe();
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
+
it("rejects a tool call the frame drops while it is disposed", async () => {
|
|
241
|
+
vi.mocked(iframeWindow.postMessage).mockImplementation((data: any) => {
|
|
242
|
+
Promise.resolve().then(() => {
|
|
243
|
+
messageHandlers.get("iframe")?.({
|
|
244
|
+
data,
|
|
245
|
+
source: parentWindow,
|
|
246
|
+
origin: window.location.origin,
|
|
247
|
+
} as MessageEvent);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
vi.mocked(window.removeEventListener).mockImplementation(
|
|
251
|
+
(event: string, handler: any) => {
|
|
252
|
+
if (event === "message" && messageHandlers.get("iframe") === handler) {
|
|
253
|
+
messageHandlers.delete("iframe");
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
const registry = new ModelContextRegistry();
|
|
258
|
+
const toolExecute = vi.fn().mockResolvedValue("result");
|
|
259
|
+
registry.addTool({
|
|
260
|
+
toolName: "search",
|
|
261
|
+
parameters: z.object({}),
|
|
262
|
+
execute: toolExecute,
|
|
263
|
+
});
|
|
264
|
+
AssistantFrameProvider.addModelContextProvider(registry);
|
|
265
|
+
const host = new AssistantFrameHost(iframeWindow);
|
|
266
|
+
await vi.waitFor(() => {
|
|
267
|
+
expect(host.getModelContext().tools?.search).toBeDefined();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const result = host.getModelContext().tools!.search!.execute!(
|
|
271
|
+
{},
|
|
272
|
+
{} as any,
|
|
273
|
+
);
|
|
274
|
+
AssistantFrameProvider.dispose();
|
|
275
|
+
|
|
276
|
+
await expect(result).rejects.toThrow(
|
|
277
|
+
"AssistantFrameProvider has been disposed",
|
|
278
|
+
);
|
|
279
|
+
expect(toolExecute).not.toHaveBeenCalled();
|
|
280
|
+
expect(host.getModelContext().tools).toBeUndefined();
|
|
281
|
+
|
|
282
|
+
host.dispose();
|
|
283
|
+
});
|
|
284
|
+
|
|
240
285
|
it("should handle multiple providers", async () => {
|
|
241
286
|
// Setup multiple providers
|
|
242
287
|
const registry1 = new ModelContextRegistry();
|
|
@@ -78,6 +78,7 @@ All messages are wrapped with a channel identifier to avoid conflicts with other
|
|
|
78
78
|
2. **Tool Execution**
|
|
79
79
|
- `tool-call`: Parent requests tool execution in iframe (where tools are defined)
|
|
80
80
|
- `tool-result`: Iframe returns execution result or error to parent
|
|
81
|
+
- `provider-disposed`: Iframe tells the parent it was disposed, so the parent rejects tool calls the iframe will not answer
|
|
81
82
|
|
|
82
83
|
#### Serialization
|
|
83
84
|
|
|
@@ -7,6 +7,7 @@ import { useAuiEvent } from "@assistant-ui/store";
|
|
|
7
7
|
|
|
8
8
|
export namespace AssistantModalPrimitiveRoot {
|
|
9
9
|
export type Props = PopoverPrimitive.PopoverProps & {
|
|
10
|
+
/** @deprecated Experimental since 2024-10-12. Not scheduled for removal; the API may change in any release. */
|
|
10
11
|
unstable_openOnRunStart?: boolean | undefined;
|
|
11
12
|
};
|
|
12
13
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export { AttachmentPrimitiveRoot as Root } from "./attachment/AttachmentRoot";
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
/** @deprecated Experimental since 2024-10-10. Not scheduled for removal; the API may change in any release. */
|
|
4
|
+
AttachmentPrimitiveThumb as unstable_Thumb,
|
|
5
|
+
} from "./attachment/AttachmentThumb";
|
|
3
6
|
export { AttachmentPrimitiveName as Name } from "./attachment/AttachmentName";
|
|
4
7
|
export { AttachmentPrimitiveRemove as Remove } from "./attachment/AttachmentRemove";
|
|
@@ -6,9 +6,10 @@ import { createRoot, type Root } from "react-dom/client";
|
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
7
|
import { ComposerPrimitiveAttachmentDropzone } from "./ComposerAttachmentDropzone";
|
|
8
8
|
|
|
9
|
-
const { addAttachment, threadCapabilities } = vi.hoisted(() => ({
|
|
9
|
+
const { addAttachment, threadCapabilities, threadState } = vi.hoisted(() => ({
|
|
10
10
|
addAttachment: vi.fn<(file: File) => Promise<void>>(),
|
|
11
11
|
threadCapabilities: { attachments: true },
|
|
12
|
+
threadState: { isDisabled: false },
|
|
12
13
|
}));
|
|
13
14
|
|
|
14
15
|
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
@@ -25,6 +26,9 @@ vi.mock("@assistant-ui/store", async (importOriginal) => {
|
|
|
25
26
|
getState: () => ({ capabilities: threadCapabilities }),
|
|
26
27
|
},
|
|
27
28
|
}),
|
|
29
|
+
useAuiState: (
|
|
30
|
+
selector: (state: { thread: typeof threadState }) => unknown,
|
|
31
|
+
) => selector({ thread: threadState }),
|
|
28
32
|
};
|
|
29
33
|
});
|
|
30
34
|
|
|
@@ -53,6 +57,7 @@ describe("ComposerPrimitiveAttachmentDropzone", () => {
|
|
|
53
57
|
beforeEach(async () => {
|
|
54
58
|
addAttachment.mockReset();
|
|
55
59
|
threadCapabilities.attachments = true;
|
|
60
|
+
threadState.isDisabled = false;
|
|
56
61
|
container = document.createElement("div");
|
|
57
62
|
document.body.appendChild(container);
|
|
58
63
|
root = createRoot(container);
|
|
@@ -250,6 +255,32 @@ describe("ComposerPrimitiveAttachmentDropzone", () => {
|
|
|
250
255
|
expect(dropzone.hasAttribute("data-dragging")).toBe(false);
|
|
251
256
|
});
|
|
252
257
|
|
|
258
|
+
it("blocks file drops when the thread is disabled", async () => {
|
|
259
|
+
threadState.isDisabled = true;
|
|
260
|
+
await act(async () => {
|
|
261
|
+
root.render(
|
|
262
|
+
<ComposerPrimitiveAttachmentDropzone data-testid="dropzone">
|
|
263
|
+
<div>dropzone</div>
|
|
264
|
+
</ComposerPrimitiveAttachmentDropzone>,
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
const dropzone = container.querySelector("[data-testid='dropzone']")!;
|
|
268
|
+
const over = createDragEvent("dragover", ["Files"]);
|
|
269
|
+
const drop = createDropEvent([
|
|
270
|
+
new File(["file"], "photo.png", { type: "image/png" }),
|
|
271
|
+
]);
|
|
272
|
+
|
|
273
|
+
await act(async () => {
|
|
274
|
+
dropzone.dispatchEvent(over);
|
|
275
|
+
dropzone.dispatchEvent(drop);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(over.defaultPrevented).toBe(true);
|
|
279
|
+
expect(drop.defaultPrevented).toBe(true);
|
|
280
|
+
expect(addAttachment).not.toHaveBeenCalled();
|
|
281
|
+
expect(dropzone.hasAttribute("data-dragging")).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
|
|
253
284
|
it("ignores non-file drags", async () => {
|
|
254
285
|
const dropzone = container.querySelector("[data-testid='dropzone']");
|
|
255
286
|
expect(dropzone).not.toBeNull();
|
|
@@ -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,
|