@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
@@ -5,6 +5,7 @@ import { registerFirstRunOnboardingExtension } from "@agent-native/core/client/o
5
5
  import { TooltipProvider } from "@agent-native/toolkit/ui/tooltip";
6
6
  import React, { act } from "react";
7
7
  import { createRoot, type Root } from "react-dom/client";
8
+ import { MemoryRouter, Route, Routes } from "react-router";
8
9
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
9
10
 
10
11
  import { createToolkitI18nCatalog } from "../i18n.js";
@@ -31,7 +32,10 @@ const mocks = vi.hoisted(() => ({
31
32
  completeFirstRun: vi.fn(),
32
33
  useBuilderConnectFlow: vi.fn(),
33
34
  routePathname: "/",
35
+ useActualRouter: false,
36
+ navigate: vi.fn(),
34
37
  trackOnboardingEvent: vi.fn(),
38
+ setCustomKeyOnboardingAttempt: vi.fn(),
35
39
  useOnboarding: vi.fn(),
36
40
  useOnboardingPreviewMode: vi.fn(),
37
41
  useOnboardingPreviewStep: vi.fn(),
@@ -41,7 +45,12 @@ vi.mock("react-router", async (importOriginal) => {
41
45
  const actual = await importOriginal<typeof import("react-router")>();
42
46
  return {
43
47
  ...actual,
44
- useLocation: () => ({ pathname: mocks.routePathname }),
48
+ useLocation: () =>
49
+ mocks.useActualRouter
50
+ ? actual.useLocation()
51
+ : { pathname: mocks.routePathname },
52
+ useNavigate: () =>
53
+ mocks.useActualRouter ? actual.useNavigate() : mocks.navigate,
45
54
  };
46
55
  });
47
56
 
@@ -50,6 +59,8 @@ vi.mock("@agent-native/core/client/feature-flags/use-feature-flag", () => ({
50
59
  }));
51
60
 
52
61
  vi.mock("@agent-native/core/client/onboarding/use-onboarding", () => ({
62
+ createOnboardingCorrelationId: () => "test-onboarding-correlation-id",
63
+ setCustomKeyOnboardingAttempt: mocks.setCustomKeyOnboardingAttempt,
53
64
  trackOnboardingEvent: mocks.trackOnboardingEvent,
54
65
  useOnboarding: mocks.useOnboarding,
55
66
  }));
@@ -77,9 +88,13 @@ describe("FirstRunOnboarding", () => {
77
88
  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
78
89
  mocks.completeFirstRun.mockReset();
79
90
  mocks.routePathname = "/";
91
+ mocks.useActualRouter = false;
92
+ mocks.navigate.mockReset();
80
93
  mocks.completeFirstRun.mockResolvedValue(undefined);
81
94
  mocks.useBuilderConnectFlow.mockReset();
82
95
  mocks.trackOnboardingEvent.mockReset();
96
+ mocks.setCustomKeyOnboardingAttempt.mockReset();
97
+ mocks.setCustomKeyOnboardingAttempt.mockResolvedValue("stored");
83
98
  mocks.useOnboarding.mockReset();
84
99
  mocks.useOnboardingPreviewMode.mockReset();
85
100
  mocks.useOnboardingPreviewStep.mockReset();
@@ -768,6 +783,11 @@ describe("FirstRunOnboarding", () => {
768
783
  .querySelector("[data-testid='first-run-role-skip']")
769
784
  ?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
770
785
  });
786
+ expect(
787
+ document.body.querySelector(
788
+ '[data-testid="first-run-builder-create-account"]',
789
+ )?.textContent,
790
+ ).toBe("Use Builder.io");
771
791
  act(() => {
772
792
  document.body
773
793
  .querySelector('[data-testid="first-run-builder-create-account"]')
@@ -1325,6 +1345,7 @@ describe("FirstRunOnboarding", () => {
1325
1345
  );
1326
1346
  const attemptId = (methodClick?.[1] as Record<string, unknown>)
1327
1347
  ?.onboarding_attempt_id;
1348
+ expect(mocks.setCustomKeyOnboardingAttempt).toHaveBeenCalledWith(attemptId);
1328
1349
  expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
1329
1350
  "onboarding_method_outcome",
1330
1351
  expect.objectContaining({
@@ -1336,6 +1357,61 @@ describe("FirstRunOnboarding", () => {
1336
1357
  window.history.replaceState(null, "", "/");
1337
1358
  });
1338
1359
 
1360
+ it.each(["no_session", "unavailable"] as const)(
1361
+ "navigates while custom-key attempt storage resolves and reports %s",
1362
+ async (status) => {
1363
+ let resolveAttemptStorage:
1364
+ | ((status: "stored" | "no_session" | "unavailable") => void)
1365
+ | undefined;
1366
+ mocks.setCustomKeyOnboardingAttempt.mockReturnValue(
1367
+ new Promise((resolve) => {
1368
+ resolveAttemptStorage = resolve;
1369
+ }),
1370
+ );
1371
+
1372
+ act(() => {
1373
+ root.render(
1374
+ <TooltipProvider>
1375
+ <FirstRunOnboarding />
1376
+ </TooltipProvider>,
1377
+ );
1378
+ });
1379
+ act(() => {
1380
+ document.body
1381
+ .querySelector("[data-testid='first-run-role-skip']")
1382
+ ?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
1383
+ });
1384
+
1385
+ await act(async () => {
1386
+ document.body
1387
+ .querySelector("[data-testid='first-run-open-key-settings']")
1388
+ ?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
1389
+ await Promise.resolve();
1390
+ });
1391
+
1392
+ expect(window.location.pathname).toBe(
1393
+ manualSetupSettingsRoute({ redesign: true }),
1394
+ );
1395
+ expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
1396
+ "onboarding_method_outcome",
1397
+ expect.objectContaining({
1398
+ method_id: "custom_keys",
1399
+ outcome: "settings_opened",
1400
+ }),
1401
+ );
1402
+
1403
+ await act(async () => {
1404
+ resolveAttemptStorage?.(status);
1405
+ await Promise.resolve();
1406
+ });
1407
+ expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
1408
+ "onboarding_correlation_unavailable",
1409
+ expect.objectContaining({ correlation_status: status }),
1410
+ );
1411
+ window.history.replaceState(null, "", "/");
1412
+ },
1413
+ );
1414
+
1339
1415
  it("lets Clips skip provider setup and finish first-run onboarding", async () => {
1340
1416
  let resolveCompletion: (() => void) | undefined;
1341
1417
  mocks.completeFirstRun.mockImplementation(
@@ -1356,6 +1432,7 @@ describe("FirstRunOnboarding", () => {
1356
1432
  completeFirstRun: mocks.completeFirstRun,
1357
1433
  completeFirstRunError: null,
1358
1434
  });
1435
+ mocks.routePathname = "/library";
1359
1436
 
1360
1437
  await act(async () => {
1361
1438
  root.render(
@@ -1399,7 +1476,8 @@ describe("FirstRunOnboarding", () => {
1399
1476
  });
1400
1477
 
1401
1478
  expect(mocks.completeFirstRun).toHaveBeenCalledOnce();
1402
- expect(window.location.pathname).toBe("/");
1479
+ expect(mocks.navigate).toHaveBeenCalledOnce();
1480
+ expect(mocks.navigate).toHaveBeenCalledWith("/record", { replace: true });
1403
1481
  expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
1404
1482
  "onboarding_step_skipped",
1405
1483
  expect.objectContaining({
@@ -1414,6 +1492,63 @@ describe("FirstRunOnboarding", () => {
1414
1492
  );
1415
1493
  });
1416
1494
 
1495
+ it("navigates to Clips recording inside the router basename", async () => {
1496
+ mocks.useActualRouter = true;
1497
+ mocks.useOnboardingPreviewMode.mockReturnValue(true);
1498
+ mocks.useOnboardingPreviewStep.mockReturnValue("choice");
1499
+ mocks.useOnboarding.mockReturnValue({
1500
+ firstRun: true,
1501
+ loading: false,
1502
+ error: null,
1503
+ profile: {
1504
+ appId: "clips",
1505
+ appName: "Clips",
1506
+ capabilities: [],
1507
+ },
1508
+ completeFirstRun: mocks.completeFirstRun,
1509
+ completeFirstRunError: null,
1510
+ });
1511
+ vi.stubEnv("VITE_APP_BASE_PATH", "/clips");
1512
+ window.history.replaceState(
1513
+ null,
1514
+ "",
1515
+ "/clips/library?onboarding=preview&step=choice",
1516
+ );
1517
+
1518
+ await act(async () => {
1519
+ root.render(
1520
+ <MemoryRouter
1521
+ basename="/clips"
1522
+ initialEntries={["/clips/library?onboarding=preview&step=choice"]}
1523
+ >
1524
+ <Routes>
1525
+ <Route path="/library" element={<FirstRunOnboarding />} />
1526
+ <Route
1527
+ path="/record"
1528
+ element={<div data-testid="clips-record-route" />}
1529
+ />
1530
+ <Route path="*" element={<div data-testid="not-found-route" />} />
1531
+ </Routes>
1532
+ </MemoryRouter>,
1533
+ );
1534
+ });
1535
+
1536
+ await act(async () => {
1537
+ document.body
1538
+ .querySelector("[data-testid='first-run-setup-skip']")
1539
+ ?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
1540
+ await Promise.resolve();
1541
+ await Promise.resolve();
1542
+ });
1543
+
1544
+ expect(
1545
+ document.body.querySelector('[data-testid="clips-record-route"]'),
1546
+ ).not.toBeNull();
1547
+ expect(
1548
+ document.body.querySelector('[data-testid="not-found-route"]'),
1549
+ ).toBeNull();
1550
+ });
1551
+
1417
1552
  it("does not start duplicate manual setup attempts while completion is pending", async () => {
1418
1553
  let resolveCompletion: (() => void) | undefined;
1419
1554
  mocks.completeFirstRun.mockImplementation(
@@ -1619,6 +1754,56 @@ describe("FirstRunOnboarding", () => {
1619
1754
  );
1620
1755
  });
1621
1756
 
1757
+ it("does not reuse a failed skip redirect for manual setup completion", async () => {
1758
+ mocks.completeFirstRun
1759
+ .mockRejectedValueOnce(new Error("first-run completion failed: 500"))
1760
+ .mockResolvedValueOnce(undefined);
1761
+ mocks.useOnboarding.mockReturnValue({
1762
+ firstRun: true,
1763
+ loading: false,
1764
+ error: null,
1765
+ profile: {
1766
+ appId: "clips",
1767
+ appName: "Clips",
1768
+ capabilities: [],
1769
+ },
1770
+ completeFirstRun: mocks.completeFirstRun,
1771
+ completeFirstRunError: "first-run completion failed: 500",
1772
+ });
1773
+
1774
+ await act(async () => {
1775
+ root.render(
1776
+ <TooltipProvider>
1777
+ <FirstRunOnboarding />
1778
+ </TooltipProvider>,
1779
+ );
1780
+ });
1781
+ act(() => {
1782
+ document.body
1783
+ .querySelector("[data-testid='first-run-role-skip']")
1784
+ ?.click();
1785
+ });
1786
+ await act(async () => {
1787
+ document.body
1788
+ .querySelector("[data-testid='first-run-setup-skip']")
1789
+ ?.click();
1790
+ await Promise.resolve();
1791
+ });
1792
+
1793
+ expect(mocks.completeFirstRun).toHaveBeenCalledOnce();
1794
+ await act(async () => {
1795
+ document.body
1796
+ .querySelector("[data-testid='first-run-open-key-settings']")
1797
+ ?.click();
1798
+ await Promise.resolve();
1799
+ await Promise.resolve();
1800
+ });
1801
+
1802
+ expect(mocks.completeFirstRun).toHaveBeenCalledTimes(2);
1803
+ expect(mocks.navigate).not.toHaveBeenCalled();
1804
+ expect(window.location.pathname).toBe("/settings/model");
1805
+ });
1806
+
1622
1807
  it("preserves the completed step when first-run completion succeeds on retry", async () => {
1623
1808
  let completionResult: boolean | void;
1624
1809
  mocks.completeFirstRun
@@ -1983,4 +2168,69 @@ describe("FirstRunOnboarding", () => {
1983
2168
  ),
1984
2169
  ).toBeNull();
1985
2170
  });
2171
+
2172
+ it("preserves a skip redirect when the diverted completion is retried", async () => {
2173
+ mocks.completeFirstRun
2174
+ .mockRejectedValueOnce(new Error("first-run completion failed: 500"))
2175
+ .mockResolvedValueOnce(undefined);
2176
+ mocks.useOnboarding.mockReturnValue({
2177
+ firstRun: true,
2178
+ loading: false,
2179
+ error: null,
2180
+ profile: {
2181
+ appId: "clips",
2182
+ appName: "Clips",
2183
+ capabilities: [],
2184
+ },
2185
+ completeFirstRun: mocks.completeFirstRun,
2186
+ completeFirstRunError: "first-run completion failed: 500",
2187
+ });
2188
+
2189
+ await act(async () => {
2190
+ root.render(
2191
+ <TooltipProvider>
2192
+ <FirstRunOnboarding />
2193
+ </TooltipProvider>,
2194
+ );
2195
+ });
2196
+ act(() => {
2197
+ document.body
2198
+ .querySelector("[data-testid='first-run-role-skip']")
2199
+ ?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
2200
+ });
2201
+ await act(async () => {
2202
+ document.body
2203
+ .querySelector("[data-testid='first-run-setup-skip']")
2204
+ ?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
2205
+ await Promise.resolve();
2206
+ });
2207
+
2208
+ const extensionSkip = [...document.body.querySelectorAll("button")].find(
2209
+ (button) => button.textContent === "Extension Skip",
2210
+ );
2211
+ if (extensionSkip) {
2212
+ await act(async () => {
2213
+ extensionSkip.click();
2214
+ await Promise.resolve();
2215
+ });
2216
+ }
2217
+
2218
+ expect(mocks.completeFirstRun).toHaveBeenCalledTimes(1);
2219
+ expect(document.body.textContent).toContain(
2220
+ "first-run completion failed: 500",
2221
+ );
2222
+ await act(async () => {
2223
+ [...document.body.querySelectorAll("button")]
2224
+ .find((button) => button.textContent === "Try again")
2225
+ ?.click();
2226
+ await Promise.resolve();
2227
+ await Promise.resolve();
2228
+ });
2229
+
2230
+ expect(mocks.completeFirstRun).toHaveBeenCalledTimes(2);
2231
+ expect(mocks.navigate).toHaveBeenCalledOnce();
2232
+ expect(mocks.navigate).toHaveBeenCalledWith("/record", {
2233
+ replace: true,
2234
+ });
2235
+ });
1986
2236
  });
@@ -7,6 +7,8 @@ import {
7
7
  } from "@agent-native/core/client/onboarding/first-run-registry";
8
8
  import { saveFirstRunOnboardingRole } from "@agent-native/core/client/onboarding/first-run-status";
9
9
  import {
10
+ createOnboardingCorrelationId,
11
+ setCustomKeyOnboardingAttempt,
10
12
  trackOnboardingEvent,
11
13
  useOnboarding,
12
14
  } from "@agent-native/core/client/onboarding/use-onboarding";
@@ -37,7 +39,7 @@ import React, {
37
39
  useRef,
38
40
  useState,
39
41
  } from "react";
40
- import { useLocation } from "react-router";
42
+ import { useLocation, useNavigate } from "react-router";
41
43
 
42
44
  import { useBuilderConnectFlow } from "../settings/useBuilderStatus.js";
43
45
  import {
@@ -166,6 +168,7 @@ export function FirstRunOnboarding({
166
168
  }: FirstRunOnboardingProps = {}) {
167
169
  const t = useT();
168
170
  const { pathname } = useLocation();
171
+ const navigate = useNavigate();
169
172
  const previewMode = useOnboardingPreviewMode();
170
173
  const previewStep = useOnboardingPreviewStep();
171
174
  const {
@@ -301,18 +304,21 @@ export function FirstRunOnboarding({
301
304
  const completionAttemptRef = useRef<{
302
305
  screen: FirstRunScreen | null;
303
306
  extensionIndex: number;
307
+ redirect: string | null;
304
308
  } | null>(null);
305
309
  const completionInFlightRef = useRef(false);
310
+ const completionRedirectRef = useRef<string | null>(null);
306
311
  const setupSkipStartedRef = useRef(false);
307
312
  const onboardingTerminalRef = useRef(false);
308
313
  const abandonmentTrackedRef = useRef(false);
309
314
  const setupAttemptRef = useRef<FirstRunSetupAttempt | null>(null);
310
315
  const builderSetupAttemptRef = useRef<FirstRunSetupAttempt | null>(null);
316
+ const stepViewRef = useRef<{ key: string; id: string } | null>(null);
311
317
  const startSetupMethod = useCallback(
312
318
  (methodId: FirstRunSetupMethodId, methodKind: "builder" | "manual") => {
313
319
  if (previewMode || typeof window === "undefined") return null;
314
320
  const attempt = {
315
- id: window.crypto.randomUUID(),
321
+ id: createOnboardingCorrelationId(),
316
322
  methodId,
317
323
  outcomeTracked: false,
318
324
  };
@@ -334,10 +340,14 @@ export function FirstRunOnboarding({
334
340
  async (
335
341
  completedScreen: FirstRunScreen | null,
336
342
  completedExtensionIndex = extensionIndex,
343
+ retryRedirect: string | null = completionRedirectRef.current,
337
344
  ) => {
338
- completionAttemptRef.current = completedScreen
339
- ? { screen: completedScreen, extensionIndex: completedExtensionIndex }
340
- : { screen: null, extensionIndex: completedExtensionIndex };
345
+ completionRedirectRef.current = null;
346
+ completionAttemptRef.current = {
347
+ screen: completedScreen,
348
+ extensionIndex: completedExtensionIndex,
349
+ redirect: retryRedirect,
350
+ };
341
351
  completionInFlightRef.current = true;
342
352
  try {
343
353
  await completeFirstRun();
@@ -346,6 +356,9 @@ export function FirstRunOnboarding({
346
356
  }
347
357
  onboardingTerminalRef.current = true;
348
358
  completionAttemptRef.current = null;
359
+ if (retryRedirect) {
360
+ navigate(retryRedirect, { replace: true });
361
+ }
349
362
  return true;
350
363
  } catch {
351
364
  // coercion-ok: completeFirstRun exposes this failure as the inline retry state.
@@ -354,7 +367,13 @@ export function FirstRunOnboarding({
354
367
  completionInFlightRef.current = false;
355
368
  }
356
369
  },
357
- [completeFirstRun, extensionIndex, trackFirstRunStepCompleted],
370
+ [
371
+ completeFirstRun,
372
+ extensionIndex,
373
+ navigate,
374
+ pathname,
375
+ trackFirstRunStepCompleted,
376
+ ],
358
377
  );
359
378
  useEffect(() => {
360
379
  if (!previewMode && firstRun && !loading && profile) {
@@ -362,7 +381,10 @@ export function FirstRunOnboarding({
362
381
  }
363
382
  }, [firstRun, loading, previewMode, profile]);
364
383
  useEffect(() => {
365
- if (previewMode || !firstRun || loading || !profile) return;
384
+ if (previewMode || !firstRun || loading || !profile) {
385
+ stepViewRef.current = null;
386
+ return;
387
+ }
366
388
  const step = firstRunStepProperties(
367
389
  screen,
368
390
  beforeSetupExtensions,
@@ -371,7 +393,14 @@ export function FirstRunOnboarding({
371
393
  extensionIndex,
372
394
  extensionStepIndex,
373
395
  );
374
- trackOnboardingEvent("onboarding_step_viewed", step);
396
+ const key = [step.step_id, step.extension_id, step.step_index].join(":");
397
+ if (stepViewRef.current?.key !== key) {
398
+ stepViewRef.current = { key, id: createOnboardingCorrelationId() };
399
+ }
400
+ trackOnboardingEvent("onboarding_step_viewed", {
401
+ ...step,
402
+ step_view_id: stepViewRef.current.id,
403
+ });
375
404
  }, [
376
405
  afterSetupExtensions,
377
406
  beforeSetupExtensions,
@@ -498,6 +527,7 @@ export function FirstRunOnboarding({
498
527
  void finishOnboarding(
499
528
  attempt?.screen ?? null,
500
529
  attempt?.extensionIndex ?? extensionIndex,
530
+ attempt?.redirect ?? null,
501
531
  );
502
532
  }, [extensionIndex, finishOnboarding]);
503
533
  const completionErrorProps = {
@@ -581,7 +611,23 @@ export function FirstRunOnboarding({
581
611
  );
582
612
  return;
583
613
  }
614
+ const attemptStorage = attempt
615
+ ? setCustomKeyOnboardingAttempt(attempt.id)
616
+ : null;
584
617
  trackFirstRunSetupOutcome(attempt, "settings_opened");
618
+ if (attempt && attemptStorage) {
619
+ void attemptStorage.then((status) => {
620
+ if (status !== "stored") {
621
+ trackOnboardingEvent("onboarding_correlation_unavailable", {
622
+ flow: "first_run",
623
+ step_id: "choice",
624
+ method_id: "custom_keys",
625
+ onboarding_attempt_id: attempt.id,
626
+ correlation_status: status,
627
+ });
628
+ }
629
+ });
630
+ }
585
631
  if (typeof window === "undefined") return;
586
632
  const search = new URLSearchParams(window.location.search);
587
633
  search.delete(ONBOARDING_PREVIEW_QUERY_PARAM);
@@ -756,7 +802,7 @@ export function FirstRunOnboarding({
756
802
  onClick={() => handleBuilder(true)}
757
803
  disabled={connectFlow.connecting}
758
804
  >
759
- {t("agentChat.onboarding.builderCreateAndActivate")}
805
+ {t("agentChat.onboarding.builderCreateAccount")}
760
806
  </button>
761
807
  )}
762
808
  <button
@@ -839,6 +885,7 @@ export function FirstRunOnboarding({
839
885
  return;
840
886
  setupSkipStartedRef.current = true;
841
887
  trackFirstRunStepSkipped("choice");
888
+ completionRedirectRef.current = "/record";
842
889
  handleFinish(null);
843
890
  }}
844
891
  >
@@ -9,6 +9,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
9
9
 
10
10
  let steps: OnboardingStepStatus[] = [];
11
11
  vi.mock("@agent-native/core/client/onboarding/use-onboarding", () => ({
12
+ createOnboardingCorrelationId: () => "test-onboarding-correlation-id",
12
13
  trackOnboardingEvent: vi.fn(),
13
14
  useOnboarding: () => ({
14
15
  steps,
@@ -2,6 +2,7 @@ import { sendToAgentChat } from "@agent-native/core/client/agent-chat";
2
2
  import { agentNativePath } from "@agent-native/core/client/api-path";
3
3
  import { useT } from "@agent-native/core/client/i18n";
4
4
  import {
5
+ createOnboardingCorrelationId,
5
6
  trackOnboardingEvent,
6
7
  useOnboarding,
7
8
  } from "@agent-native/core/client/onboarding/use-onboarding";
@@ -26,7 +27,7 @@ import {
26
27
  IconKey,
27
28
  IconLoader2,
28
29
  } from "@tabler/icons-react";
29
- import React, { useState, useEffect, useId } from "react";
30
+ import React, { useState, useEffect, useId, useRef } from "react";
30
31
 
31
32
  import {
32
33
  BuilderConnectPopover,
@@ -74,21 +75,32 @@ export function OnboardingPanel({
74
75
  const checklistVisible =
75
76
  !loading && totalCount > 0 && (previewMode || (!dismissed && !allComplete));
76
77
  const [expanded, setExpanded] = useState(true);
78
+ const stepViewRef = useRef<{ key: string; id: string } | null>(null);
77
79
 
78
80
  useEffect(() => {
79
- if (!checklistVisible) return;
81
+ if (!checklistVisible || !expanded) {
82
+ stepViewRef.current = null;
83
+ return;
84
+ }
80
85
  const activeStepIndex = steps.findIndex(
81
86
  (step) => step.id === currentStepId,
82
87
  );
83
88
  const activeStep = steps[activeStepIndex];
84
89
  trackOnboardingEvent("onboarding_started", { flow: "checklist" });
85
90
  if (!activeStep) return;
91
+ if (stepViewRef.current?.key !== activeStep.id) {
92
+ stepViewRef.current = {
93
+ key: activeStep.id,
94
+ id: createOnboardingCorrelationId(),
95
+ };
96
+ }
86
97
  trackOnboardingEvent("onboarding_step_viewed", {
87
98
  flow: "checklist",
88
99
  step_id: activeStep.id,
89
100
  step_index: activeStepIndex,
101
+ step_view_id: stepViewRef.current.id,
90
102
  });
91
- }, [checklistVisible, currentStepId, previewMode, steps]);
103
+ }, [checklistVisible, currentStepId, expanded, previewMode, steps]);
92
104
 
93
105
  if (loading || totalCount === 0) return null;
94
106
  if (!previewMode) {