@agent-native/toolkit 0.205.0 → 0.206.0-nightly-20261009011246
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.map +1 -1
- package/dist/app/agentkit/react/components.js +17 -9
- package/dist/app/agentkit/react/components.js.map +1 -1
- package/dist/app/auth/AuthPage.d.ts.map +1 -1
- package/dist/app/auth/AuthPage.js +11 -0
- package/dist/app/auth/AuthPage.js.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.d.ts +2 -0
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +24 -14
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/chat/AgentPanel.d.ts.map +1 -1
- package/dist/app/chat/AgentPanel.js +3 -2
- package/dist/app/chat/AgentPanel.js.map +1 -1
- package/dist/app/chat/MultiTabAssistantChat.d.ts +4 -1
- package/dist/app/chat/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/MultiTabAssistantChat.js +61 -3
- package/dist/app/chat/MultiTabAssistantChat.js.map +1 -1
- package/dist/app/chat/agentkit-chat/approval-card.d.ts.map +1 -1
- package/dist/app/chat/agentkit-chat/approval-card.js +2 -1
- package/dist/app/chat/agentkit-chat/approval-card.js.map +1 -1
- package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
- package/dist/app/chat/chat/run-recovery.js +74 -8
- package/dist/app/chat/chat/run-recovery.js.map +1 -1
- package/dist/app/chat/chat/tool-call-display.d.ts.map +1 -1
- package/dist/app/chat/chat/tool-call-display.js +6 -4
- package/dist/app/chat/chat/tool-call-display.js.map +1 -1
- package/dist/app/chat/chat-first/ChatFirstAgentActivityPanel.d.ts.map +1 -1
- package/dist/app/chat/chat-first/ChatFirstAgentActivityPanel.js +2 -1
- package/dist/app/chat/chat-first/ChatFirstAgentActivityPanel.js.map +1 -1
- package/dist/app/chat/conversation/AgentConversation.d.ts.map +1 -1
- package/dist/app/chat/conversation/AgentConversation.js +10 -7
- package/dist/app/chat/conversation/AgentConversation.js.map +1 -1
- package/dist/app/chat/fallback-chat-title.d.ts.map +1 -1
- package/dist/app/chat/fallback-chat-title.js +2 -4
- package/dist/app/chat/fallback-chat-title.js.map +1 -1
- package/dist/app/chat/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/app/chat/mcp-apps/McpAppRenderer.js +3 -2
- package/dist/app/chat/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/app/chat/session-replay-privacy.d.ts +7 -0
- package/dist/app/chat/session-replay-privacy.d.ts.map +1 -0
- package/dist/app/chat/session-replay-privacy.js +6 -0
- package/dist/app/chat/session-replay-privacy.js.map +1 -0
- package/dist/app/extensions/ExtensionViewer.d.ts +4 -1
- package/dist/app/extensions/ExtensionViewer.d.ts.map +1 -1
- package/dist/app/extensions/ExtensionViewer.js +3 -3
- package/dist/app/extensions/ExtensionViewer.js.map +1 -1
- package/dist/app/extensions/ExtensionViewerPage.d.ts +4 -1
- package/dist/app/extensions/ExtensionViewerPage.d.ts.map +1 -1
- package/dist/app/extensions/ExtensionViewerPage.js +2 -2
- package/dist/app/extensions/ExtensionViewerPage.js.map +1 -1
- package/dist/app/onboarding/FirstRunOnboarding.d.ts.map +1 -1
- package/dist/app/onboarding/FirstRunOnboarding.js +53 -12
- package/dist/app/onboarding/FirstRunOnboarding.js.map +1 -1
- package/dist/app/onboarding/OnboardingPanel.d.ts.map +1 -1
- package/dist/app/onboarding/OnboardingPanel.js +14 -4
- package/dist/app/onboarding/OnboardingPanel.js.map +1 -1
- package/dist/app/resources/ResourceEditor.d.ts +4 -1
- package/dist/app/resources/ResourceEditor.d.ts.map +1 -1
- package/dist/app/resources/ResourceEditor.js +18 -7
- package/dist/app/resources/ResourceEditor.js.map +1 -1
- package/dist/app/resources/ResourcesPanel.d.ts +4 -1
- package/dist/app/resources/ResourcesPanel.d.ts.map +1 -1
- package/dist/app/resources/ResourcesPanel.js +18 -18
- package/dist/app/resources/ResourcesPanel.js.map +1 -1
- package/dist/app/resources/custom-agent-model-options.d.ts +16 -0
- package/dist/app/resources/custom-agent-model-options.d.ts.map +1 -0
- package/dist/app/resources/custom-agent-model-options.js +53 -0
- package/dist/app/resources/custom-agent-model-options.js.map +1 -0
- package/dist/app/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/app/settings/SettingsPanel.js +34 -3
- package/dist/app/settings/SettingsPanel.js.map +1 -1
- package/dist/app/settings/SettingsTabsPage.d.ts.map +1 -1
- package/dist/app/settings/SettingsTabsPage.js +2 -0
- package/dist/app/settings/SettingsTabsPage.js.map +1 -1
- package/dist/app/settings/api-keys/ApiKeyDialogs.d.ts.map +1 -1
- package/dist/app/settings/api-keys/ApiKeyDialogs.js +49 -9
- package/dist/app/settings/api-keys/ApiKeyDialogs.js.map +1 -1
- package/dist/app/settings/api-keys/ApiKeysSettingsPage.js +2 -1
- package/dist/app/settings/api-keys/ApiKeysSettingsPage.js.map +1 -1
- package/dist/app/settings/model/ModelSettingsPage.d.ts.map +1 -1
- package/dist/app/settings/model/ModelSettingsPage.js +43 -9
- package/dist/app/settings/model/ModelSettingsPage.js.map +1 -1
- package/dist/app/settings/model/ProviderDialog.d.ts +4 -0
- package/dist/app/settings/model/ProviderDialog.d.ts.map +1 -1
- package/dist/app/settings/model/ProviderDialog.js +107 -11
- package/dist/app/settings/model/ProviderDialog.js.map +1 -1
- package/dist/app/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/app/settings/useBuilderStatus.js +13 -10
- package/dist/app/settings/useBuilderStatus.js.map +1 -1
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +51 -13
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/package.json +4 -4
- package/src/app/agentkit/react/components.interaction.spec.tsx +229 -1
- package/src/app/agentkit/react/components.spec.tsx +42 -1
- package/src/app/agentkit/react/components.tsx +46 -12
- package/src/app/auth/AuthPage.tsx +13 -0
- package/src/app/chat/AgentKitAssistantChat.spec.tsx +116 -5
- package/src/app/chat/AgentKitAssistantChat.tsx +41 -20
- package/src/app/chat/AgentPanel.tsx +5 -1
- package/src/app/chat/MultiTabAssistantChat.spec.tsx +152 -7
- package/src/app/chat/MultiTabAssistantChat.tsx +71 -4
- package/src/app/chat/agentkit-chat/approval-card.spec.tsx +17 -0
- package/src/app/chat/agentkit-chat/approval-card.tsx +2 -0
- package/src/app/chat/chat/run-recovery.spec.tsx +354 -9
- package/src/app/chat/chat/run-recovery.tsx +178 -10
- package/src/app/chat/chat/tool-call-display.spec.tsx +77 -0
- package/src/app/chat/chat/tool-call-display.tsx +23 -6
- package/src/app/chat/chat-first/ChatFirstAgentActivityPanel.tsx +2 -1
- package/src/app/chat/conversation/AgentConversation.spec.tsx +190 -1
- package/src/app/chat/conversation/AgentConversation.tsx +32 -8
- package/src/app/chat/fallback-chat-title.spec.ts +44 -0
- package/src/app/chat/fallback-chat-title.ts +3 -4
- package/src/app/chat/mcp-apps/McpAppRenderer.spec.ts +24 -0
- package/src/app/chat/mcp-apps/McpAppRenderer.tsx +10 -2
- package/src/app/chat/session-replay-privacy.spec.ts +14 -0
- package/src/app/chat/session-replay-privacy.ts +5 -0
- package/src/app/extensions/ExtensionViewer.tsx +15 -3
- package/src/app/extensions/ExtensionViewerPage.tsx +7 -3
- package/src/app/onboarding/FirstRunOnboarding.spec.tsx +252 -2
- package/src/app/onboarding/FirstRunOnboarding.tsx +56 -9
- package/src/app/onboarding/OnboardingPanel.save-scope.spec.tsx +1 -0
- package/src/app/onboarding/OnboardingPanel.tsx +15 -3
- package/src/app/resources/ResourceEditor.spec.tsx +240 -5
- package/src/app/resources/ResourceEditor.tsx +57 -10
- package/src/app/resources/ResourcesPanel.spec.ts +86 -4
- package/src/app/resources/ResourcesPanel.tsx +49 -16
- package/src/app/resources/custom-agent-model-options.ts +92 -0
- package/src/app/settings/SettingsPanel.model-defaults.spec.tsx +26 -13
- package/src/app/settings/SettingsPanel.tsx +35 -3
- package/src/app/settings/SettingsTabsPage.spec.tsx +97 -0
- package/src/app/settings/SettingsTabsPage.tsx +2 -0
- package/src/app/settings/api-keys/ApiKeyDialogs.tsx +84 -22
- package/src/app/settings/api-keys/ApiKeysSettingsPage.spec.tsx +229 -1
- package/src/app/settings/api-keys/ApiKeysSettingsPage.tsx +3 -2
- package/src/app/settings/model/ModelSettingsPage.spec.tsx +99 -9
- package/src/app/settings/model/ModelSettingsPage.tsx +79 -11
- package/src/app/settings/model/ProviderDialog.spec.tsx +192 -1
- package/src/app/settings/model/ProviderDialog.tsx +225 -29
- package/src/app/settings/useBuilderStatus.spec.tsx +49 -0
- package/src/app/settings/useBuilderStatus.ts +16 -10
- package/src/composer/TiptapComposer.spec.ts +97 -2
- package/src/composer/TiptapComposer.tsx +67 -13
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { getAgentProviderOption } from "@agent-native/core/client/agent-provider-catalog";
|
|
2
2
|
import { callAction, useActionQuery } from "@agent-native/core/client/hooks";
|
|
3
3
|
import { useT } from "@agent-native/core/client/i18n";
|
|
4
|
+
import {
|
|
5
|
+
requestCustomKeyOnboardingAbandonment,
|
|
6
|
+
trackCustomKeyOnboardingOutcome,
|
|
7
|
+
withCustomKeyOnboardingCredentialSave,
|
|
8
|
+
} from "@agent-native/core/client/onboarding/use-onboarding";
|
|
4
9
|
import { useOrg } from "@agent-native/core/client/org";
|
|
5
10
|
import type { SecretRemovalPreview } from "@agent-native/core/secrets/usage";
|
|
6
11
|
import { Alert, AlertDescription } from "@agent-native/toolkit/ui/alert";
|
|
@@ -83,9 +88,33 @@ export interface KeyValueDialogProps {
|
|
|
83
88
|
|
|
84
89
|
/** Add key (Name, Value, Available to), or Replace value on a saved key. */
|
|
85
90
|
export function KeyValueDialog(props: KeyValueDialogProps) {
|
|
91
|
+
const savePending = useRef(false);
|
|
92
|
+
const dismiss = () => {
|
|
93
|
+
if (savePending.current) {
|
|
94
|
+
requestCustomKeyOnboardingAbandonment();
|
|
95
|
+
} else {
|
|
96
|
+
trackCustomKeyOnboardingOutcome("credential_skipped");
|
|
97
|
+
}
|
|
98
|
+
props.onOpenChange(false);
|
|
99
|
+
};
|
|
100
|
+
|
|
86
101
|
return (
|
|
87
|
-
<Dialog
|
|
88
|
-
{props.open
|
|
102
|
+
<Dialog
|
|
103
|
+
open={props.open}
|
|
104
|
+
onOpenChange={(open) => {
|
|
105
|
+
if (open) props.onOpenChange(true);
|
|
106
|
+
else dismiss();
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
{props.open ? (
|
|
110
|
+
<KeyValueDialogContent
|
|
111
|
+
{...props}
|
|
112
|
+
onDismiss={dismiss}
|
|
113
|
+
onSavingChange={(saving) => {
|
|
114
|
+
savePending.current = saving;
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
) : null}
|
|
89
118
|
</Dialog>
|
|
90
119
|
);
|
|
91
120
|
}
|
|
@@ -114,11 +143,16 @@ function FormError({ message }: { message: string | null }) {
|
|
|
114
143
|
|
|
115
144
|
function KeyValueDialogContent({
|
|
116
145
|
onOpenChange,
|
|
146
|
+
onDismiss = () => onOpenChange(false),
|
|
147
|
+
onSavingChange,
|
|
117
148
|
dialog,
|
|
118
149
|
listing,
|
|
119
150
|
orgName,
|
|
120
151
|
onSaved,
|
|
121
|
-
}: KeyValueDialogProps
|
|
152
|
+
}: KeyValueDialogProps & {
|
|
153
|
+
onDismiss?: () => void;
|
|
154
|
+
onSavingChange?: (saving: boolean) => void;
|
|
155
|
+
}) {
|
|
122
156
|
const t = useT();
|
|
123
157
|
const queryClient = useQueryClient();
|
|
124
158
|
const mounted = useMounted();
|
|
@@ -175,14 +209,17 @@ function KeyValueDialogContent({
|
|
|
175
209
|
const save = async () => {
|
|
176
210
|
if (saving || !valid) return;
|
|
177
211
|
setSaving(true);
|
|
212
|
+
onSavingChange?.(true);
|
|
178
213
|
setError(null);
|
|
179
214
|
try {
|
|
180
|
-
await
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
215
|
+
await withCustomKeyOnboardingCredentialSave(() =>
|
|
216
|
+
saveApiKeyValue({
|
|
217
|
+
name: target.kind === "registered" ? target.key.name : target.name,
|
|
218
|
+
value: value.trim(),
|
|
219
|
+
registered: target.kind === "registered",
|
|
220
|
+
shared: isShared,
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
186
223
|
void refreshKeys(queryClient);
|
|
187
224
|
toast.success(replacing ? t(`${K}valueReplaced`) : t(`${K}keyAdded`));
|
|
188
225
|
onSaved?.();
|
|
@@ -191,6 +228,7 @@ function KeyValueDialogContent({
|
|
|
191
228
|
setError(err instanceof Error ? err.message : String(err));
|
|
192
229
|
} finally {
|
|
193
230
|
setSaving(false);
|
|
231
|
+
onSavingChange?.(false);
|
|
194
232
|
}
|
|
195
233
|
};
|
|
196
234
|
|
|
@@ -273,6 +311,9 @@ function KeyValueDialogContent({
|
|
|
273
311
|
onChange={(event) => {
|
|
274
312
|
setValue(event.target.value);
|
|
275
313
|
setError(null);
|
|
314
|
+
if (event.target.value.trim()) {
|
|
315
|
+
trackCustomKeyOnboardingOutcome("credential_entry_started");
|
|
316
|
+
}
|
|
276
317
|
}}
|
|
277
318
|
/>
|
|
278
319
|
{docsUrl ? (
|
|
@@ -328,11 +369,7 @@ function KeyValueDialogContent({
|
|
|
328
369
|
) : null}
|
|
329
370
|
<FormError message={error} />
|
|
330
371
|
<DialogFooter className="gap-2 sm:space-x-0">
|
|
331
|
-
<Button
|
|
332
|
-
type="button"
|
|
333
|
-
variant="secondary"
|
|
334
|
-
onClick={() => onOpenChange(false)}
|
|
335
|
-
>
|
|
372
|
+
<Button type="button" variant="secondary" onClick={onDismiss}>
|
|
336
373
|
{t(`${M}cancel`)}
|
|
337
374
|
</Button>
|
|
338
375
|
<Button type="submit" disabled={saving || !valid}>
|
|
@@ -366,10 +403,32 @@ export interface ServiceKeyDialogProps {
|
|
|
366
403
|
* from Infrastructure. Reads the listing the dialog needs itself.
|
|
367
404
|
*/
|
|
368
405
|
export function ServiceKeyDialog(props: ServiceKeyDialogProps) {
|
|
406
|
+
const savePending = useRef(false);
|
|
407
|
+
const dismiss = () => {
|
|
408
|
+
if (savePending.current) {
|
|
409
|
+
requestCustomKeyOnboardingAbandonment();
|
|
410
|
+
} else {
|
|
411
|
+
trackCustomKeyOnboardingOutcome("credential_skipped");
|
|
412
|
+
}
|
|
413
|
+
props.onOpenChange(false);
|
|
414
|
+
};
|
|
415
|
+
|
|
369
416
|
return (
|
|
370
|
-
<Dialog
|
|
417
|
+
<Dialog
|
|
418
|
+
open={props.open}
|
|
419
|
+
onOpenChange={(open) => {
|
|
420
|
+
if (open) props.onOpenChange(true);
|
|
421
|
+
else dismiss();
|
|
422
|
+
}}
|
|
423
|
+
>
|
|
371
424
|
{props.open && props.keyName ? (
|
|
372
|
-
<ServiceKeyDialogContent
|
|
425
|
+
<ServiceKeyDialogContent
|
|
426
|
+
{...props}
|
|
427
|
+
onDismiss={dismiss}
|
|
428
|
+
onSavingChange={(saving) => {
|
|
429
|
+
savePending.current = saving;
|
|
430
|
+
}}
|
|
431
|
+
/>
|
|
373
432
|
) : null}
|
|
374
433
|
</Dialog>
|
|
375
434
|
);
|
|
@@ -380,7 +439,12 @@ function ServiceKeyDialogContent({
|
|
|
380
439
|
keyName,
|
|
381
440
|
mode,
|
|
382
441
|
onSaved,
|
|
383
|
-
|
|
442
|
+
onDismiss = () => onOpenChange(false),
|
|
443
|
+
onSavingChange,
|
|
444
|
+
}: ServiceKeyDialogProps & {
|
|
445
|
+
onDismiss?: () => void;
|
|
446
|
+
onSavingChange?: (saving: boolean) => void;
|
|
447
|
+
}) {
|
|
384
448
|
const t = useT();
|
|
385
449
|
const org = useOrg();
|
|
386
450
|
const listing = useActionQuery<ApiKeysListing>("list-api-keys" as never);
|
|
@@ -403,6 +467,8 @@ function ServiceKeyDialogContent({
|
|
|
403
467
|
}
|
|
404
468
|
listing={listing.data}
|
|
405
469
|
orgName={org.data?.orgName ?? ""}
|
|
470
|
+
onDismiss={onDismiss}
|
|
471
|
+
onSavingChange={onSavingChange}
|
|
406
472
|
{...(onSaved ? { onSaved } : {})}
|
|
407
473
|
/>
|
|
408
474
|
);
|
|
@@ -451,11 +517,7 @@ function ServiceKeyDialogContent({
|
|
|
451
517
|
</div>
|
|
452
518
|
)}
|
|
453
519
|
<DialogFooter className="gap-2 sm:space-x-0">
|
|
454
|
-
<Button
|
|
455
|
-
type="button"
|
|
456
|
-
variant="secondary"
|
|
457
|
-
onClick={() => onOpenChange(false)}
|
|
458
|
-
>
|
|
520
|
+
<Button type="button" variant="secondary" onClick={onDismiss}>
|
|
459
521
|
{t(`${M}cancel`)}
|
|
460
522
|
</Button>
|
|
461
523
|
<Button type="button" disabled>
|
|
@@ -21,6 +21,22 @@ const clientMock = vi.hoisted(() => ({
|
|
|
21
21
|
test: vi.fn(),
|
|
22
22
|
notify: vi.fn(),
|
|
23
23
|
}));
|
|
24
|
+
const onboardingOutcomeMock = vi.hoisted(() => vi.fn());
|
|
25
|
+
const onboardingAbandonmentRequestMock = vi.hoisted(() => vi.fn());
|
|
26
|
+
const credentialSaveBoundaryMock = vi.hoisted(() => vi.fn());
|
|
27
|
+
|
|
28
|
+
vi.mock("@agent-native/core/client/onboarding/use-onboarding", () => ({
|
|
29
|
+
requestCustomKeyOnboardingAbandonment: onboardingAbandonmentRequestMock,
|
|
30
|
+
trackCustomKeyOnboardingOutcome: onboardingOutcomeMock,
|
|
31
|
+
withCustomKeyOnboardingCredentialSave: async (
|
|
32
|
+
save: () => Promise<unknown>,
|
|
33
|
+
) => {
|
|
34
|
+
credentialSaveBoundaryMock();
|
|
35
|
+
const result = await save();
|
|
36
|
+
onboardingOutcomeMock("credential_saved");
|
|
37
|
+
return result;
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
24
40
|
|
|
25
41
|
vi.mock("@agent-native/core/client/hooks", () => ({
|
|
26
42
|
getBrowserTabId: () => "test-tab",
|
|
@@ -88,7 +104,11 @@ vi.mock("@agent-native/core/client/i18n", () => ({
|
|
|
88
104
|
}),
|
|
89
105
|
}));
|
|
90
106
|
|
|
91
|
-
import {
|
|
107
|
+
import {
|
|
108
|
+
DeleteKeyDialog,
|
|
109
|
+
KeyValueDialog,
|
|
110
|
+
ServiceKeyDialog,
|
|
111
|
+
} from "./ApiKeyDialogs.js";
|
|
92
112
|
import ApiKeysSettingsPage from "./ApiKeysSettingsPage.js";
|
|
93
113
|
|
|
94
114
|
function entry(overrides: Partial<ApiKeyEntry>): ApiKeyEntry {
|
|
@@ -176,6 +196,17 @@ function buttonByText(text: string, scope: ParentNode = document) {
|
|
|
176
196
|
return button;
|
|
177
197
|
}
|
|
178
198
|
|
|
199
|
+
function pointerDown(element: Element | null | undefined) {
|
|
200
|
+
if (!element) throw new Error("Nothing to open");
|
|
201
|
+
element.dispatchEvent(
|
|
202
|
+
new PointerEvent("pointerdown", {
|
|
203
|
+
bubbles: true,
|
|
204
|
+
button: 0,
|
|
205
|
+
pointerType: "mouse",
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
179
210
|
function typeInto(input: HTMLInputElement, value: string) {
|
|
180
211
|
const setter = Object.getOwnPropertyDescriptor(
|
|
181
212
|
HTMLInputElement.prototype,
|
|
@@ -199,6 +230,9 @@ describe("ApiKeysSettingsPage", () => {
|
|
|
199
230
|
navigateMock.mockReset();
|
|
200
231
|
clientMock.save.mockReset();
|
|
201
232
|
clientMock.test.mockReset();
|
|
233
|
+
onboardingOutcomeMock.mockReset();
|
|
234
|
+
onboardingAbandonmentRequestMock.mockReset();
|
|
235
|
+
credentialSaveBoundaryMock.mockReset();
|
|
202
236
|
window.history.replaceState(null, "", "/settings/api-keys");
|
|
203
237
|
container = document.createElement("div");
|
|
204
238
|
document.body.appendChild(container);
|
|
@@ -297,6 +331,32 @@ describe("ApiKeysSettingsPage", () => {
|
|
|
297
331
|
});
|
|
298
332
|
});
|
|
299
333
|
|
|
334
|
+
it("does not count testing an arbitrary saved key as onboarding validation", async () => {
|
|
335
|
+
state.listing = listing({ keys: [entry({ canTest: true })] });
|
|
336
|
+
clientMock.test.mockResolvedValue({ ok: true });
|
|
337
|
+
await render();
|
|
338
|
+
|
|
339
|
+
const keyRow = row("secrets:STRIPE_SECRET_KEY");
|
|
340
|
+
const menuTrigger = keyRow.querySelector(
|
|
341
|
+
'[aria-label="Manage STRIPE_SECRET_KEY"]',
|
|
342
|
+
);
|
|
343
|
+
await act(async () => pointerDown(menuTrigger));
|
|
344
|
+
const testItem = [...document.querySelectorAll('[role="menuitem"]')].find(
|
|
345
|
+
(item) => item.textContent?.trim() === "Test",
|
|
346
|
+
);
|
|
347
|
+
if (!testItem) throw new Error("No Test menu item");
|
|
348
|
+
await act(async () =>
|
|
349
|
+
testItem.dispatchEvent(new MouseEvent("click", { bubbles: true })),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
await vi.waitFor(() =>
|
|
353
|
+
expect(clientMock.test).toHaveBeenCalledWith("STRIPE_SECRET_KEY"),
|
|
354
|
+
);
|
|
355
|
+
expect(onboardingOutcomeMock).not.toHaveBeenCalledWith(
|
|
356
|
+
"credential_validated",
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
|
|
300
360
|
it("lists organization keys for admins", async () => {
|
|
301
361
|
state.listing = listing({
|
|
302
362
|
canManageOrg: true,
|
|
@@ -407,6 +467,93 @@ describe("ApiKeysSettingsPage", () => {
|
|
|
407
467
|
expect(dialog.textContent).toContain("Get key");
|
|
408
468
|
});
|
|
409
469
|
|
|
470
|
+
it("does not count saving a registered key as validation", async () => {
|
|
471
|
+
clientMock.save.mockResolvedValue(undefined);
|
|
472
|
+
await render();
|
|
473
|
+
const header = await renderHeader();
|
|
474
|
+
await act(async () => buttonByText("Add key", header).click());
|
|
475
|
+
|
|
476
|
+
const dialog = document.querySelector('[role="dialog"]') as HTMLElement;
|
|
477
|
+
const [name, value] = [
|
|
478
|
+
...dialog.querySelectorAll("input"),
|
|
479
|
+
] as HTMLInputElement[];
|
|
480
|
+
await act(async () => typeInto(name!, "git"));
|
|
481
|
+
await act(async () => buttonByText("GITHUB_TOKEN", dialog).click());
|
|
482
|
+
await act(async () => typeInto(value!, "fake-github-token"));
|
|
483
|
+
await act(async () => {
|
|
484
|
+
dialog
|
|
485
|
+
.querySelector("form")!
|
|
486
|
+
.dispatchEvent(
|
|
487
|
+
new Event("submit", { bubbles: true, cancelable: true }),
|
|
488
|
+
);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
const outcomes = onboardingOutcomeMock.mock.calls.map(
|
|
492
|
+
([outcome]) => outcome,
|
|
493
|
+
);
|
|
494
|
+
expect(outcomes).toContain("credential_saved");
|
|
495
|
+
expect(outcomes).not.toContain("credential_validated");
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("allows leaving a key dialog while preserving its pending save outcome", async () => {
|
|
499
|
+
let resolveSave!: () => void;
|
|
500
|
+
clientMock.save.mockReturnValue(
|
|
501
|
+
new Promise<void>((resolve) => {
|
|
502
|
+
resolveSave = resolve;
|
|
503
|
+
}),
|
|
504
|
+
);
|
|
505
|
+
const onOpenChange = vi.fn();
|
|
506
|
+
await act(async () => {
|
|
507
|
+
root.render(
|
|
508
|
+
<QueryClientProvider client={new QueryClient()}>
|
|
509
|
+
<KeyValueDialog
|
|
510
|
+
open
|
|
511
|
+
onOpenChange={onOpenChange}
|
|
512
|
+
dialog={{ mode: "add" }}
|
|
513
|
+
listing={listing()}
|
|
514
|
+
orgName="Acme"
|
|
515
|
+
/>
|
|
516
|
+
</QueryClientProvider>,
|
|
517
|
+
);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const dialog = document.querySelector('[role="dialog"]') as HTMLElement;
|
|
521
|
+
const [name, value] = [
|
|
522
|
+
...dialog.querySelectorAll("input"),
|
|
523
|
+
] as HTMLInputElement[];
|
|
524
|
+
await act(async () => typeInto(name!, "CUSTOM_API_KEY"));
|
|
525
|
+
await act(async () => typeInto(value!, "fake-custom-value"));
|
|
526
|
+
await act(async () => {
|
|
527
|
+
dialog
|
|
528
|
+
.querySelector("form")!
|
|
529
|
+
.dispatchEvent(
|
|
530
|
+
new Event("submit", { bubbles: true, cancelable: true }),
|
|
531
|
+
);
|
|
532
|
+
});
|
|
533
|
+
expect(clientMock.save).toHaveBeenCalledWith({
|
|
534
|
+
name: "CUSTOM_API_KEY",
|
|
535
|
+
value: "fake-custom-value",
|
|
536
|
+
registered: false,
|
|
537
|
+
shared: false,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
await act(async () => buttonByText("Close", dialog).click());
|
|
541
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
542
|
+
expect(onboardingAbandonmentRequestMock).toHaveBeenCalledTimes(1);
|
|
543
|
+
expect(
|
|
544
|
+
onboardingOutcomeMock.mock.calls.map(([outcome]) => outcome),
|
|
545
|
+
).not.toContain("credential_skipped");
|
|
546
|
+
|
|
547
|
+
await act(async () => resolveSave());
|
|
548
|
+
await vi.waitFor(() => expect(onOpenChange).toHaveBeenCalledWith(false));
|
|
549
|
+
const outcomes = onboardingOutcomeMock.mock.calls.map(
|
|
550
|
+
([outcome]) => outcome,
|
|
551
|
+
);
|
|
552
|
+
expect(outcomes).toContain("credential_saved");
|
|
553
|
+
expect(outcomes).not.toContain("credential_skipped");
|
|
554
|
+
expect(credentialSaveBoundaryMock).toHaveBeenCalledTimes(1);
|
|
555
|
+
});
|
|
556
|
+
|
|
410
557
|
it("opens Add key for a #secrets:KEY link to a key nobody saved", async () => {
|
|
411
558
|
window.history.replaceState(
|
|
412
559
|
null,
|
|
@@ -505,6 +652,9 @@ describe("DeleteKeyDialog", () => {
|
|
|
505
652
|
beforeEach(() => {
|
|
506
653
|
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
507
654
|
callActionMock.mockReset();
|
|
655
|
+
clientMock.save.mockReset();
|
|
656
|
+
onboardingOutcomeMock.mockReset();
|
|
657
|
+
credentialSaveBoundaryMock.mockReset();
|
|
508
658
|
container = document.createElement("div");
|
|
509
659
|
document.body.appendChild(container);
|
|
510
660
|
root = createRoot(container);
|
|
@@ -617,6 +767,84 @@ describe("DeleteKeyDialog", () => {
|
|
|
617
767
|
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
618
768
|
});
|
|
619
769
|
|
|
770
|
+
it("records a service-key attempt as skipped when canceled", async () => {
|
|
771
|
+
state.listing = listing({ canManageOrg: true });
|
|
772
|
+
const onOpenChange = vi.fn();
|
|
773
|
+
await act(async () => {
|
|
774
|
+
root.render(
|
|
775
|
+
<QueryClientProvider client={new QueryClient()}>
|
|
776
|
+
<ServiceKeyDialog
|
|
777
|
+
open
|
|
778
|
+
onOpenChange={onOpenChange}
|
|
779
|
+
keyName="VOYAGE_API_KEY"
|
|
780
|
+
mode="add"
|
|
781
|
+
/>
|
|
782
|
+
</QueryClientProvider>,
|
|
783
|
+
);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
const dialog = document.querySelector('[role="dialog"]') as HTMLElement;
|
|
787
|
+
await act(async () => buttonByText("Cancel", dialog).click());
|
|
788
|
+
|
|
789
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
790
|
+
expect(onboardingOutcomeMock).toHaveBeenCalledWith("credential_skipped");
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it("allows leaving a service-key dialog while preserving its pending save outcome", async () => {
|
|
794
|
+
let resolveSave!: () => void;
|
|
795
|
+
clientMock.save.mockReturnValue(
|
|
796
|
+
new Promise<void>((resolve) => {
|
|
797
|
+
resolveSave = resolve;
|
|
798
|
+
}),
|
|
799
|
+
);
|
|
800
|
+
state.listing = listing({ canManageOrg: true });
|
|
801
|
+
const onOpenChange = vi.fn();
|
|
802
|
+
await act(async () => {
|
|
803
|
+
root.render(
|
|
804
|
+
<QueryClientProvider client={new QueryClient()}>
|
|
805
|
+
<ServiceKeyDialog
|
|
806
|
+
open
|
|
807
|
+
onOpenChange={onOpenChange}
|
|
808
|
+
keyName="VOYAGE_API_KEY"
|
|
809
|
+
mode="add"
|
|
810
|
+
/>
|
|
811
|
+
</QueryClientProvider>,
|
|
812
|
+
);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
const dialog = document.querySelector('[role="dialog"]') as HTMLElement;
|
|
816
|
+
const [, value] = [
|
|
817
|
+
...dialog.querySelectorAll("input"),
|
|
818
|
+
] as HTMLInputElement[];
|
|
819
|
+
await act(async () => typeInto(value!, "fake-voyage-value"));
|
|
820
|
+
await act(async () => {
|
|
821
|
+
dialog
|
|
822
|
+
.querySelector("form")!
|
|
823
|
+
.dispatchEvent(
|
|
824
|
+
new Event("submit", { bubbles: true, cancelable: true }),
|
|
825
|
+
);
|
|
826
|
+
});
|
|
827
|
+
await vi.waitFor(() => expect(clientMock.save).toHaveBeenCalled());
|
|
828
|
+
|
|
829
|
+
await act(async () => buttonByText("Close", dialog).click());
|
|
830
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
831
|
+
expect(onboardingAbandonmentRequestMock).toHaveBeenCalledTimes(1);
|
|
832
|
+
expect(
|
|
833
|
+
onboardingOutcomeMock.mock.calls.map(([outcome]) => outcome),
|
|
834
|
+
).not.toContain("credential_skipped");
|
|
835
|
+
|
|
836
|
+
await act(async () => {
|
|
837
|
+
resolveSave();
|
|
838
|
+
await vi.waitFor(() => expect(onOpenChange).toHaveBeenCalledWith(false));
|
|
839
|
+
});
|
|
840
|
+
const outcomes = onboardingOutcomeMock.mock.calls.map(
|
|
841
|
+
([outcome]) => outcome,
|
|
842
|
+
);
|
|
843
|
+
expect(outcomes).toContain("credential_saved");
|
|
844
|
+
expect(outcomes).not.toContain("credential_skipped");
|
|
845
|
+
expect(credentialSaveBoundaryMock).toHaveBeenCalledTimes(1);
|
|
846
|
+
});
|
|
847
|
+
|
|
620
848
|
it("replaces a service's saved organization key from Manage", async () => {
|
|
621
849
|
state.listing = listing({
|
|
622
850
|
canManageOrg: true,
|
|
@@ -383,8 +383,9 @@ function KeyMenu({
|
|
|
383
383
|
const t = useT();
|
|
384
384
|
const test = async () => {
|
|
385
385
|
const result = await testSavedApiKey(entry.name);
|
|
386
|
-
if (result.ok)
|
|
387
|
-
|
|
386
|
+
if (result.ok) {
|
|
387
|
+
toast.success(t(`${K}testPassed`));
|
|
388
|
+
} else toast.error(result.error);
|
|
388
389
|
};
|
|
389
390
|
return (
|
|
390
391
|
<DropdownMenu>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
4
4
|
import React, { act } from "react";
|
|
5
5
|
import { createRoot, type Root } from "react-dom/client";
|
|
6
|
+
import { MemoryRouter, useLocation } from "react-router";
|
|
6
7
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
8
|
|
|
8
9
|
import englishMessages from "../../i18n/catalogs/en-US.js";
|
|
@@ -23,6 +24,7 @@ const state = vi.hoisted(() => ({
|
|
|
23
24
|
modelsRefetch: vi.fn(),
|
|
24
25
|
builder: {} as Record<string, unknown>,
|
|
25
26
|
header: null as { action?: unknown } | null,
|
|
27
|
+
location: null as unknown,
|
|
26
28
|
loop: {
|
|
27
29
|
maxIterations: 400,
|
|
28
30
|
defaultMaxIterations: 400,
|
|
@@ -150,6 +152,11 @@ vi.mock("@agent-native/core/client/i18n", () => ({
|
|
|
150
152
|
|
|
151
153
|
import ModelSettingsPage from "./ModelSettingsPage.js";
|
|
152
154
|
|
|
155
|
+
function LocationProbe() {
|
|
156
|
+
state.location = useLocation();
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
153
160
|
const PROVIDERS = [
|
|
154
161
|
"openrouter",
|
|
155
162
|
"ollama",
|
|
@@ -324,6 +331,8 @@ describe("ModelSettingsPage", () => {
|
|
|
324
331
|
};
|
|
325
332
|
state.builder = builderFlow();
|
|
326
333
|
state.header = null;
|
|
334
|
+
state.location = null;
|
|
335
|
+
dialogProps.last = null;
|
|
327
336
|
queryClient = new QueryClient();
|
|
328
337
|
state.loop = { ...state.loop, canUpdate: true };
|
|
329
338
|
callActionMock.mockReset();
|
|
@@ -331,6 +340,7 @@ describe("ModelSettingsPage", () => {
|
|
|
331
340
|
popupMock.mockReset();
|
|
332
341
|
navigateMock.mockReset();
|
|
333
342
|
loopMock.save.mockReset();
|
|
343
|
+
window.history.replaceState(null, "", "/settings/model");
|
|
334
344
|
container = document.createElement("div");
|
|
335
345
|
document.body.appendChild(container);
|
|
336
346
|
root = createRoot(container);
|
|
@@ -343,21 +353,77 @@ describe("ModelSettingsPage", () => {
|
|
|
343
353
|
vi.unstubAllGlobals();
|
|
344
354
|
});
|
|
345
355
|
|
|
346
|
-
async function render() {
|
|
356
|
+
async function render(locationState?: unknown) {
|
|
357
|
+
const entry = new URL(window.location.href);
|
|
347
358
|
await act(async () => {
|
|
348
359
|
root.render(
|
|
349
|
-
<
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
360
|
+
<MemoryRouter
|
|
361
|
+
initialEntries={[
|
|
362
|
+
{
|
|
363
|
+
pathname: entry.pathname,
|
|
364
|
+
search: entry.search,
|
|
365
|
+
hash: entry.hash,
|
|
366
|
+
state: locationState,
|
|
367
|
+
},
|
|
368
|
+
]}
|
|
369
|
+
>
|
|
370
|
+
<LocationProbe />
|
|
371
|
+
<QueryClientProvider client={queryClient}>
|
|
372
|
+
<ModelSettingsPage
|
|
373
|
+
pageId="model"
|
|
374
|
+
sub={null}
|
|
375
|
+
context={{} as never}
|
|
376
|
+
bridge={{} as never}
|
|
377
|
+
/>
|
|
378
|
+
</QueryClientProvider>
|
|
379
|
+
</MemoryRouter>,
|
|
357
380
|
);
|
|
358
381
|
});
|
|
359
382
|
}
|
|
360
383
|
|
|
384
|
+
it("uses chat setup attribution for the first provider dialog opening only", async () => {
|
|
385
|
+
window.history.replaceState(null, "", "/settings/model?from=chat#llm");
|
|
386
|
+
state.listing = listing(
|
|
387
|
+
{},
|
|
388
|
+
{
|
|
389
|
+
anthropic: {
|
|
390
|
+
personal: { scope: "user", masked: "••••1234", updatedAt: 1 },
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
);
|
|
394
|
+
await render({
|
|
395
|
+
providerSetupTrackingFlow: "chat_setup",
|
|
396
|
+
returnTo: "/chat",
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
await act(async () => {
|
|
400
|
+
row("provider-personal-anthropic").querySelector("button")!.click();
|
|
401
|
+
});
|
|
402
|
+
expect(dialogProps.last).toMatchObject({
|
|
403
|
+
open: true,
|
|
404
|
+
trackingFlow: "chat_setup",
|
|
405
|
+
});
|
|
406
|
+
expect(state.location).toMatchObject({
|
|
407
|
+
pathname: "/settings/model",
|
|
408
|
+
search: "?from=chat",
|
|
409
|
+
hash: "#llm",
|
|
410
|
+
state: { returnTo: "/chat" },
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
await act(async () => {
|
|
414
|
+
(
|
|
415
|
+
dialogProps.last as { onOpenChange: (open: boolean) => void }
|
|
416
|
+
).onOpenChange(false);
|
|
417
|
+
});
|
|
418
|
+
await act(async () => {
|
|
419
|
+
row("provider-personal-anthropic").querySelector("button")!.click();
|
|
420
|
+
});
|
|
421
|
+
expect(dialogProps.last).toMatchObject({
|
|
422
|
+
open: true,
|
|
423
|
+
trackingFlow: "settings",
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
361
427
|
it("shows members organization providers read-only and their own as manageable", async () => {
|
|
362
428
|
state.listing = listing(
|
|
363
429
|
{},
|
|
@@ -709,6 +775,30 @@ describe("ModelSettingsPage", () => {
|
|
|
709
775
|
expect(row("llm").textContent).not.toContain("Add a model provider");
|
|
710
776
|
});
|
|
711
777
|
|
|
778
|
+
it("shows the current Builder model for a retired stored default", async () => {
|
|
779
|
+
state.listing = listing({
|
|
780
|
+
canManageOrg: true,
|
|
781
|
+
canUpdateDefault: true,
|
|
782
|
+
defaultModel: { engine: "builder", model: "claude-sonnet-5" },
|
|
783
|
+
});
|
|
784
|
+
state.models = {
|
|
785
|
+
providers: models().providers.map((provider) =>
|
|
786
|
+
provider.provider === "builder"
|
|
787
|
+
? { ...provider, recommendedModels: ["claude-sonnet-5-5"] }
|
|
788
|
+
: provider,
|
|
789
|
+
),
|
|
790
|
+
};
|
|
791
|
+
state.builder = builderFlow();
|
|
792
|
+
await render();
|
|
793
|
+
|
|
794
|
+
const defaultRow = row("default-model");
|
|
795
|
+
await vi.waitFor(() => {
|
|
796
|
+
expect(defaultRow.textContent).toContain(
|
|
797
|
+
"claude-sonnet-5-5 · Builder.io",
|
|
798
|
+
);
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
|
|
712
802
|
it("offers a deployment-configured provider when there is no saved key or default", async () => {
|
|
713
803
|
state.listing = listing(
|
|
714
804
|
{ canManageOrg: true, canUpdateDefault: true, defaultModel: null },
|