@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.
Files changed (143) hide show
  1. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  2. package/dist/app/agentkit/react/components.js +17 -9
  3. package/dist/app/agentkit/react/components.js.map +1 -1
  4. package/dist/app/auth/AuthPage.d.ts.map +1 -1
  5. package/dist/app/auth/AuthPage.js +11 -0
  6. package/dist/app/auth/AuthPage.js.map +1 -1
  7. package/dist/app/chat/AgentKitAssistantChat.d.ts +2 -0
  8. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  9. package/dist/app/chat/AgentKitAssistantChat.js +24 -14
  10. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  11. package/dist/app/chat/AgentPanel.d.ts.map +1 -1
  12. package/dist/app/chat/AgentPanel.js +3 -2
  13. package/dist/app/chat/AgentPanel.js.map +1 -1
  14. package/dist/app/chat/MultiTabAssistantChat.d.ts +4 -1
  15. package/dist/app/chat/MultiTabAssistantChat.d.ts.map +1 -1
  16. package/dist/app/chat/MultiTabAssistantChat.js +61 -3
  17. package/dist/app/chat/MultiTabAssistantChat.js.map +1 -1
  18. package/dist/app/chat/agentkit-chat/approval-card.d.ts.map +1 -1
  19. package/dist/app/chat/agentkit-chat/approval-card.js +2 -1
  20. package/dist/app/chat/agentkit-chat/approval-card.js.map +1 -1
  21. package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
  22. package/dist/app/chat/chat/run-recovery.js +74 -8
  23. package/dist/app/chat/chat/run-recovery.js.map +1 -1
  24. package/dist/app/chat/chat/tool-call-display.d.ts.map +1 -1
  25. package/dist/app/chat/chat/tool-call-display.js +6 -4
  26. package/dist/app/chat/chat/tool-call-display.js.map +1 -1
  27. package/dist/app/chat/chat-first/ChatFirstAgentActivityPanel.d.ts.map +1 -1
  28. package/dist/app/chat/chat-first/ChatFirstAgentActivityPanel.js +2 -1
  29. package/dist/app/chat/chat-first/ChatFirstAgentActivityPanel.js.map +1 -1
  30. package/dist/app/chat/conversation/AgentConversation.d.ts.map +1 -1
  31. package/dist/app/chat/conversation/AgentConversation.js +10 -7
  32. package/dist/app/chat/conversation/AgentConversation.js.map +1 -1
  33. package/dist/app/chat/fallback-chat-title.d.ts.map +1 -1
  34. package/dist/app/chat/fallback-chat-title.js +2 -4
  35. package/dist/app/chat/fallback-chat-title.js.map +1 -1
  36. package/dist/app/chat/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  37. package/dist/app/chat/mcp-apps/McpAppRenderer.js +3 -2
  38. package/dist/app/chat/mcp-apps/McpAppRenderer.js.map +1 -1
  39. package/dist/app/chat/session-replay-privacy.d.ts +7 -0
  40. package/dist/app/chat/session-replay-privacy.d.ts.map +1 -0
  41. package/dist/app/chat/session-replay-privacy.js +6 -0
  42. package/dist/app/chat/session-replay-privacy.js.map +1 -0
  43. package/dist/app/extensions/ExtensionViewer.d.ts +4 -1
  44. package/dist/app/extensions/ExtensionViewer.d.ts.map +1 -1
  45. package/dist/app/extensions/ExtensionViewer.js +3 -3
  46. package/dist/app/extensions/ExtensionViewer.js.map +1 -1
  47. package/dist/app/extensions/ExtensionViewerPage.d.ts +4 -1
  48. package/dist/app/extensions/ExtensionViewerPage.d.ts.map +1 -1
  49. package/dist/app/extensions/ExtensionViewerPage.js +2 -2
  50. package/dist/app/extensions/ExtensionViewerPage.js.map +1 -1
  51. package/dist/app/onboarding/FirstRunOnboarding.d.ts.map +1 -1
  52. package/dist/app/onboarding/FirstRunOnboarding.js +53 -12
  53. package/dist/app/onboarding/FirstRunOnboarding.js.map +1 -1
  54. package/dist/app/onboarding/OnboardingPanel.d.ts.map +1 -1
  55. package/dist/app/onboarding/OnboardingPanel.js +14 -4
  56. package/dist/app/onboarding/OnboardingPanel.js.map +1 -1
  57. package/dist/app/resources/ResourceEditor.d.ts +4 -1
  58. package/dist/app/resources/ResourceEditor.d.ts.map +1 -1
  59. package/dist/app/resources/ResourceEditor.js +18 -7
  60. package/dist/app/resources/ResourceEditor.js.map +1 -1
  61. package/dist/app/resources/ResourcesPanel.d.ts +4 -1
  62. package/dist/app/resources/ResourcesPanel.d.ts.map +1 -1
  63. package/dist/app/resources/ResourcesPanel.js +18 -18
  64. package/dist/app/resources/ResourcesPanel.js.map +1 -1
  65. package/dist/app/resources/custom-agent-model-options.d.ts +16 -0
  66. package/dist/app/resources/custom-agent-model-options.d.ts.map +1 -0
  67. package/dist/app/resources/custom-agent-model-options.js +53 -0
  68. package/dist/app/resources/custom-agent-model-options.js.map +1 -0
  69. package/dist/app/settings/SettingsPanel.d.ts.map +1 -1
  70. package/dist/app/settings/SettingsPanel.js +34 -3
  71. package/dist/app/settings/SettingsPanel.js.map +1 -1
  72. package/dist/app/settings/SettingsTabsPage.d.ts.map +1 -1
  73. package/dist/app/settings/SettingsTabsPage.js +2 -0
  74. package/dist/app/settings/SettingsTabsPage.js.map +1 -1
  75. package/dist/app/settings/api-keys/ApiKeyDialogs.d.ts.map +1 -1
  76. package/dist/app/settings/api-keys/ApiKeyDialogs.js +49 -9
  77. package/dist/app/settings/api-keys/ApiKeyDialogs.js.map +1 -1
  78. package/dist/app/settings/api-keys/ApiKeysSettingsPage.js +2 -1
  79. package/dist/app/settings/api-keys/ApiKeysSettingsPage.js.map +1 -1
  80. package/dist/app/settings/model/ModelSettingsPage.d.ts.map +1 -1
  81. package/dist/app/settings/model/ModelSettingsPage.js +43 -9
  82. package/dist/app/settings/model/ModelSettingsPage.js.map +1 -1
  83. package/dist/app/settings/model/ProviderDialog.d.ts +4 -0
  84. package/dist/app/settings/model/ProviderDialog.d.ts.map +1 -1
  85. package/dist/app/settings/model/ProviderDialog.js +107 -11
  86. package/dist/app/settings/model/ProviderDialog.js.map +1 -1
  87. package/dist/app/settings/useBuilderStatus.d.ts.map +1 -1
  88. package/dist/app/settings/useBuilderStatus.js +13 -10
  89. package/dist/app/settings/useBuilderStatus.js.map +1 -1
  90. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  91. package/dist/composer/TiptapComposer.js +51 -13
  92. package/dist/composer/TiptapComposer.js.map +1 -1
  93. package/package.json +4 -4
  94. package/src/app/agentkit/react/components.interaction.spec.tsx +229 -1
  95. package/src/app/agentkit/react/components.spec.tsx +42 -1
  96. package/src/app/agentkit/react/components.tsx +46 -12
  97. package/src/app/auth/AuthPage.tsx +13 -0
  98. package/src/app/chat/AgentKitAssistantChat.spec.tsx +116 -5
  99. package/src/app/chat/AgentKitAssistantChat.tsx +41 -20
  100. package/src/app/chat/AgentPanel.tsx +5 -1
  101. package/src/app/chat/MultiTabAssistantChat.spec.tsx +152 -7
  102. package/src/app/chat/MultiTabAssistantChat.tsx +71 -4
  103. package/src/app/chat/agentkit-chat/approval-card.spec.tsx +17 -0
  104. package/src/app/chat/agentkit-chat/approval-card.tsx +2 -0
  105. package/src/app/chat/chat/run-recovery.spec.tsx +354 -9
  106. package/src/app/chat/chat/run-recovery.tsx +178 -10
  107. package/src/app/chat/chat/tool-call-display.spec.tsx +77 -0
  108. package/src/app/chat/chat/tool-call-display.tsx +23 -6
  109. package/src/app/chat/chat-first/ChatFirstAgentActivityPanel.tsx +2 -1
  110. package/src/app/chat/conversation/AgentConversation.spec.tsx +190 -1
  111. package/src/app/chat/conversation/AgentConversation.tsx +32 -8
  112. package/src/app/chat/fallback-chat-title.spec.ts +44 -0
  113. package/src/app/chat/fallback-chat-title.ts +3 -4
  114. package/src/app/chat/mcp-apps/McpAppRenderer.spec.ts +24 -0
  115. package/src/app/chat/mcp-apps/McpAppRenderer.tsx +10 -2
  116. package/src/app/chat/session-replay-privacy.spec.ts +14 -0
  117. package/src/app/chat/session-replay-privacy.ts +5 -0
  118. package/src/app/extensions/ExtensionViewer.tsx +15 -3
  119. package/src/app/extensions/ExtensionViewerPage.tsx +7 -3
  120. package/src/app/onboarding/FirstRunOnboarding.spec.tsx +252 -2
  121. package/src/app/onboarding/FirstRunOnboarding.tsx +56 -9
  122. package/src/app/onboarding/OnboardingPanel.save-scope.spec.tsx +1 -0
  123. package/src/app/onboarding/OnboardingPanel.tsx +15 -3
  124. package/src/app/resources/ResourceEditor.spec.tsx +240 -5
  125. package/src/app/resources/ResourceEditor.tsx +57 -10
  126. package/src/app/resources/ResourcesPanel.spec.ts +86 -4
  127. package/src/app/resources/ResourcesPanel.tsx +49 -16
  128. package/src/app/resources/custom-agent-model-options.ts +92 -0
  129. package/src/app/settings/SettingsPanel.model-defaults.spec.tsx +26 -13
  130. package/src/app/settings/SettingsPanel.tsx +35 -3
  131. package/src/app/settings/SettingsTabsPage.spec.tsx +97 -0
  132. package/src/app/settings/SettingsTabsPage.tsx +2 -0
  133. package/src/app/settings/api-keys/ApiKeyDialogs.tsx +84 -22
  134. package/src/app/settings/api-keys/ApiKeysSettingsPage.spec.tsx +229 -1
  135. package/src/app/settings/api-keys/ApiKeysSettingsPage.tsx +3 -2
  136. package/src/app/settings/model/ModelSettingsPage.spec.tsx +99 -9
  137. package/src/app/settings/model/ModelSettingsPage.tsx +79 -11
  138. package/src/app/settings/model/ProviderDialog.spec.tsx +192 -1
  139. package/src/app/settings/model/ProviderDialog.tsx +225 -29
  140. package/src/app/settings/useBuilderStatus.spec.tsx +49 -0
  141. package/src/app/settings/useBuilderStatus.ts +16 -10
  142. package/src/composer/TiptapComposer.spec.ts +97 -2
  143. package/src/composer/TiptapComposer.tsx +67 -13
