@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
@@ -8,8 +8,17 @@ import {
8
8
  getAgentProviderOption,
9
9
  type AgentProviderId,
10
10
  } from "@agent-native/core/client/agent-provider-catalog";
11
+ import { injectedAgentNativeAppId } from "@agent-native/core/client/app-config";
11
12
  import { callAction, useActionQuery } from "@agent-native/core/client/hooks";
12
13
  import { useFormatters, useT } from "@agent-native/core/client/i18n";
14
+ import {
15
+ requestCustomKeyOnboardingAbandonment,
16
+ setCustomKeyOnboardingSetupKind,
17
+ trackOnboardingEvent,
18
+ trackCustomKeyOnboardingOutcome,
19
+ withCustomKeyOnboardingCredentialSave,
20
+ withCustomKeyOnboardingLocalEndpointSave,
21
+ } from "@agent-native/core/client/onboarding/use-onboarding";
13
22
  import { useOrg } from "@agent-native/core/client/org";
14
23
  import { Alert, AlertDescription } from "@agent-native/toolkit/ui/alert";
15
24
  import { Button } from "@agent-native/toolkit/ui/button";
@@ -40,9 +49,17 @@ import {
40
49
  IconServer,
41
50
  } from "@tabler/icons-react";
42
51
  import { useQueryClient } from "@tanstack/react-query";
43
- import { useEffect, useId, useMemo, useRef, useState } from "react";
52
+ import {
53
+ useCallback,
54
+ useEffect,
55
+ useId,
56
+ useMemo,
57
+ useRef,
58
+ useState,
59
+ } from "react";
44
60
 
45
61
  import { BrandLogo } from "../infra/logos.js";
62
+ import { currentTemplateId } from "../shell/app-identity.js";
46
63
  import { WhoField } from "../WhoField.js";
