@agent-native/toolkit 0.198.3 → 0.198.5
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/app/agentkit/react/components.d.ts +8 -3
- package/dist/app/agentkit/react/components.d.ts.map +1 -1
- package/dist/app/agentkit/react/components.js +24 -10
- package/dist/app/agentkit/react/components.js.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +65 -33
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/chat/AgentSidebar.d.ts.map +1 -1
- package/dist/app/chat/AgentSidebar.js +2 -0
- package/dist/app/chat/AgentSidebar.js.map +1 -1
- package/dist/app/chat/FileStorageSetupPopover.js +2 -2
- package/dist/app/chat/FileStorageSetupPopover.js.map +1 -1
- package/dist/app/chat/MultiTabAssistantChat.js +1 -1
- package/dist/app/chat/agentkit-chat/connections.d.ts +1 -4
- package/dist/app/chat/agentkit-chat/connections.d.ts.map +1 -1
- package/dist/app/chat/agentkit-chat/connections.js +1 -26
- package/dist/app/chat/agentkit-chat/connections.js.map +1 -1
- package/dist/app/chat/agentkit-chat/history.d.ts +4 -1
- package/dist/app/chat/agentkit-chat/history.d.ts.map +1 -1
- package/dist/app/chat/agentkit-chat/history.js +17 -5
- package/dist/app/chat/agentkit-chat/history.js.map +1 -1
- package/dist/app/chat/chat/history-types.d.ts +1 -1
- package/dist/app/chat/chat/history-types.d.ts.map +1 -1
- package/dist/app/chat/chat-first/copy.d.ts +1 -2
- package/dist/app/chat/chat-first/copy.d.ts.map +1 -1
- package/dist/app/chat/chat-first/copy.js +1 -121
- package/dist/app/chat/chat-first/copy.js.map +1 -1
- package/dist/app/chat/chat-first-copy.d.ts +3 -0
- package/dist/app/chat/chat-first-copy.d.ts.map +1 -0
- package/dist/app/chat/chat-first-copy.js +122 -0
- package/dist/app/chat/chat-first-copy.js.map +1 -0
- package/dist/app/feature-flags/FeatureFlagsPanel.d.ts.map +1 -1
- package/dist/app/feature-flags/FeatureFlagsPanel.js +1 -0
- package/dist/app/feature-flags/FeatureFlagsPanel.js.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +2 -1
- package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.js +2 -1
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +2 -1
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +2 -1
- package/dist/app/i18n/catalogs/en-US.js.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.js +2 -1
- package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.js +2 -1
- package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.js +2 -1
- package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.js +2 -1
- package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.js +2 -1
- package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.js +2 -1
- package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.js +2 -1
- package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.js +2 -1
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/app/labs/LabsSettings.d.ts.map +1 -1
- package/dist/app/labs/LabsSettings.js +7 -3
- package/dist/app/labs/LabsSettings.js.map +1 -1
- package/dist/app/org/OrgGeneralSection.d.ts.map +1 -1
- package/dist/app/org/OrgGeneralSection.js +21 -18
- package/dist/app/org/OrgGeneralSection.js.map +1 -1
- package/dist/app/org/OrgSwitcher.d.ts.map +1 -1
- package/dist/app/org/OrgSwitcher.js +3 -2
- package/dist/app/org/OrgSwitcher.js.map +1 -1
- package/dist/app/org/workspace-app-links.d.ts +1 -0
- package/dist/app/org/workspace-app-links.d.ts.map +1 -1
- package/dist/app/org/workspace-app-links.js +3 -0
- package/dist/app/org/workspace-app-links.js.map +1 -1
- package/dist/app/resources/McpAgentKitConnectionRequest.d.ts +7 -2
- package/dist/app/resources/McpAgentKitConnectionRequest.d.ts.map +1 -1
- package/dist/app/resources/McpAgentKitConnectionRequest.js +132 -13
- package/dist/app/resources/McpAgentKitConnectionRequest.js.map +1 -1
- package/dist/composer/PromptComposer.d.ts +3 -0
- package/dist/composer/PromptComposer.d.ts.map +1 -1
- package/dist/composer/PromptComposer.js +2 -2
- package/dist/composer/PromptComposer.js.map +1 -1
- package/dist/composer/TiptapComposer.d.ts +4 -1
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +8 -4
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/dist/icons/ResourceIconPicker.d.ts +6 -1
- package/dist/icons/ResourceIconPicker.d.ts.map +1 -1
- package/dist/icons/ResourceIconPicker.js +45 -6
- package/dist/icons/ResourceIconPicker.js.map +1 -1
- package/package.json +6 -4
- package/src/app/agentkit/react/components.interaction.spec.tsx +93 -0
- package/src/app/agentkit/react/components.spec.tsx +4 -5
- package/src/app/agentkit/react/components.tsx +51 -8
- package/src/app/agentkit/react/composer-context.spec.tsx +7 -0
- package/src/app/agentkit/react/lifecycle.spec.tsx +42 -0
- package/src/app/chat/AgentKitAssistantChat.spec.tsx +151 -11
- package/src/app/chat/AgentKitAssistantChat.tsx +110 -28
- package/src/app/chat/AgentPanel.header.spec.ts +59 -22
- package/src/app/chat/AgentSidebar.tsx +2 -0
- package/src/app/chat/FileStorageSetupPopover.tsx +2 -2
- package/src/app/chat/MultiTabAssistantChat.messageCount.spec.tsx +3 -1
- package/src/app/chat/MultiTabAssistantChat.spec.tsx +8 -4
- package/src/app/chat/MultiTabAssistantChat.tsx +1 -1
- package/src/app/chat/agentkit-chat/action-widget.spec.tsx +5 -17
- package/src/app/chat/agentkit-chat/connections.spec.tsx +137 -0
- package/src/app/chat/agentkit-chat/connections.ts +2 -37
- package/src/app/chat/agentkit-chat/history.tsx +23 -8
- package/src/app/chat/chat/history-types.ts +1 -1
- package/src/app/chat/chat/tool-call-display.spec.tsx +2 -2
- package/src/app/chat/chat/widgets/record-change-widget.spec.tsx +20 -29
- package/src/app/chat/chat-first/copy.ts +1 -133
- package/src/app/chat/chat-first-copy.ts +133 -0
- package/src/app/chat/composer/use-agentkit-capabilities.hardening.spec.tsx +3 -2
- package/src/app/feature-flags/FeatureFlagsPanel.tsx +1 -0
- package/src/app/i18n/catalogs/ar-SA.ts +2 -1
- package/src/app/i18n/catalogs/de-DE.ts +3 -1
- package/src/app/i18n/catalogs/en-US.ts +2 -1
- package/src/app/i18n/catalogs/es-ES.ts +2 -1
- package/src/app/i18n/catalogs/fr-FR.ts +3 -1
- package/src/app/i18n/catalogs/hi-IN.ts +2 -1
- package/src/app/i18n/catalogs/ja-JP.ts +2 -1
- package/src/app/i18n/catalogs/ko-KR.ts +2 -1
- package/src/app/i18n/catalogs/pt-BR.ts +3 -1
- package/src/app/i18n/catalogs/zh-CN.ts +2 -1
- package/src/app/i18n/catalogs/zh-TW.ts +2 -1
- package/src/app/integrations/IntegrationDetailPage.spec.tsx +1 -1
- package/src/app/labs/LabsSettings.tsx +46 -10
- package/src/app/observability/ThumbsFeedback.spec.tsx +15 -0
- package/src/app/onboarding/FirstRunOnboarding.spec.tsx +4 -0
- package/src/app/org/OrgGeneralSection.tsx +32 -22
- package/src/app/org/OrgSwitcher.tsx +3 -2
- package/src/app/org/workspace-app-links.spec.ts +4 -0
- package/src/app/org/workspace-app-links.ts +8 -1
- package/src/app/resources/McpAgentKitConnectionRequest.spec.tsx +371 -2
- package/src/app/resources/McpAgentKitConnectionRequest.tsx +171 -11
- package/src/app/settings/SettingsTabsPage.spec.tsx +4 -2
- package/src/app/settings/shell/SettingsShell.spec.tsx +14 -8
- package/src/app/settings/shell/pages/app-group-pages.spec.tsx +35 -5
- package/src/app/settings/shell/pages/resource-pages.spec.tsx +3 -0
- package/src/app/settings/usage/UsagePage.spec.tsx +3 -3
- package/src/app/shared/lazy-chunk-error-boundary.spec.tsx +2 -0
- package/src/app-shell/sidebar-overflow.spec.tsx +4 -1
- package/src/composer/PromptComposer.tsx +7 -0
- package/src/composer/TiptapComposer.spec.ts +26 -4
- package/src/composer/TiptapComposer.tsx +12 -3
- package/src/design-tweaks/visual-style-controls.spec.tsx +38 -0
- package/src/editor/useCollabReconcile.concurrent.spec.ts +2 -1
- package/src/icons/ResourceIconPicker.spec.tsx +270 -0
- package/src/icons/ResourceIconPicker.tsx +72 -4
|
@@ -1,15 +1,79 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { openOAuthPopup } from "@agent-native/core/client/oauth-popup";
|
|
4
|
+
import {
|
|
5
|
+
clearMcpConnectionResume,
|
|
6
|
+
consumeMcpConnectionResume,
|
|
7
|
+
notifyMcpConnectionComplete,
|
|
8
|
+
saveMcpConnectionResume,
|
|
9
|
+
} from "@agent-native/core/client/resources/mcp-connection-resume";
|
|
4
10
|
import React, { act } from "react";
|
|
5
11
|
import { createRoot } from "react-dom/client";
|
|
6
|
-
import { describe, expect, it, vi } from "vitest";
|
|
12
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
13
|
|
|
14
|
+
import { dispatchIntegrationsHref } from "../org/workspace-app-links.js";
|
|
8
15
|
import {
|
|
9
16
|
McpAgentKitConnectionRequestCard,
|
|
10
17
|
McpAgentKitConnectionResume,
|
|
11
18
|
} from "./McpAgentKitConnectionRequest.js";
|
|
12
19
|
|
|
20
|
+
const popupState = vi.hoisted(() => ({ popup: null as unknown }));
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
clearMcpConnectionResume();
|
|
24
|
+
window.localStorage.clear();
|
|
25
|
+
vi.unstubAllEnvs();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
vi.mock("@agent-native/core/client/oauth-popup", () => ({
|
|
29
|
+
openOAuthPopup: vi.fn(() => popupState.popup as Window | null),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock("../org/workspace-app-links.js", async (importOriginal) => {
|
|
33
|
+
const actual =
|
|
34
|
+
await importOriginal<typeof import("../org/workspace-app-links.js")>();
|
|
35
|
+
return {
|
|
36
|
+
...actual,
|
|
37
|
+
useOrgSwitcherAppLinks: () => ({
|
|
38
|
+
apps: [],
|
|
39
|
+
isWorkspace: false,
|
|
40
|
+
isLoading: false,
|
|
41
|
+
dispatchHref: "",
|
|
42
|
+
dispatchAllAppsHref: "",
|
|
43
|
+
dispatchVaultHref: "",
|
|
44
|
+
dispatchResourcesHref: "",
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
vi.mock("../agentkit/react/components.js", async () => {
|
|
50
|
+
const { createElement } = await import("react");
|
|
51
|
+
return {
|
|
52
|
+
AgentConnectionRequestCard: ({
|
|
53
|
+
request,
|
|
54
|
+
retry,
|
|
55
|
+
onConnect,
|
|
56
|
+
}: {
|
|
57
|
+
request: { provider: string; status: string };
|
|
58
|
+
retry?: boolean;
|
|
59
|
+
onConnect?: () => void | boolean | Promise<void | boolean>;
|
|
60
|
+
}) => {
|
|
61
|
+
if (request.status === "connected" || request.status === "declined") {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return createElement(
|
|
65
|
+
"button",
|
|
66
|
+
{
|
|
67
|
+
type: "button",
|
|
68
|
+
"data-status": request.status,
|
|
69
|
+
onClick: () => void onConnect?.(),
|
|
70
|
+
},
|
|
71
|
+
`${retry ? "Try again" : "Connect"} ${request.provider}`,
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
13
77
|
describe("McpAgentKitConnectionRequestCard", () => {
|
|
14
78
|
it("keeps an unknown provider request visible without trusting its setup data", () => {
|
|
15
79
|
const container = document.createElement("div");
|
|
@@ -37,6 +101,311 @@ describe("McpAgentKitConnectionRequestCard", () => {
|
|
|
37
101
|
act(() => root.unmount());
|
|
38
102
|
});
|
|
39
103
|
|
|
104
|
+
it("keeps the request alive in a popup and resumes it on OAuth completion", async () => {
|
|
105
|
+
window.sessionStorage.clear();
|
|
106
|
+
vi.stubEnv("VITE_APP_BASE_PATH", "/dispatch");
|
|
107
|
+
window.history.replaceState(
|
|
108
|
+
{},
|
|
109
|
+
"",
|
|
110
|
+
"/dispatch/chat/thread-1?tab=docs#selected",
|
|
111
|
+
);
|
|
112
|
+
vi.mocked(openOAuthPopup).mockClear();
|
|
113
|
+
const locationAssign = vi.fn();
|
|
114
|
+
const popup = {
|
|
115
|
+
closed: false,
|
|
116
|
+
location: { assign: locationAssign },
|
|
117
|
+
close: vi.fn(),
|
|
118
|
+
};
|
|
119
|
+
popupState.popup = popup;
|
|
120
|
+
const onConnected = vi.fn();
|
|
121
|
+
const target = {
|
|
122
|
+
threadId: "thread-1",
|
|
123
|
+
runId: "run-1",
|
|
124
|
+
requestId: "request-1",
|
|
125
|
+
};
|
|
126
|
+
const container = document.createElement("div");
|
|
127
|
+
const root = createRoot(container);
|
|
128
|
+
|
|
129
|
+
await act(async () => {
|
|
130
|
+
root.render(
|
|
131
|
+
<>
|
|
132
|
+
<McpAgentKitConnectionRequestCard
|
|
133
|
+
provider="google_drive"
|
|
134
|
+
reason="connect"
|
|
135
|
+
appId="dispatch"
|
|
136
|
+
source={{
|
|
137
|
+
id: "google_drive",
|
|
138
|
+
kind: "workspace_connection",
|
|
139
|
+
label: "Google Drive",
|
|
140
|
+
}}
|
|
141
|
+
target={target}
|
|
142
|
+
onConnected={onConnected}
|
|
143
|
+
onDeclined={() => undefined}
|
|
144
|
+
/>
|
|
145
|
+
<McpAgentKitConnectionResume
|
|
146
|
+
onResume={() => onConnected()}
|
|
147
|
+
onMessageResume={() => undefined}
|
|
148
|
+
/>
|
|
149
|
+
</>,
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
const button = container.querySelector("button");
|
|
153
|
+
await act(async () => button?.click());
|
|
154
|
+
|
|
155
|
+
expect(openOAuthPopup).toHaveBeenCalledWith({
|
|
156
|
+
features: "width=640,height=760",
|
|
157
|
+
});
|
|
158
|
+
const oauthUrl = new URL(
|
|
159
|
+
locationAssign.mock.calls[0]![0],
|
|
160
|
+
window.location.origin,
|
|
161
|
+
);
|
|
162
|
+
expect(oauthUrl.pathname).toContain(
|
|
163
|
+
"/connections/oauth/google_drive/start",
|
|
164
|
+
);
|
|
165
|
+
expect(oauthUrl.searchParams.get("appId")).toBe("dispatch");
|
|
166
|
+
expect(oauthUrl.searchParams.get("scope")).toBe("user");
|
|
167
|
+
const returnPath = oauthUrl.searchParams.get("return");
|
|
168
|
+
expect(returnPath).toContain(
|
|
169
|
+
"/_agent-native/oauth/popup?complete=workspace-connection&resume=",
|
|
170
|
+
);
|
|
171
|
+
expect(returnPath).not.toContain("#selected");
|
|
172
|
+
expect(consumeMcpConnectionResume()).toBeNull();
|
|
173
|
+
const completionId = new URL(
|
|
174
|
+
returnPath!,
|
|
175
|
+
window.location.origin,
|
|
176
|
+
).searchParams.get("resume");
|
|
177
|
+
expect(completionId).toMatch(
|
|
178
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
await act(async () => {
|
|
182
|
+
window.dispatchEvent(
|
|
183
|
+
new MessageEvent("message", {
|
|
184
|
+
data: { type: "agent-native:workspace-connection-complete" },
|
|
185
|
+
origin: "https://untrusted.example",
|
|
186
|
+
source: popup as unknown as MessageEventSource,
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
window.dispatchEvent(
|
|
190
|
+
new MessageEvent("message", {
|
|
191
|
+
data: { type: "agent-native:workspace-connection-complete" },
|
|
192
|
+
origin: window.location.origin,
|
|
193
|
+
source: null,
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
await Promise.resolve();
|
|
197
|
+
});
|
|
198
|
+
expect(onConnected).not.toHaveBeenCalled();
|
|
199
|
+
|
|
200
|
+
act(() => root.unmount());
|
|
201
|
+
expect(popup.close).not.toHaveBeenCalled();
|
|
202
|
+
const resumedRoot = createRoot(container);
|
|
203
|
+
await act(async () => {
|
|
204
|
+
notifyMcpConnectionComplete(completionId!);
|
|
205
|
+
resumedRoot.render(
|
|
206
|
+
<McpAgentKitConnectionResume
|
|
207
|
+
onResume={() => onConnected()}
|
|
208
|
+
onMessageResume={() => undefined}
|
|
209
|
+
/>,
|
|
210
|
+
);
|
|
211
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
212
|
+
});
|
|
213
|
+
expect(onConnected).toHaveBeenCalledOnce();
|
|
214
|
+
act(() => resumedRoot.unmount());
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("keeps a granted connection request pending until the user retries", async () => {
|
|
218
|
+
const container = document.createElement("div");
|
|
219
|
+
const root = createRoot(container);
|
|
220
|
+
vi.mocked(openOAuthPopup).mockClear();
|
|
221
|
+
const locationAssign = vi.fn();
|
|
222
|
+
const popup = {
|
|
223
|
+
opener: window,
|
|
224
|
+
location: { assign: locationAssign },
|
|
225
|
+
close: vi.fn(),
|
|
226
|
+
} as unknown as Window;
|
|
227
|
+
const open = vi
|
|
228
|
+
.spyOn(window, "open")
|
|
229
|
+
.mockReturnValueOnce(null)
|
|
230
|
+
.mockReturnValue(popup);
|
|
231
|
+
const onConnected = vi.fn();
|
|
232
|
+
|
|
233
|
+
act(() => {
|
|
234
|
+
root.render(
|
|
235
|
+
<McpAgentKitConnectionRequestCard
|
|
236
|
+
provider="google_drive"
|
|
237
|
+
reason="grant"
|
|
238
|
+
appId="dispatch"
|
|
239
|
+
source={{
|
|
240
|
+
id: "google_drive",
|
|
241
|
+
kind: "workspace_connection",
|
|
242
|
+
label: "Google Drive",
|
|
243
|
+
}}
|
|
244
|
+
target={{
|
|
245
|
+
threadId: "thread-1",
|
|
246
|
+
runId: "run-1",
|
|
247
|
+
requestId: "request-1",
|
|
248
|
+
}}
|
|
249
|
+
onConnected={onConnected}
|
|
250
|
+
onDeclined={() => undefined}
|
|
251
|
+
/>,
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
expect(container.textContent).toContain("Connect google_drive");
|
|
256
|
+
expect(openOAuthPopup).not.toHaveBeenCalled();
|
|
257
|
+
await act(async () => container.querySelector("button")?.click());
|
|
258
|
+
expect(open).toHaveBeenCalledWith("", "_blank");
|
|
259
|
+
expect(onConnected).not.toHaveBeenCalled();
|
|
260
|
+
expect(container.textContent).toContain("Connect google_drive");
|
|
261
|
+
|
|
262
|
+
await act(async () => container.querySelector("button")?.click());
|
|
263
|
+
expect(open).toHaveBeenCalledTimes(2);
|
|
264
|
+
expect(popup.opener).toBeNull();
|
|
265
|
+
expect(locationAssign).toHaveBeenCalledWith(dispatchIntegrationsHref([]));
|
|
266
|
+
expect(container.textContent).toContain("Try again google_drive");
|
|
267
|
+
expect(onConnected).not.toHaveBeenCalled();
|
|
268
|
+
await act(async () => container.querySelector("button")?.click());
|
|
269
|
+
expect(onConnected).toHaveBeenCalledOnce();
|
|
270
|
+
|
|
271
|
+
await act(async () => {
|
|
272
|
+
root.render(
|
|
273
|
+
<McpAgentKitConnectionRequestCard
|
|
274
|
+
provider="google_drive"
|
|
275
|
+
reason="grant"
|
|
276
|
+
status="connected"
|
|
277
|
+
appId="dispatch"
|
|
278
|
+
source={{
|
|
279
|
+
id: "google_drive",
|
|
280
|
+
kind: "workspace_connection",
|
|
281
|
+
label: "Google Drive",
|
|
282
|
+
}}
|
|
283
|
+
target={{
|
|
284
|
+
threadId: "thread-1",
|
|
285
|
+
runId: "run-1",
|
|
286
|
+
requestId: "request-1",
|
|
287
|
+
}}
|
|
288
|
+
onConnected={onConnected}
|
|
289
|
+
onDeclined={() => undefined}
|
|
290
|
+
/>,
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
expect(container.querySelector("button")).toBeNull();
|
|
294
|
+
|
|
295
|
+
await act(async () => {
|
|
296
|
+
root.render(
|
|
297
|
+
<McpAgentKitConnectionRequestCard
|
|
298
|
+
provider="google_drive"
|
|
299
|
+
reason="grant"
|
|
300
|
+
status="failed"
|
|
301
|
+
appId="dispatch"
|
|
302
|
+
source={{
|
|
303
|
+
id: "google_drive",
|
|
304
|
+
kind: "workspace_connection",
|
|
305
|
+
label: "Google Drive",
|
|
306
|
+
}}
|
|
307
|
+
target={{
|
|
308
|
+
threadId: "thread-1",
|
|
309
|
+
runId: "run-1",
|
|
310
|
+
requestId: "request-1",
|
|
311
|
+
}}
|
|
312
|
+
onConnected={onConnected}
|
|
313
|
+
onDeclined={() => undefined}
|
|
314
|
+
/>,
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
expect(container.querySelector("button")?.getAttribute("data-status")).toBe(
|
|
318
|
+
"failed",
|
|
319
|
+
);
|
|
320
|
+
await act(async () => container.querySelector("button")?.click());
|
|
321
|
+
expect(open).toHaveBeenCalledTimes(3);
|
|
322
|
+
expect(onConnected).toHaveBeenCalledOnce();
|
|
323
|
+
await act(async () => container.querySelector("button")?.click());
|
|
324
|
+
expect(onConnected).toHaveBeenCalledTimes(2);
|
|
325
|
+
act(() => root.unmount());
|
|
326
|
+
open.mockRestore();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("keeps non-OAuth workspace requests pending until the user retries", async () => {
|
|
330
|
+
const container = document.createElement("div");
|
|
331
|
+
const root = createRoot(container);
|
|
332
|
+
vi.mocked(openOAuthPopup).mockClear();
|
|
333
|
+
const locationAssign = vi.fn();
|
|
334
|
+
const popup = {
|
|
335
|
+
opener: window,
|
|
336
|
+
location: { assign: locationAssign },
|
|
337
|
+
close: vi.fn(),
|
|
338
|
+
} as unknown as Window;
|
|
339
|
+
const open = vi.spyOn(window, "open").mockReturnValue(popup);
|
|
340
|
+
const onConnected = vi.fn();
|
|
341
|
+
|
|
342
|
+
act(() => {
|
|
343
|
+
root.render(
|
|
344
|
+
<McpAgentKitConnectionRequestCard
|
|
345
|
+
provider="slack"
|
|
346
|
+
appId="dispatch"
|
|
347
|
+
source={{
|
|
348
|
+
id: "slack",
|
|
349
|
+
kind: "workspace_connection",
|
|
350
|
+
label: "Slack",
|
|
351
|
+
}}
|
|
352
|
+
target={{
|
|
353
|
+
threadId: "thread-1",
|
|
354
|
+
runId: "run-1",
|
|
355
|
+
requestId: "request-1",
|
|
356
|
+
}}
|
|
357
|
+
onConnected={onConnected}
|
|
358
|
+
onDeclined={() => undefined}
|
|
359
|
+
fallback={<div data-unsupported-provider="">Setup unavailable</div>}
|
|
360
|
+
/>,
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
expect(container.textContent).toContain("Connect slack");
|
|
365
|
+
expect(openOAuthPopup).not.toHaveBeenCalled();
|
|
366
|
+
await act(async () => container.querySelector("button")?.click());
|
|
367
|
+
expect(open).toHaveBeenCalledWith("", "_blank");
|
|
368
|
+
expect(popup.opener).toBeNull();
|
|
369
|
+
expect(locationAssign).toHaveBeenCalledWith(dispatchIntegrationsHref([]));
|
|
370
|
+
expect(onConnected).not.toHaveBeenCalled();
|
|
371
|
+
expect(container.textContent).toContain("Try again slack");
|
|
372
|
+
await act(async () => container.querySelector("button")?.click());
|
|
373
|
+
expect(onConnected).toHaveBeenCalledOnce();
|
|
374
|
+
act(() => root.unmount());
|
|
375
|
+
open.mockRestore();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("passes terminal request state through to the shared card", () => {
|
|
379
|
+
const container = document.createElement("div");
|
|
380
|
+
const root = createRoot(container);
|
|
381
|
+
|
|
382
|
+
act(() => {
|
|
383
|
+
root.render(
|
|
384
|
+
<McpAgentKitConnectionRequestCard
|
|
385
|
+
provider="google_drive"
|
|
386
|
+
reason="connect"
|
|
387
|
+
status="declined"
|
|
388
|
+
appId="dispatch"
|
|
389
|
+
source={{
|
|
390
|
+
id: "google_drive",
|
|
391
|
+
kind: "workspace_connection",
|
|
392
|
+
label: "Google Drive",
|
|
393
|
+
}}
|
|
394
|
+
target={{
|
|
395
|
+
threadId: "thread-1",
|
|
396
|
+
runId: "run-1",
|
|
397
|
+
requestId: "request-1",
|
|
398
|
+
}}
|
|
399
|
+
onConnected={() => undefined}
|
|
400
|
+
onDeclined={() => undefined}
|
|
401
|
+
/>,
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
expect(container.querySelector("button")).toBeNull();
|
|
406
|
+
act(() => root.unmount());
|
|
407
|
+
});
|
|
408
|
+
|
|
40
409
|
it("resumes a prose-inferred connection request through the host thread", async () => {
|
|
41
410
|
window.sessionStorage.clear();
|
|
42
411
|
window.history.replaceState({}, "", "/chat/thread-1");
|
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
import type { AgentConnectionRequest } from "@agent-native/agentkit/protocol";
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
+
agentNativePath,
|
|
4
|
+
appBasePath,
|
|
5
|
+
} from "@agent-native/core/client/api-path";
|
|
6
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
7
|
+
import {
|
|
8
|
+
getWorkspaceConnectionProvider,
|
|
9
|
+
workspaceProviderOAuthUrl,
|
|
10
|
+
} from "@agent-native/core/client/integrations";
|
|
11
|
+
import { openOAuthPopup } from "@agent-native/core/client/oauth-popup";
|
|
12
|
+
import {
|
|
13
|
+
addMcpConnectionCompleteListener,
|
|
14
|
+
clearMcpConnectionResume,
|
|
15
|
+
getPendingMcpConnectionResume,
|
|
3
16
|
saveMcpConnectionResume,
|
|
4
17
|
type McpConnectionResumeRequest,
|
|
5
18
|
} from "@agent-native/core/client/resources/mcp-connection-resume";
|
|
@@ -7,8 +20,14 @@ import {
|
|
|
7
20
|
getDefaultMcpIntegrations,
|
|
8
21
|
navigateToMcpOAuthStart,
|
|
9
22
|
} from "@agent-native/core/client/resources/mcp-integration-catalog";
|
|
10
|
-
import {
|
|
23
|
+
import { IconAlertCircle } from "@tabler/icons-react";
|
|
24
|
+
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
11
25
|
|
|
26
|
+
import { AgentConnectionRequestCard } from "../agentkit/react/components.js";
|
|
27
|
+
import {
|
|
28
|
+
dispatchIntegrationsHref,
|
|
29
|
+
useOrgSwitcherAppLinks,
|
|
30
|
+
} from "../org/workspace-app-links.js";
|
|
12
31
|
import { McpConnectionSuggestion } from "./McpConnectionSuggestion.js";
|
|
13
32
|
|
|
14
33
|
export interface McpAgentKitConnectionTarget {
|
|
@@ -20,6 +39,10 @@ export interface McpAgentKitConnectionTarget {
|
|
|
20
39
|
export interface McpAgentKitConnectionRequestCardProps {
|
|
21
40
|
provider: string;
|
|
22
41
|
detail?: string;
|
|
42
|
+
reason?: AgentConnectionRequest["reason"];
|
|
43
|
+
status?: AgentConnectionRequest["status"];
|
|
44
|
+
appId?: string;
|
|
45
|
+
source?: AgentConnectionRequest["source"];
|
|
23
46
|
target: McpAgentKitConnectionTarget;
|
|
24
47
|
onConnected: () => void | Promise<void>;
|
|
25
48
|
onDeclined: () => void | Promise<void>;
|
|
@@ -29,18 +52,35 @@ export interface McpAgentKitConnectionRequestCardProps {
|
|
|
29
52
|
export function McpAgentKitConnectionRequestCard({
|
|
30
53
|
provider,
|
|
31
54
|
detail,
|
|
55
|
+
reason,
|
|
56
|
+
status = "requested",
|
|
57
|
+
appId,
|
|
58
|
+
source,
|
|
32
59
|
target,
|
|
33
60
|
onConnected,
|
|
34
61
|
onDeclined,
|
|
35
62
|
fallback = null,
|
|
36
63
|
}: McpAgentKitConnectionRequestCardProps) {
|
|
37
64
|
const settledRef = useRef(false);
|
|
65
|
+
const [workspaceSetupOpened, setWorkspaceSetupOpened] = useState(false);
|
|
38
66
|
const integrations = useMemo(() => getDefaultMcpIntegrations(), []);
|
|
39
67
|
const integration = integrations.find(
|
|
40
68
|
(candidate) =>
|
|
41
69
|
candidate.id.toLowerCase() === provider.trim().toLowerCase() ||
|
|
42
70
|
candidate.provider.toLowerCase() === provider.trim().toLowerCase(),
|
|
43
71
|
);
|
|
72
|
+
const workspaceProvider =
|
|
73
|
+
source?.kind === "workspace_connection" && source.id === provider
|
|
74
|
+
? getWorkspaceConnectionProvider(source.id)
|
|
75
|
+
: null;
|
|
76
|
+
const needsWorkspaceSetup =
|
|
77
|
+
source?.kind === "workspace_connection" &&
|
|
78
|
+
source.id === provider &&
|
|
79
|
+
(reason === "grant" || !workspaceProvider?.oauth);
|
|
80
|
+
const { apps: workspaceApps } = useOrgSwitcherAppLinks(needsWorkspaceSetup);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (status === "failed") setWorkspaceSetupOpened(false);
|
|
83
|
+
}, [status]);
|
|
44
84
|
const settle = async (callback: () => void | Promise<void>) => {
|
|
45
85
|
if (settledRef.current) return;
|
|
46
86
|
settledRef.current = true;
|
|
@@ -51,6 +91,86 @@ export function McpAgentKitConnectionRequestCard({
|
|
|
51
91
|
throw error;
|
|
52
92
|
}
|
|
53
93
|
};
|
|
94
|
+
if (source?.kind === "workspace_connection") {
|
|
95
|
+
if (!workspaceProvider || !appId) return fallback;
|
|
96
|
+
const request: AgentConnectionRequest = {
|
|
97
|
+
id: target.requestId,
|
|
98
|
+
provider,
|
|
99
|
+
reason: reason ?? "connect",
|
|
100
|
+
status,
|
|
101
|
+
appId,
|
|
102
|
+
detail,
|
|
103
|
+
source,
|
|
104
|
+
};
|
|
105
|
+
if (reason === "grant" || !workspaceProvider.oauth) {
|
|
106
|
+
return (
|
|
107
|
+
<AgentConnectionRequestCard
|
|
108
|
+
request={request}
|
|
109
|
+
runId={target.runId}
|
|
110
|
+
providerLabel={source.label}
|
|
111
|
+
retry={workspaceSetupOpened}
|
|
112
|
+
onConnect={() => {
|
|
113
|
+
if (workspaceSetupOpened) return onConnected();
|
|
114
|
+
const setupWindow = window.open("", "_blank");
|
|
115
|
+
if (!setupWindow) return false;
|
|
116
|
+
try {
|
|
117
|
+
setupWindow.opener = null;
|
|
118
|
+
setupWindow.location.assign(
|
|
119
|
+
dispatchIntegrationsHref(workspaceApps),
|
|
120
|
+
);
|
|
121
|
+
} catch {
|
|
122
|
+
setupWindow.close();
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
setWorkspaceSetupOpened(true);
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return (
|
|
131
|
+
<AgentConnectionRequestCard
|
|
132
|
+
request={request}
|
|
133
|
+
runId={target.runId}
|
|
134
|
+
providerLabel={source.label}
|
|
135
|
+
onConnect={() => {
|
|
136
|
+
const popup = openOAuthPopup({ features: "width=640,height=760" });
|
|
137
|
+
if (!popup) return false;
|
|
138
|
+
const completionId = crypto.randomUUID();
|
|
139
|
+
const message = detail ?? `Continue after connecting ${provider}.`;
|
|
140
|
+
if (!saveMcpConnectionResume(message, target, completionId)) {
|
|
141
|
+
popup.close();
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
const returnUrl = new URL(
|
|
145
|
+
agentNativePath("/_agent-native/oauth/popup"),
|
|
146
|
+
window.location.href,
|
|
147
|
+
);
|
|
148
|
+
returnUrl.searchParams.set("complete", "workspace-connection");
|
|
149
|
+
returnUrl.searchParams.set("resume", completionId);
|
|
150
|
+
const basePath = appBasePath();
|
|
151
|
+
const hasBasePath =
|
|
152
|
+
basePath && returnUrl.pathname.startsWith(basePath + "/");
|
|
153
|
+
const returnPath =
|
|
154
|
+
(hasBasePath
|
|
155
|
+
? returnUrl.pathname.slice(basePath.length)
|
|
156
|
+
: returnUrl.pathname) + returnUrl.search;
|
|
157
|
+
try {
|
|
158
|
+
popup.location.assign(
|
|
159
|
+
workspaceProviderOAuthUrl(source.id, {
|
|
160
|
+
appId,
|
|
161
|
+
scope: "user",
|
|
162
|
+
returnPath,
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
clearMcpConnectionResume(completionId);
|
|
167
|
+
popup.close();
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
54
174
|
if (!integration) return fallback;
|
|
55
175
|
return (
|
|
56
176
|
<McpConnectionSuggestion
|
|
@@ -87,14 +207,54 @@ export function McpAgentKitConnectionResume({
|
|
|
87
207
|
onResume,
|
|
88
208
|
onMessageResume,
|
|
89
209
|
}: McpAgentKitConnectionResumeProps) {
|
|
210
|
+
const t = useT();
|
|
211
|
+
const onResumeRef = useRef(onResume);
|
|
212
|
+
const onMessageResumeRef = useRef(onMessageResume);
|
|
213
|
+
const processingRef = useRef(false);
|
|
214
|
+
const [failed, setFailed] = useState(false);
|
|
215
|
+
onResumeRef.current = onResume;
|
|
216
|
+
onMessageResumeRef.current = onMessageResume;
|
|
217
|
+
|
|
90
218
|
useEffect(() => {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
219
|
+
const resumePending = async () => {
|
|
220
|
+
if (processingRef.current) return;
|
|
221
|
+
const pending = getPendingMcpConnectionResume();
|
|
222
|
+
if (!pending) return;
|
|
223
|
+
processingRef.current = true;
|
|
224
|
+
try {
|
|
225
|
+
if (pending.agentKit) {
|
|
226
|
+
try {
|
|
227
|
+
await onResumeRef.current(pending.agentKit, pending);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (
|
|
230
|
+
!(error instanceof Error) ||
|
|
231
|
+
!error.message.startsWith("Unknown AgentKit run:") ||
|
|
232
|
+
!onMessageResumeRef.current
|
|
233
|
+
) {
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
await onMessageResumeRef.current(pending);
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
if (!onMessageResumeRef.current) return;
|
|
240
|
+
await onMessageResumeRef.current(pending);
|
|
241
|
+
}
|
|
242
|
+
clearMcpConnectionResume(pending);
|
|
243
|
+
setFailed(false);
|
|
244
|
+
} finally {
|
|
245
|
+
processingRef.current = false;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
const resume = () => {
|
|
249
|
+
void resumePending().catch(() => setFailed(true));
|
|
250
|
+
};
|
|
251
|
+
resume();
|
|
252
|
+
return addMcpConnectionCompleteListener(resume);
|
|
253
|
+
}, []);
|
|
254
|
+
return failed ? (
|
|
255
|
+
<div className="agentkit-command-error" role="alert">
|
|
256
|
+
<IconAlertCircle aria-hidden="true" className="agentkit-icon" />
|
|
257
|
+
<span>{t("agentChat.connection.failed")}</span>
|
|
258
|
+
</div>
|
|
259
|
+
) : null;
|
|
100
260
|
}
|
|
@@ -518,6 +518,7 @@ describe("SettingsTabsPage", () => {
|
|
|
518
518
|
});
|
|
519
519
|
|
|
520
520
|
it("renders What's new markdown in the legacy settings fallback", async () => {
|
|
521
|
+
await import("../changelog/Changelog.js");
|
|
521
522
|
act(() => {
|
|
522
523
|
root.render(
|
|
523
524
|
<SettingsTabsPage
|
|
@@ -607,11 +608,11 @@ describe("SettingsTabsPage", () => {
|
|
|
607
608
|
]);
|
|
608
609
|
});
|
|
609
610
|
|
|
610
|
-
it("keeps shell-only app groups out of the tabbed page", () => {
|
|
611
|
+
it("keeps shell-only app groups out of the tabbed page with legacy General content", () => {
|
|
611
612
|
act(() => {
|
|
612
613
|
root.render(
|
|
613
614
|
<SettingsTabsPage
|
|
614
|
-
general={<div>General content</div>}
|
|
615
|
+
general={<div>Legacy General content</div>}
|
|
615
616
|
generalGroups={<div>App groups</div>}
|
|
616
617
|
notifications={<div>Email settings</div>}
|
|
617
618
|
notificationsLabel="Notifications"
|
|
@@ -644,6 +645,7 @@ describe("SettingsTabsPage", () => {
|
|
|
644
645
|
]);
|
|
645
646
|
// `generalGroups` may call useSettingsShell(), which throws here.
|
|
646
647
|
expect(container.textContent).not.toContain("App groups");
|
|
648
|
+
expect(container.textContent).toContain("Legacy General content");
|
|
647
649
|
});
|
|
648
650
|
|
|
649
651
|
it("visually separates app, agent, and workspace tabs", () => {
|
|
@@ -35,10 +35,14 @@ const appState = vi.hoisted(() => ({
|
|
|
35
35
|
write: vi.fn(async (_key: string, value: unknown) => value),
|
|
36
36
|
remove: vi.fn(async () => undefined),
|
|
37
37
|
}));
|
|
38
|
-
vi.mock(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
vi.mock(
|
|
39
|
+
"@agent-native/core/client/application-state",
|
|
40
|
+
async (importOriginal) => ({
|
|
41
|
+
...(await importOriginal<Record<string, unknown>>()),
|
|
42
|
+
writeClientAppState: appState.write,
|
|
43
|
+
deleteClientAppState: appState.remove,
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
42
46
|
|
|
43
47
|
import { AgentNativeI18nProvider } from "@agent-native/core/client/i18n";
|
|
44
48
|
|
|
@@ -728,10 +732,12 @@ describe("SettingsShell", () => {
|
|
|
728
732
|
// the router is still committing the previous navigation.
|
|
729
733
|
const [, setTick] = useState(0);
|
|
730
734
|
React.useEffect(() => {
|
|
731
|
-
//
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
+
// Re-render during the 60ms loader without keeping act() busy forever.
|
|
736
|
+
let ticks = 0;
|
|
737
|
+
const timer = setInterval(() => {
|
|
738
|
+
setTick((tick) => tick + 1);
|
|
739
|
+
if (++ticks === 5) clearInterval(timer);
|
|
740
|
+
}, 10);
|
|
735
741
|
return () => clearInterval(timer);
|
|
736
742
|
}, []);
|
|
737
743
|
const current = useLocation();
|