@@ -1,6 +1,6 @@
1
1
  import type { ProviderKeyPolicyStatus } from "@agent-native/core/agent/actions/manage-provider-key-policy";
2
2
  import { CHATGPT_SUBSCRIPTION_ENGINE_NAME } from "@agent-native/core/agent/chatgpt-subscription-contract";
3
- import { upgradeModelToLatestSupportedVersion } from "@agent-native/core/agent/model-version";
3
+ import { upgradeModelForProvider } from "@agent-native/core/agent/model-version";
4
4
  import {
5
5
  setAgentEngineDefaultModel,
6
6
  type AgentEngineKeyScope,
@@ -42,7 +42,16 @@ import {
42
42
  } from "@agent-native/toolkit/ui/tooltip";
43
43
  import { IconCpu, IconLock, IconPlus } from "@tabler/icons-react";
44
44
  import { useQuery, useQueryClient } from "@tanstack/react-query";
45
- import { useEffect, useId, useMemo, useState, type ReactNode } from "react";
45
+ import {
46
+ useCallback,
47
+ useEffect,
48
+ useId,
49
+ useMemo,
50
+ useRef,
51
+ useState,
52
+ type ReactNode,
53
+ } from "react";
54
+ import { useLocation, useNavigate } from "react-router";
46
55
 
47
56
  import { ErrorRow, SettingsEmpty } from "../../resources/index.js";
48
57
  import { DeferredBuilderConnectPopover } from "../deferred-builder-connect-popover.js";
@@ -90,6 +99,11 @@ type DialogState =
90
99
  | { mode: "add" }
91
100
  | { mode: "manage"; provider: AgentProviderId; scope: AgentEngineKeyScope };
92
101
 
102
+ type ActiveDialogState = {
103
+ dialog: DialogState;
104
+ trackingFlow: "chat_setup" | "settings";
105
+ };
106
+
93
107
  type ChatGPTSubscriptionStatusRead = {
94
108
  connected: boolean;
95
109
  activeAccountId: string | null;
@@ -156,6 +170,12 @@ function useChatGPTModels(
156
170
  */
157
171
  export default function ModelSettingsPage(_props: SettingsPageProps) {
158
172
  const t = useT();
173
+ const location = useLocation();
174
+ const navigate = useNavigate();
175
+ const chatSetupTrackingPending = useRef(
176
+ (location.state as { providerSetupTrackingFlow?: unknown } | null)
177
+ ?.providerSetupTrackingFlow === "chat_setup",
178
+ );
159
179
  const org = useOrg();
160
180
  const listing = useActionQuery<ModelProvidersListing>(
161
181
  "list-model-providers" as never,
@@ -168,8 +188,42 @@ export default function ModelSettingsPage(_props: SettingsPageProps) {
168
188
  trackingSource: "settings_model",
169
189
  trackingFlow: "connect_llm",
170
190
  });
171
- const [dialog, setDialog] = useState<DialogState | null>(null);
191
+ const [dialog, setDialog] = useState<ActiveDialogState | null>(null);
172
192
  const [removing, setRemoving] = useState<ProviderKeyRow | null>(null);
193
+ const openProviderDialog = useCallback(
194
+ (nextDialog: DialogState) => {
195
+ const trackingFlow = chatSetupTrackingPending.current
196
+ ? "chat_setup"
197
+ : "settings";
198
+ if (chatSetupTrackingPending.current) {
199
+ chatSetupTrackingPending.current = false;
200
+ const locationState =
201
+ location.state && typeof location.state === "object"
202
+ ? Object.fromEntries(
203
+ Object.entries(location.state).filter(
204
+ ([key]) => key !== "providerSetupTrackingFlow",
205
+ ),
206
+ )
207
+ : null;
208
+ navigate(
209
+ {
210
+ pathname: location.pathname,
211
+ search: location.search,
212
+ hash: location.hash,
213
+ },
214
+ {
215
+ replace: true,
216
+ state:
217
+ locationState && Object.keys(locationState).length > 0
218
+ ? locationState
219
+ : null,
220
+ },
221
+ );
222
+ }
223
+ setDialog({ dialog: nextDialog, trackingFlow });
224
+ },
225
+ [location.hash, location.pathname, location.search, navigate],
226
+ );
173
227
 
174
228
  const chatgptLab = useLabState("chatgpt-subscription");
175
229
  const chatgptStatus = useActionQuery<ChatGPTSubscriptionStatusRead>(
@@ -212,10 +266,12 @@ export default function ModelSettingsPage(_props: SettingsPageProps) {
212
266
  () => ({
213
267
  action:
214
268
  canAdd && !needsProvider ? (
215
- <AddProviderButton onClick={() => setDialog({ mode: "add" })} />
269
+ <AddProviderButton
270
+ onClick={() => openProviderDialog({ mode: "add" })}
271
+ />
216
272
  ) : undefined,
217
273
  }),
218
- [canAdd, needsProvider],
274
+ [canAdd, needsProvider, openProviderDialog],
219
275
  );
220
276
  useSettingsPageHeader(header);
221
277
 
@@ -240,7 +296,11 @@ export default function ModelSettingsPage(_props: SettingsPageProps) {
240
296
  : { status: "loading" };
241
297
  const orgName = org.data?.orgName ?? "";
242
298
  const openManage = (row: ProviderKeyRow) =>
243
- setDialog({ mode: "manage", provider: row.provider, scope: row.key.scope });
299
+ openProviderDialog({
300
+ mode: "manage",
301
+ provider: row.provider,
302
+ scope: row.key.scope,
303
+ });
244
304
 
245
305
  return (
246
306
  <TooltipProvider delayDuration={200}>
@@ -251,7 +311,7 @@ export default function ModelSettingsPage(_props: SettingsPageProps) {
251
311
  listing={data}
252
312
  canAdd={canAdd}
253
313
  builder={builder}
254
- onAdd={() => setDialog({ mode: "add" })}
314
+ onAdd={() => openProviderDialog({ mode: "add" })}
255
315
  />
256
316
  </SettingsGroup>
257
317
  ) : data.hasOrganization ? (
@@ -295,12 +355,16 @@ export default function ModelSettingsPage(_props: SettingsPageProps) {
295
355
  </div>
296
356
  <ProviderDialog
297
357
  open={dialog !== null}
358
+ trackingFlow={dialog?.trackingFlow ?? "settings"}
298
359
  onOpenChange={(open) => {
299
360
  if (!open) setDialog(null);
300
361
  }}
301
- mode={dialog?.mode ?? "add"}
302
- {...(dialog?.mode === "manage"
303
- ? { provider: dialog.provider, scope: dialog.scope }
362
+ mode={dialog?.dialog.mode ?? "add"}
363
+ {...(dialog?.dialog.mode === "manage"
364
+ ? {
365
+ provider: dialog.dialog.provider,
366
+ scope: dialog.dialog.scope,
367
+ }
304
368
  : {})}
305
369
  />
306
370
  {removing ? (
@@ -802,7 +866,11 @@ function DefaultModelRow({
802
866
  engine: listing.defaultModel.engine,
803
867
  model:
804
868
  (!preserveCustomModels &&
805
- upgradeModelToLatestSupportedVersion(model, supportedModels)) ||
869
+ upgradeModelForProvider(
870
+ model,
871
+ supportedModels,
872
+ listing.defaultModel.engine,
873
+ )) ||
806
874
  model,
807
875
  };
808
876
  })()
@@ -23,6 +23,12 @@ const keyMock = vi.hoisted(() => ({
23
23
  deleteAgentEngineProviderSettings: vi.fn(),
24
24
  }));
25
25
  const callActionMock = vi.hoisted(() => vi.fn());
26
+ const setupTelemetryMock = vi.hoisted(() => vi.fn());
27
+ const onboardingOutcomeMock = vi.hoisted(() => vi.fn());
28
+ const onboardingAbandonmentRequestMock = vi.hoisted(() => vi.fn());
29
+ const onboardingSetupKindMock = vi.hoisted(() => vi.fn());
30
+ const credentialSaveBoundaryMock = vi.hoisted(() => vi.fn());
31
+ const localEndpointSaveBoundaryMock = vi.hoisted(() => vi.fn());
26
32
 
27
33
  vi.mock("@agent-native/core/client/hooks", () => ({
28
34
  useActionQuery: (name: string) => ({
@@ -42,6 +48,29 @@ vi.mock("@agent-native/core/client/hooks", () => ({
42
48
 
43
49
  vi.mock("@agent-native/core/client/agent-engine-key", () => keyMock);
44
50
 
51
+ vi.mock("@agent-native/core/client/onboarding/use-onboarding", () => ({
52
+ trackOnboardingEvent: setupTelemetryMock,
53
+ requestCustomKeyOnboardingAbandonment: onboardingAbandonmentRequestMock,
54
+ setCustomKeyOnboardingSetupKind: onboardingSetupKindMock,
55
+ trackCustomKeyOnboardingOutcome: onboardingOutcomeMock,
56
+ withCustomKeyOnboardingCredentialSave: async (
57
+ save: () => Promise<unknown>,
58
+ ) => {
59
+ credentialSaveBoundaryMock();
60
+ const result = await save();
61
+ onboardingOutcomeMock("credential_saved");
62
+ return result;
63
+ },
64
+ withCustomKeyOnboardingLocalEndpointSave: async (
65
+ save: () => Promise<unknown>,
66
+ ) => {
67
+ localEndpointSaveBoundaryMock();
68
+ const result = await save();
69
+ onboardingOutcomeMock("local_endpoint_saved");
70
+ return result;
71
+ },
72
+ }));
73
+
45
74
  vi.mock("@agent-native/core/client/org", () => ({
46
75
  useOrg: () => ({ data: { orgName: "Acme" }, isLoading: false }),
47
76
  }));
@@ -187,6 +216,12 @@ describe("ProviderDialog", () => {
187
216
  .mockReset()
188
217
  .mockResolvedValue(undefined);
189
218
  callActionMock.mockReset().mockResolvedValue({});
219
+ setupTelemetryMock.mockReset();
220
+ onboardingOutcomeMock.mockReset();
221
+ onboardingAbandonmentRequestMock.mockReset();
222
+ onboardingSetupKindMock.mockReset();
223
+ credentialSaveBoundaryMock.mockReset();
224
+ localEndpointSaveBoundaryMock.mockReset();
190
225
  container = document.createElement("div");
191
226
  document.body.appendChild(container);
192
227
  root = createRoot(container);
@@ -275,6 +310,54 @@ describe("ProviderDialog", () => {
275
310
  expect(onOpenChange).toHaveBeenCalledWith(false);
276
311
  });
277
312
 
313
+ it("tracks chat setup key entry and save without recording the key", async () => {
314
+ keyMock.fetchProviderModels.mockResolvedValue({
315
+ ok: true,
316
+ provider: "anthropic",
317
+ models: ["claude-sonnet-5"],
318
+ checkedAt: 1,
319
+ });
320
+ render({ provider: "anthropic", trackingFlow: "chat_setup" });
321
+
322
+ typeInto(inputByLabel("API key"), "sk-ant-test-0000");
323
+ await vi.waitFor(() => {
324
+ expect(setupTelemetryMock).toHaveBeenCalledWith(
325
+ "integration_key_entry_started",
326
+ expect.objectContaining({
327
+ flow: "chat_setup",
328
+ app_name: expect.any(String),
329
+ step_id: "connect_ai",
330
+ method_id: "custom_keys",
331
+ action: "enter",
332
+ outcome: "started",
333
+ }),
334
+ );
335
+ expect(setupTelemetryMock).toHaveBeenCalledWith(
336
+ "integration_key_validation_outcome",
337
+ expect.objectContaining({
338
+ flow: "chat_setup",
339
+ action: "validate",
340
+ outcome: "accepted",
341
+ }),
342
+ );
343
+ });
344
+
345
+ await act(async () => button("Add provider").click());
346
+ await vi.waitFor(() => {
347
+ expect(setupTelemetryMock).toHaveBeenCalledWith(
348
+ "integration_key_save_outcome",
349
+ expect.objectContaining({
350
+ flow: "chat_setup",
351
+ action: "save",
352
+ outcome: "saved",
353
+ }),
354
+ );
355
+ });
356
+ expect(JSON.stringify(setupTelemetryMock.mock.calls)).not.toContain(
357
+ "sk-ant-test-0000",
358
+ );
359
+ });
360
+
278
361
  it("keeps Add disabled until the key checks out and keeps a failed save open", async () => {
279
362
  keyMock.fetchProviderModels.mockResolvedValue({
280
363
  ok: true,
@@ -306,6 +389,49 @@ describe("ProviderDialog", () => {
306
389
  expect(onOpenChange).not.toHaveBeenCalledWith(false);
307
390
  });
308
391
 
392
+ it("allows leaving a provider dialog while preserving its pending save outcome", async () => {
393
+ let resolveSave!: () => void;
394
+ keyMock.saveAgentEngineProviderSettings.mockReturnValue(
395
+ new Promise<void>((resolve) => {
396
+ resolveSave = resolve;
397
+ }),
398
+ );
399
+ keyMock.fetchProviderModels.mockResolvedValue({
400
+ ok: true,
401
+ provider: "anthropic",
402
+ models: ["claude-sonnet-5"],
403
+ checkedAt: 1,
404
+ });
405
+ const { onOpenChange } = render({ provider: "anthropic" });
406
+
407
+ typeInto(inputByLabel("API key"), "sk-ant-test-0000");
408
+ await vi.waitFor(() => {
409
+ expect(button("Add provider").disabled).toBe(false);
410
+ });
411
+ await act(async () => button("Add provider").click());
412
+ await vi.waitFor(() => {
413
+ expect(keyMock.saveAgentEngineProviderSettings).toHaveBeenCalled();
414
+ });
415
+
416
+ await act(async () => button("Close").click());
417
+ expect(onOpenChange).toHaveBeenCalledWith(false);
418
+ expect(onboardingAbandonmentRequestMock).toHaveBeenCalledTimes(1);
419
+ expect(
420
+ onboardingOutcomeMock.mock.calls.map(([outcome]) => outcome),
421
+ ).not.toContain("credential_skipped");
422
+
423
+ await act(async () => {
424
+ resolveSave();
425
+ await vi.waitFor(() => expect(onOpenChange).toHaveBeenCalledWith(false));
426
+ });
427
+ const outcomes = onboardingOutcomeMock.mock.calls.map(
428
+ ([outcome]) => outcome,
429
+ );
430
+ expect(outcomes).toContain("credential_saved");
431
+ expect(outcomes).not.toContain("credential_skipped");
432
+ expect(credentialSaveBoundaryMock).toHaveBeenCalledTimes(1);
433
+ });
434
+
309
435
  it("locks members to a personal key", async () => {
310
436
  state.listing = listing({ canManageOrg: false });
311
437
  keyMock.fetchProviderModels.mockResolvedValue({
@@ -387,9 +513,10 @@ describe("ProviderDialog", () => {
387
513
  models: ["llama3.1:latest"],
388
514
  checkedAt: 1,
389
515
  });
390
- render({ provider: "ollama" });
516
+ const { onSaved } = render({ provider: "ollama" });
391
517
  expect(document.body.textContent).toContain("No API key required.");
392
518
  typeInto(inputByLabel("Endpoint URL"), "http://ollama.internal:11434");
519
+ expect(onboardingOutcomeMock).not.toHaveBeenCalled();
393
520
  await vi.waitFor(() => {
394
521
  expect(document.body.textContent).toContain("llama3.1:latest");
395
522
  });
@@ -397,6 +524,36 @@ describe("ProviderDialog", () => {
397
524
  provider: "ollama",
398
525
  baseUrl: "http://ollama.internal:11434",
399
526
  });
527
+ expect(onboardingOutcomeMock).not.toHaveBeenCalled();
528
+
529
+ await act(async () => button("Add provider").click());
530
+ await vi.waitFor(() => expect(onSaved).toHaveBeenCalled());
531
+
532
+ expect(keyMock.saveAgentEngineProviderSettings).toHaveBeenCalledWith({
533
+ provider: "ollama",
534
+ baseUrl: "http://ollama.internal:11434",
535
+ scope: "org",
536
+ });
537
+ expect(credentialSaveBoundaryMock).not.toHaveBeenCalled();
538
+ expect(localEndpointSaveBoundaryMock).toHaveBeenCalledOnce();
539
+ expect(onboardingOutcomeMock).toHaveBeenCalledWith("local_endpoint_saved");
540
+ expect(onboardingOutcomeMock).not.toHaveBeenCalledWith("credential_saved");
541
+ });
542
+
543
+ it("classifies dismissing Ollama setup separately from skipping credentials", () => {
544
+ render({ provider: "ollama" });
545
+
546
+ expect(onboardingSetupKindMock).toHaveBeenCalledWith("local_endpoint");
547
+
548
+ act(() => button("Cancel").click());
549
+
550
+ expect(onboardingOutcomeMock).toHaveBeenCalledExactlyOnceWith(
551
+ "local_endpoint_skipped",
552
+ );
553
+ expect(onboardingOutcomeMock).not.toHaveBeenCalledWith(
554
+ "credential_skipped",
555
+ );
556
+ expect(onboardingAbandonmentRequestMock).not.toHaveBeenCalled();
400
557
  });
401
558
 
402
559
  it("manages a saved key: masked, checked on save, and models only", async () => {
@@ -442,6 +599,40 @@ describe("ProviderDialog", () => {
442
599
  });
443
600
  });
444
601
 
602
+ it("does not count an endpoint-only update as a credential save", async () => {
603
+ state.listing = listing(
604
+ {},
605
+ {
606
+ openai: {
607
+ org: {
608
+ scope: "org",
609
+ masked: "••••9f3a",
610
+ updatedAt: 1,
611
+ endpoint: "https://old.example",
612
+ },
613
+ },
614
+ },
615
+ );
616
+ state.models = models({ openai: { org: ["model-a"] } });
617
+ const { onSaved } = render({
618
+ mode: "manage",
619
+ provider: "openai",
620
+ scope: "org",
621
+ });
622
+
623
+ typeInto(inputByLabel("Endpoint URL"), "https://gateway.example");
624
+ await act(async () => button("Save").click());
625
+ await vi.waitFor(() => expect(onSaved).toHaveBeenCalled());
626
+
627
+ expect(keyMock.saveAgentEngineProviderSettings).toHaveBeenCalledWith({
628
+ provider: "openai",
629
+ baseUrl: "https://gateway.example",
630
+ scope: "org",
631
+ });
632
+ expect(credentialSaveBoundaryMock).not.toHaveBeenCalled();
633
+ expect(onboardingOutcomeMock).not.toHaveBeenCalledWith("credential_saved");
634
+ });
635
+
445
636
  it("asks for a new key when the saved one was rejected", () => {
446
637
  state.listing = listing(
447
638
  {},