47
64
  import {
48
65
  addDialogChoices,
@@ -82,10 +99,73 @@ const PROVIDER_LOGO_IDS: Record<AgentProviderId, string | null> = {
82
99
  */
83
100
  export type ProviderDialogMode = "add" | "manage" | "add-from-service";
84
101
 
102
+ type ProviderSetupTrackingFlow = "chat_setup" | "settings";
103
+ type ProviderSetupEventName =
104
+ | "integration_key_entry_started"
105
+ | "integration_key_validation_outcome"
106
+ | "integration_key_save_outcome";
107
+ type ProviderSetupOutcome =
108
+ | "started"
109
+ | "accepted"
110
+ | "rejected"
111
+ | "missing_key"
112
+ | "invalid_endpoint"
113
+ | "unreachable"
114
+ | "provider_error"
115
+ | "error"
116
+ | "saved"
117
+ | "failed";
118
+
119
+ function setupTelemetryAppName(): string {
120
+ const appId = injectedAgentNativeAppId() ?? currentTemplateId();
121
+ return appId && /^[a-z0-9][a-z0-9-]{0,63}$/.test(appId)
122
+ ? appId.replace(/^agent-native-/, "") || "framework"
123
+ : "framework";
124
+ }
125
+
126
+ function trackProviderSetupEvent(
127
+ flow: ProviderSetupTrackingFlow,
128
+ eventName: ProviderSetupEventName,
129
+ action: "enter" | "validate" | "save",
130
+ outcome: ProviderSetupOutcome,
131
+ ): void {
132
+ trackOnboardingEvent(eventName, {
133
+ flow,
134
+ app_name: setupTelemetryAppName(),
135
+ step_id: "connect_ai",
136
+ method_id: "custom_keys",
137
+ action,
138
+ outcome,
139
+ });
140
+ }
141
+
142
+ function providerCheckOutcome(
143
+ result: ProviderModelsCheck,
144
+ ): ProviderSetupOutcome {
145
+ if (result.ok) return "accepted";
146
+ switch (result.code) {
147
+ case "rejected":
148
+ case "wrong-provider":
149
+ return "rejected";
150
+ case "missing-key":
151
+ return "missing_key";
152
+ case "invalid-endpoint":
153
+ return "invalid_endpoint";
154
+ case "unreachable":
155
+ return "unreachable";
156
+ case "provider-error":
157
+ return "provider_error";
158
+ default:
159
+ return "error";
160
+ }
161
+ }
162
+
85
163
  export interface ProviderDialogProps {
86
164
  open: boolean;
87
165
  onOpenChange: (open: boolean) => void;
88
166
  mode: ProviderDialogMode;
167
+ /** Preserve the chat setup handoff through the first provider dialog. */
168
+ trackingFlow?: ProviderSetupTrackingFlow;
89
169
  /** Required for `manage` and `add-from-service`; the first choice for `add`. */
90
170
  provider?: AgentProviderId;
91
171
  /** `manage`: which saved key, the personal (`user`) or organization (`org`) one. */
@@ -134,14 +214,58 @@ function unique(models: readonly string[]): string[] {
134
214
  * the checked models in one step.
135
215
  */
136
216
  export function ProviderDialog(props: ProviderDialogProps) {
217
+ const savePending = useRef(false);
218
+ const selectedProvider = useRef<AgentProviderId>(
219
+ props.provider ?? props.providers?.[0] ?? "anthropic",
220
+ );
221
+ const reportSelectedProvider = useCallback((provider: AgentProviderId) => {
222
+ selectedProvider.current = provider;
223
+ setCustomKeyOnboardingSetupKind(
224
+ provider === "ollama" ? "local_endpoint" : "credential",
225
+ );
226
+ }, []);
227
+ const dismiss = () => {
228
+ if (selectedProvider.current === "ollama" && !savePending.current) {
229
+ trackCustomKeyOnboardingOutcome("local_endpoint_skipped");
230
+ } else if (savePending.current) {
231
+ requestCustomKeyOnboardingAbandonment();
232
+ } else {
233
+ trackCustomKeyOnboardingOutcome("credential_skipped");
234
+ }
235
+ props.onOpenChange(false);
236
+ };
237
+
137
238
  return (
138
- <Dialog open={props.open} onOpenChange={props.onOpenChange}>
139
- {props.open ? <ProviderDialogContent {...props} /> : null}
239
+ <Dialog
240
+ open={props.open}
241
+ onOpenChange={(open) => {
242
+ if (open) props.onOpenChange(true);
243
+ else dismiss();
244
+ }}
245
+ >
246
+ {props.open ? (
247
+ <ProviderDialogContent
248
+ {...props}
249
+ onDismiss={dismiss}
250
+ onProviderChange={reportSelectedProvider}
251
+ onSavingChange={(saving) => {
252
+ savePending.current = saving;
253
+ }}
254
+ />
255
+ ) : null}
140
256
  </Dialog>
141
257
  );
142
258
  }
143
259
 
144
- function ProviderDialogContent(props: ProviderDialogProps) {
260
+ interface ProviderDialogInternalProps {
261
+ onDismiss: () => void;
262
+ onProviderChange: (provider: AgentProviderId) => void;
263
+ onSavingChange: (saving: boolean) => void;
264
+ }
265
+
266
+ function ProviderDialogContent(
267
+ props: ProviderDialogProps & ProviderDialogInternalProps,
268
+ ) {
145
269
  const t = useT();
146
270
  const listing = useActionQuery<ModelProvidersListing>(
147
271
  "list-model-providers" as never,
@@ -209,7 +333,7 @@ function ProviderDialogContent(props: ProviderDialogProps) {
209
333
  );
210
334
  }
211
335
 
212
- interface FormProps extends ProviderDialogProps {
336
+ interface FormProps extends ProviderDialogProps, ProviderDialogInternalProps {
213
337
  title: string;
214
338
  listing: ModelProvidersListing;
215
339
  models: ProviderModelsRead;
@@ -217,11 +341,15 @@ interface FormProps extends ProviderDialogProps {
217
341
 
218
342
  function ProviderDialogForm({
219
343
  mode,
344
+ trackingFlow = "settings",
220
345
  provider: requestedProvider,
221
346
  scope: requestedScope,
222
347
  serviceLabel,
223
348
  providers: providerChoices,
224
349
  onOpenChange,
350
+ onDismiss,
351
+ onProviderChange,
352
+ onSavingChange,
225
353
  onSaved,
226
354
  onRemoved,
227
355
  title,
@@ -286,6 +414,7 @@ function ProviderDialogForm({
286
414
  );
287
415
  const [keyValue, setKeyValue] = useState("");
288
416
  const [keyError, setKeyError] = useState(false);
417
+ const keyEntryTrackedRef = useRef(false);
289
418
  const savedEndpoint = isOpenAi ? (existing?.endpoint ?? "") : "";
290
419
  const [endpointOpen, setEndpointOpen] = useState(!!savedEndpoint);
291
420
  const [endpoint, setEndpoint] = useState(savedEndpoint);
@@ -313,9 +442,22 @@ function ProviderDialogForm({
313
442
  const requestRef = useRef(0);
314
443
  const fromService = mode === "add-from-service";
315
444
 
445
+ useEffect(() => {
446
+ onProviderChange(provider);
447
+ }, [onProviderChange, provider]);
448
+
316
449
  // A pasted key (or Ollama endpoint) is checked as it's entered, by asking
317
450
  // the provider which models it reaches.
318
451
  useEffect(() => {
452
+ if (keyValue.trim() && !keyEntryTrackedRef.current) {
453
+ keyEntryTrackedRef.current = true;
454
+ trackProviderSetupEvent(
455
+ trackingFlow,
456
+ "integration_key_entry_started",
457
+ "enter",
458
+ "started",
459
+ );
460
+ }
319
461
  if (!replacing) return;
320
462
  const value = keyValue.trim();
321
463
  const request = ++requestRef.current;
@@ -336,7 +478,16 @@ function ProviderDialogForm({
336
478
  .then((result) => {
337
479
  if (request !== requestRef.current) return;
338
480
  setCheck(toCheckState(result));
481
+ trackProviderSetupEvent(
482
+ trackingFlow,
483
+ "integration_key_validation_outcome",
484
+ "validate",
485
+ providerCheckOutcome(result),
486
+ );
339
487
  if (!result.ok) return;
488
+ if (provider !== "ollama") {
489
+ trackCustomKeyOnboardingOutcome("credential_validated");
490
+ }
340
491
  setChecked((previous) => {
341
492
  const kept = previous.filter((model) =>
342
493
  result.models.includes(model),
@@ -351,6 +502,12 @@ function ProviderDialogForm({
351
502
  state: "error",
352
503
  message: err instanceof Error ? err.message : String(err),
353
504
  });
505
+ trackProviderSetupEvent(
506
+ trackingFlow,
507
+ "integration_key_validation_outcome",
508
+ "validate",
509
+ "error",
510
+ );
354
511
  });
355
512
  }, CHECK_DEBOUNCE_MS);
356
513
  return () => window.clearTimeout(timer);
@@ -362,6 +519,7 @@ function ProviderDialogForm({
362
519
  replacing,
363
520
  models,
364
521
  fromService,
522
+ trackingFlow,
365
523
  ]);
366
524
 
367
525
  // Models checked when the dialog opened stay listed after a check, so one
@@ -392,6 +550,7 @@ function ProviderDialogForm({
392
550
  addChoices.find((choice) => choice.provider === next)?.replaces ?? null;
393
551
  const nextEndpoint = next === "openai" ? (target?.endpoint ?? "") : "";
394
552
  setProvider(next);
553
+ onProviderChange(next);
395
554
  setKeyValue((value) => (keepValue ? value : ""));
396
555
  setKeyError(false);
397
556
  setChecked(
@@ -411,10 +570,22 @@ function ProviderDialogForm({
411
570
  if (result.ok) {
412
571
  setCheck(toCheckState(result));
413
572
  setRecheck({ checkedAt: result.checkedAt });
573
+ trackProviderSetupEvent(
574
+ trackingFlow,
575
+ "integration_key_validation_outcome",
576
+ "validate",
577
+ providerCheckOutcome(result),
578
+ );
414
579
  return;
415
580
  }
416
581
  setRecheck("idle");
417
582
  setCheck(toCheckState(result));
583
+ trackProviderSetupEvent(
584
+ trackingFlow,
585
+ "integration_key_validation_outcome",
586
+ "validate",
587
+ providerCheckOutcome(result),
588
+ );
418
589
  if (result.code === "rejected" || result.code === "wrong-provider") {
419
590
  setSavedRejected(true);
420
591
  setReplacing(true);
@@ -425,6 +596,12 @@ function ProviderDialogForm({
425
596
  .catch((err: unknown) => {
426
597
  setRecheck("idle");
427
598
  setError(err instanceof Error ? err.message : String(err));
599
+ trackProviderSetupEvent(
600
+ trackingFlow,
601
+ "integration_key_validation_outcome",
602
+ "validate",
603
+ "error",
604
+ );
428
605
  });
429
606
  };
430
607
 
@@ -447,26 +624,39 @@ function ProviderDialogForm({
447
624
  mode !== "manage" || !sameModels(checked, initialSelection ?? []);
448
625
 
449
626
  setSaving(true);
450
- let keySaved = false;
627
+ onSavingChange(true);
628
+ let settingsSaved = false;
451
629
  try {
452
630
  if (replacing) {
453
- await saveAgentEngineProviderSettings({
454
- provider,
455
- ...(isOllama ? { baseUrl: value } : { apiKey: value }),
456
- ...(gateway ? { baseUrl: gateway } : {}),
457
- ...(isOpenAi && !gateway && existing?.endpoint
458
- ? { clearBaseUrl: true }
459
- : {}),
460
- scope,
461
- });
462
- keySaved = true;
631
+ const saveProviderSettings = () =>
632
+ saveAgentEngineProviderSettings({
633
+ provider,
634
+ ...(isOllama ? { baseUrl: value } : { apiKey: value }),
635
+ ...(gateway ? { baseUrl: gateway } : {}),
636
+ ...(isOpenAi && !gateway && existing?.endpoint
637
+ ? { clearBaseUrl: true }
638
+ : {}),
639
+ scope,
640
+ });
641
+ if (isOllama) {
642
+ await withCustomKeyOnboardingLocalEndpointSave(saveProviderSettings);
643
+ } else {
644
+ await withCustomKeyOnboardingCredentialSave(saveProviderSettings);
645
+ }
646
+ settingsSaved = true;
647
+ trackProviderSetupEvent(
648
+ trackingFlow,
649
+ "integration_key_save_outcome",
650
+ "save",
651
+ "saved",
652
+ );
463
653
  } else if (endpointChanged) {
464
654
  await saveAgentEngineProviderSettings({
465
655
  provider,
466
656
  ...(gateway ? { baseUrl: gateway } : { clearBaseUrl: true }),
467
657
  scope,
468
658
  });
469
- keySaved = true;
659
+ settingsSaved = true;
470
660
  }
471
661
  if (modelsChanged) {
472
662
  await callAction(
@@ -484,12 +674,23 @@ function ProviderDialogForm({
484
674
  onOpenChange(false);
485
675
  } catch (err) {
486
676
  const message = err instanceof Error ? err.message : String(err);
487
- setError(keySaved ? t(`${K}modelsSaveFailed`, { message }) : message);
488
- if (keySaved) {
677
+ if (replacing && !settingsSaved) {
678
+ trackProviderSetupEvent(
679
+ trackingFlow,
680
+ "integration_key_save_outcome",
681
+ "save",
682
+ "failed",
683
+ );
684
+ }
685
+ setError(
686
+ settingsSaved ? t(`${K}modelsSaveFailed`, { message }) : message,
687
+ );
688
+ if (settingsSaved) {
489
689
  void queryClient.invalidateQueries({ queryKey: ["action"] });
490
690
  }
491
691
  } finally {
492
692
  setSaving(false);
693
+ onSavingChange(false);
493
694
  }
494
695
  };
495
696
 
@@ -508,11 +709,7 @@ function ProviderDialogForm({
508
709
  {t(`${K}restricted`)}
509
710
  </p>
510
711
  <DialogFooter>
511
- <Button
512
- type="button"
513
- variant="secondary"
514
- onClick={() => onOpenChange(false)}
515
- >
712
+ <Button type="button" variant="secondary" onClick={onDismiss}>
516
713
  {t(`${K}cancel`)}
517
714
  </Button>
518
715
  </DialogFooter>
@@ -607,6 +804,9 @@ function ProviderDialogForm({
607
804
  onChange={(event) => {
608
805
  setKeyValue(event.target.value);
609
806
  setKeyError(false);
807
+ if (event.target.value.trim() && !isOllama) {
808
+ trackCustomKeyOnboardingOutcome("credential_entry_started");
809
+ }
610
810
  }}
611
811
  />
612
812
  ) : (
@@ -762,11 +962,7 @@ function ProviderDialogForm({
762
962
  {t(`${K}removeProvider`)}
763
963
  </Button>
764
964
  ) : null}
765
- <Button
766
- type="button"
767
- variant="secondary"
768
- onClick={() => onOpenChange(false)}
769
- >
965
+ <Button type="button" variant="secondary" onClick={onDismiss}>
770
966
  {t(`${K}cancel`)}
771
967
  </Button>
772
968
  <Button type="submit" disabled={saving || !ready}>
@@ -12,6 +12,7 @@ import {
12
12
  isBuilderConnectComplete,
13
13
  useBuilderStatus,
14
14
  useBuilderConnectFlow,
15
+ requestBuilderAccountActivation,
15
16
  withBuilderConnectTrackingParams,
16
17
  type BuilderConnectionScope,
17
18
  } from "./useBuilderStatus.js";
@@ -292,6 +293,7 @@ describe("useBuilderStatus", () => {
292
293
  afterEach(() => {
293
294
  act(() => root.unmount());
294
295
  container.remove();
296
+ Reflect.deleteProperty(window.location, "hostname");
295
297
  vi.unstubAllGlobals();
296
298
  });
297
299
 
@@ -498,6 +500,7 @@ describe("useBuilderConnectFlow", () => {
498
500
  afterEach(() => {
499
501
  act(() => root.unmount());
500
502
  container.remove();
503
+ Reflect.deleteProperty(window.location, "hostname");
501
504
  vi.useRealTimers();
502
505
  vi.unstubAllGlobals();
503
506
  });
@@ -776,6 +779,12 @@ describe("useBuilderConnectFlow", () => {
776
779
 
777
780
  for (const host of ["desktop", "embedded", "browser"] as const) {
778
781
  it(`creates the account with one request and no popup (${host})`, async () => {
782
+ Object.defineProperty(window.location, "hostname", {
783
+ configurable: true,
784
+ value: "design.custom-domain.com",
785
+ });
786
+ vi.stubGlobal("__AGENT_NATIVE_APP_ID__", "agent-native-clips");
787
+ vi.stubGlobal("__AGENT_NATIVE_TEMPLATE__", "clips");
779
788
  setUserAgent(
780
789
  host === "desktop"
781
790
  ? "Mozilla/5.0 AgentNativeDesktop/1.0"
@@ -809,11 +818,48 @@ describe("useBuilderConnectFlow", () => {
809
818
  expect(posts[0]!.url.searchParams.get("agentNativeFlow")).toBe(
810
819
  "connect_llm",
811
820
  );
821
+ expect(posts[0]!.url.searchParams.get("agentNativeApp")).toBe(
822
+ "agent-native-clips",
823
+ );
824
+ expect(posts[0]!.url.searchParams.get("agentNativeTemplate")).toBe(
825
+ "clips",
826
+ );
812
827
  expect(container.textContent).toContain("configured idle resolved");
813
828
  expect(onConnected).toHaveBeenCalledOnce();
814
829
  });
815
830
  }
816
831
 
832
+ it.each([
833
+ "localhost",
834
+ "127.0.0.1",
835
+ "www.agent-native.com",
836
+ "design.custom-domain.com",
837
+ ])("does not infer signup attribution from %s", async (hostname) => {
838
+ Object.defineProperty(window.location, "hostname", {
839
+ configurable: true,
840
+ value: hostname,
841
+ });
842
+ const posts = mockActivation(() => activationResponse(200, { ok: true }));
843
+
844
+ await requestBuilderAccountActivation({ provisioningToken });
845
+
846
+ expect(posts).toHaveLength(1);
847
+ expect(posts[0]!.url.searchParams.get("agentNativeApp")).toBeNull();
848
+ expect(posts[0]!.url.searchParams.get("agentNativeTemplate")).toBeNull();
849
+ });
850
+
851
+ it("does not infer a template from the injected app ID", async () => {
852
+ vi.stubGlobal("__AGENT_NATIVE_APP_ID__", "private-workspace");
853
+ const posts = mockActivation(() => activationResponse(200, { ok: true }));
854
+
855
+ await requestBuilderAccountActivation({ provisioningToken });
856
+
857
+ expect(posts[0]!.url.searchParams.get("agentNativeApp")).toBe(
858
+ "private-workspace",
859
+ );
860
+ expect(posts[0]!.url.searchParams.get("agentNativeTemplate")).toBeNull();
861
+ });
862
+
817
863
  it("activates an account for the organization's connection", async () => {
818
864
  const posts = mockActivation(() =>
819
865
  activationResponse(200, { ok: true, scope: "org" }),
@@ -2604,6 +2650,9 @@ describe("useBuilderConnectFlow", () => {
2604
2650
 
2605
2651
  expect(container.textContent).toContain("not-configured idle");
2606
2652
  expect(container.textContent).toContain("Didn't hear back from Builder");
2653
+ expect(
2654
+ container.querySelector('[data-testid="error-kind"]')?.textContent,
2655
+ ).toBe("connection");
2607
2656
  });
2608
2657
 
2609
2658
  it("clears a stale status-read error after a readable incomplete callback status", async () => {
@@ -1,5 +1,6 @@
1
1
  import { trackEvent } from "@agent-native/core/client/analytics";
2
2
  import { agentNativePath } from "@agent-native/core/client/api-path";
3
+ import { injectedAgentNativeAppId } from "@agent-native/core/client/app-config";
3
4
  import { getCallbackOrigin } from "@agent-native/core/client/frame";
4
5
  import { scheduleAfterPaint } from "@agent-native/core/client/hooks";
5
6
  import { usePollLoop } from "@agent-native/core/client/hooks";
@@ -297,6 +298,7 @@ const BUILDER_AGENT_NATIVE_FLOW_PARAM = "agentNativeFlow";
297
298
  const BUILDER_AGENT_NATIVE_CONNECT_SOURCE_PARAM = "agentNativeConnectSource";
298
299
  const BUILDER_AGENT_NATIVE_APP_PARAM = "agentNativeApp";
299
300
  const BUILDER_AGENT_NATIVE_TEMPLATE_PARAM = "agentNativeTemplate";
301
+ declare const __AGENT_NATIVE_TEMPLATE__: string | undefined;
300
302
  const BUILDER_SIGNUP_SOURCE = "agent-native";
301
303
  const STATUS_CONNECT_URL_TTL_MS = 9 * 60 * 1000;
302
304
 
@@ -354,18 +356,16 @@ function inferBuilderConnectTrackingIdentity(options: {
354
356
  const app =
355
357
  normalizeTrackingSlug(options.app) ??
356
358
  normalizeTrackingSlug(env.VITE_AGENT_NATIVE_APP) ??
357
- (typeof window !== "undefined"
358
- ? normalizeTrackingSlug(window.location.hostname.split(".")[0])
359
- : null);
359
+ normalizeTrackingSlug(injectedAgentNativeAppId());
360
360
  const template =
361
361
  normalizeTrackingSlug(options.template) ??
362
362
  normalizeTrackingSlug(env.VITE_AGENT_NATIVE_TEMPLATE) ??
363
363
  normalizeTrackingSlug(env.VITE_APP_TEMPLATE) ??
364
- (app?.startsWith("agent-native-")
365
- ? normalizeTrackingSlug(app.slice("agent-native-".length))
366
- : app && app !== "localhost"
367
- ? app
368
- : null);
364
+ normalizeTrackingSlug(
365
+ typeof __AGENT_NATIVE_TEMPLATE__ === "string"
366
+ ? __AGENT_NATIVE_TEMPLATE__
367
+ : undefined,
368
+ );
369
369
 
370
370
  return { app, template };
371
371
  }
@@ -722,9 +722,15 @@ export async function requestBuilderAccountActivation({
722
722
  signal?: AbortSignal;
723
723
  }): Promise<BuilderAccountActivationResult> {
724
724
  const origin = getCallbackOrigin() || window.location.origin;
725
+ const { app, template } = inferBuilderConnectTrackingIdentity({});
725
726
  const url = withBuilderConnectTrackingParams(
726
727
  new URL(agentNativePath("/_agent-native/builder/provision"), origin).href,
727
- { source, flow },
728
+ {
729
+ source,
730
+ flow,
731
+ app: app ?? undefined,
732
+ template: template ?? undefined,
733
+ },
728
734
  );
729
735
  let response: Response;
730
736
  try {
@@ -1812,7 +1818,7 @@ export function useBuilderConnectFlow(
1812
1818
  if (s) {
1813
1819
  setError(
1814
1820
  "Didn't hear back from Builder in 5 minutes. Allow popups and try again.",
1815
- "launch",
1821
+ "connection",
1816
1822
  );
1817
1823
  }
1818
1824
  }