@agent-native/toolkit 0.201.0 → 0.201.1

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 (109) hide show
  1. package/agent-native.eject.json +1 -0
  2. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  3. package/dist/app/agentkit/react/components.js +21 -13
  4. package/dist/app/agentkit/react/components.js.map +1 -1
  5. package/dist/app/agentkit/react/context.d.ts +15 -1
  6. package/dist/app/agentkit/react/context.d.ts.map +1 -1
  7. package/dist/app/agentkit/react/context.js +30 -4
  8. package/dist/app/agentkit/react/context.js.map +1 -1
  9. package/dist/app/agentkit/react/styles.css +11 -7
  10. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  11. package/dist/app/chat/AgentKitAssistantChat.js +9 -4
  12. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  13. package/dist/app/chat/FileStorageSetupPopover.d.ts.map +1 -1
  14. package/dist/app/chat/FileStorageSetupPopover.js +3 -2
  15. package/dist/app/chat/FileStorageSetupPopover.js.map +1 -1
  16. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  17. package/dist/app/i18n/catalogs/ar-SA.js +11 -5
  18. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  19. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  20. package/dist/app/i18n/catalogs/de-DE.js +11 -5
  21. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  22. package/dist/app/i18n/catalogs/en-US.d.ts +11 -5
  23. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  24. package/dist/app/i18n/catalogs/en-US.js +11 -5
  25. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  26. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  27. package/dist/app/i18n/catalogs/es-ES.js +11 -5
  28. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  29. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  30. package/dist/app/i18n/catalogs/fr-FR.js +11 -5
  31. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  32. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  33. package/dist/app/i18n/catalogs/hi-IN.js +11 -5
  34. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  35. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  36. package/dist/app/i18n/catalogs/ja-JP.js +11 -5
  37. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  38. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  39. package/dist/app/i18n/catalogs/ko-KR.js +11 -5
  40. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  41. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  42. package/dist/app/i18n/catalogs/pt-BR.js +11 -5
  43. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  44. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  45. package/dist/app/i18n/catalogs/zh-CN.js +11 -5
  46. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  47. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  48. package/dist/app/i18n/catalogs/zh-TW.js +11 -5
  49. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  50. package/dist/app/onboarding/BuilderIncludedServices.d.ts +23 -0
  51. package/dist/app/onboarding/BuilderIncludedServices.d.ts.map +1 -0
  52. package/dist/app/onboarding/BuilderIncludedServices.js +67 -0
  53. package/dist/app/onboarding/BuilderIncludedServices.js.map +1 -0
  54. package/dist/app/onboarding/FirstRunOnboarding.d.ts.map +1 -1
  55. package/dist/app/onboarding/FirstRunOnboarding.js +10 -29
  56. package/dist/app/onboarding/FirstRunOnboarding.js.map +1 -1
  57. package/dist/app/resources/McpConnectionSuggestion.d.ts.map +1 -1
  58. package/dist/app/resources/McpConnectionSuggestion.js +55 -21
  59. package/dist/app/resources/McpConnectionSuggestion.js.map +1 -1
  60. package/dist/app/settings/BuilderConnectPopover.d.ts +1 -2
  61. package/dist/app/settings/BuilderConnectPopover.d.ts.map +1 -1
  62. package/dist/app/settings/BuilderConnectPopover.js +35 -21
  63. package/dist/app/settings/BuilderConnectPopover.js.map +1 -1
  64. package/dist/app/settings/deferred-builder-connect-popover.d.ts.map +1 -1
  65. package/dist/app/settings/deferred-builder-connect-popover.js +28 -1
  66. package/dist/app/settings/deferred-builder-connect-popover.js.map +1 -1
  67. package/dist/app/settings/useBuilderStatus.d.ts.map +1 -1
  68. package/dist/app/settings/useBuilderStatus.js +8 -1
  69. package/dist/app/settings/useBuilderStatus.js.map +1 -1
  70. package/dist/app/styles/tokens/agent-kit.css +23 -11
  71. package/dist/composer/MessageQueueDrawer.d.ts +5 -0
  72. package/dist/composer/MessageQueueDrawer.d.ts.map +1 -1
  73. package/dist/composer/MessageQueueDrawer.js +11 -5
  74. package/dist/composer/MessageQueueDrawer.js.map +1 -1
  75. package/package.json +4 -3
  76. package/src/app/agentkit/react/components.interaction.spec.tsx +13 -5
  77. package/src/app/agentkit/react/components.tsx +24 -14
  78. package/src/app/agentkit/react/context.tsx +53 -6
  79. package/src/app/agentkit/react/styles.css +11 -7
  80. package/src/app/chat/AgentKitAssistantChat.spec.tsx +35 -16
  81. package/src/app/chat/AgentKitAssistantChat.tsx +13 -4
  82. package/src/app/chat/FileStorageSetupPopover.spec.tsx +129 -0
  83. package/src/app/chat/FileStorageSetupPopover.tsx +10 -3
  84. package/src/app/i18n/catalogs/ar-SA.ts +14 -5
  85. package/src/app/i18n/catalogs/de-DE.ts +15 -5
  86. package/src/app/i18n/catalogs/en-US.ts +14 -5
  87. package/src/app/i18n/catalogs/es-ES.ts +14 -5
  88. package/src/app/i18n/catalogs/fr-FR.ts +15 -5
  89. package/src/app/i18n/catalogs/hi-IN.ts +14 -5
  90. package/src/app/i18n/catalogs/ja-JP.ts +14 -5
  91. package/src/app/i18n/catalogs/ko-KR.ts +14 -5
  92. package/src/app/i18n/catalogs/pt-BR.ts +15 -5
  93. package/src/app/i18n/catalogs/zh-CN.ts +13 -5
  94. package/src/app/i18n/catalogs/zh-TW.ts +13 -5
  95. package/src/app/onboarding/BuilderIncludedServices.tsx +263 -0
  96. package/src/app/onboarding/FirstRunOnboarding.spec.tsx +23 -8
  97. package/src/app/onboarding/FirstRunOnboarding.tsx +22 -112
  98. package/src/app/resources/McpConnectionSuggestion.render.spec.tsx +54 -2
  99. package/src/app/resources/McpConnectionSuggestion.tsx +73 -25
  100. package/src/app/settings/BuilderConnectPopover.spec.tsx +175 -6
  101. package/src/app/settings/BuilderConnectPopover.tsx +87 -67
  102. package/src/app/settings/deferred-builder-connect-popover.tsx +46 -0
  103. package/src/app/settings/model/ModelSettingsPage.spec.tsx +8 -24
  104. package/src/app/settings/useBuilderStatus.spec.tsx +5 -0
  105. package/src/app/settings/useBuilderStatus.ts +8 -0
  106. package/src/app/styles/agent-native.spec.ts +5 -8
  107. package/src/app/styles/tokens/agent-kit.css +23 -11
  108. package/src/composer/MessageQueueDrawer.spec.tsx +20 -7
  109. package/src/composer/MessageQueueDrawer.tsx +47 -21
@@ -0,0 +1,263 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+ import type { OnboardingCapability } from "@agent-native/core/onboarding/types";
3
+ import {
4
+ Collapsible,
5
+ CollapsibleContent,
6
+ CollapsibleTrigger,
7
+ } from "@agent-native/toolkit/ui/collapsible";
8
+ import { Skeleton } from "@agent-native/toolkit/ui/skeleton";
9
+ import {
10
+ Tooltip,
11
+ TooltipContent,
12
+ TooltipProvider,
13
+ TooltipTrigger,
14
+ } from "@agent-native/toolkit/ui/tooltip";
15
+ import {
16
+ IconCheck,
17
+ IconChevronDown,
18
+ IconInfoCircle,
19
+ } from "@tabler/icons-react";
20
+
21
+ type CapabilityTranslator = (
22
+ key: string,
23
+ options?: Record<string, unknown>,
24
+ ) => string;
25
+
26
+ export function getBuilderIncludedCapabilities(
27
+ capabilities: OnboardingCapability[],
28
+ ) {
29
+ return capabilities.filter(
30
+ (capability) =>
31
+ capability.builderIncluded &&
32
+ (!!capability.service ||
33
+ capability.required ||
34
+ !!capability.suggested ||
35
+ !!capability.builderOnly),
36
+ );
37
+ }
38
+
39
+ function capabilityCopy(
40
+ t: CapabilityTranslator,
41
+ capability: OnboardingCapability,
42
+ ) {
43
+ return {
44
+ label: capability.labelKey
45
+ ? t(capability.labelKey, { defaultValue: capability.label })
46
+ : capability.label,
47
+ why: capability.whyKey
48
+ ? t(capability.whyKey, { defaultValue: capability.why })
49
+ : null,
50
+ };
51
+ }
52
+
53
+ export function CapabilityInfoButton({
54
+ label,
55
+ why,
56
+ }: {
57
+ label: string;
58
+ why: string;
59
+ }) {
60
+ const t = useT();
61
+
62
+ return (
63
+ <Tooltip>
64
+ <TooltipTrigger asChild>
65
+ <button
66
+ type="button"
67
+ aria-label={t("agentChat.onboarding.capability.about", {
68
+ defaultValue: "About {{label}}",
69
+ label,
70
+ })}
71
+ className="inline-flex size-4 items-center justify-center rounded-full text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
72
+ onClick={(event) => event.stopPropagation()}
73
+ onKeyDown={(event) => event.stopPropagation()}
74
+ >
75
+ <IconInfoCircle size={13} />
76
+ </button>
77
+ </TooltipTrigger>
78
+ <TooltipContent side="top" className="max-w-xs text-xs">
79
+ {why}
80
+ </TooltipContent>
81
+ </Tooltip>
82
+ );
83
+ }
84
+
85
+ function CapabilityRows({
86
+ capabilities,
87
+ }: {
88
+ capabilities: OnboardingCapability[];
89
+ }) {
90
+ const t = useT();
91
+
92
+ return (
93
+ <TooltipProvider>
94
+ {capabilities.map((capability) => {
95
+ const copy = capabilityCopy(t, capability);
96
+ return (
97
+ <div
98
+ key={capability.id}
99
+ className="flex items-center gap-2 rounded-md px-2 py-1"
100
+ >
101
+ <IconCheck
102
+ aria-hidden="true"
103
+ className="shrink-0 text-muted-foreground"
104
+ size={15}
105
+ />
106
+ <span className="text-xs text-foreground">{copy.label}</span>
107
+ {copy.why ? (
108
+ <CapabilityInfoButton label={copy.label} why={copy.why} />
109
+ ) : null}
110
+ </div>
111
+ );
112
+ })}
113
+ </TooltipProvider>
114
+ );
115
+ }
116
+
117
+ export function BuilderIncludedServices({
118
+ capabilities,
119
+ collapsible = false,
120
+ loading = false,
121
+ error,
122
+ testId,
123
+ }: {
124
+ capabilities: OnboardingCapability[];
125
+ collapsible?: boolean;
126
+ loading?: boolean;
127
+ error?: string | null;
128
+ testId?: string;
129
+ }) {
130
+ const t = useT();
131
+
132
+ if (!collapsible && capabilities.length === 0) return null;
133
+ if (collapsible && !loading && !error && capabilities.length === 0)
134
+ return null;
135
+
136
+ const content = loading ? (
137
+ <div className="grid gap-2 py-1" aria-label={t("agentChat.common.loading")}>
138
+ <Skeleton className="h-5 w-4/5" />
139
+ <Skeleton className="h-5 w-2/3" />
140
+ <Skeleton className="h-5 w-3/4" />
141
+ </div>
142
+ ) : error ? (
143
+ <p role="status" className="py-1 text-xs text-muted-foreground">
144
+ {error}
145
+ </p>
146
+ ) : (
147
+ <CapabilityRows capabilities={capabilities} />
148
+ );
149
+
150
+ if (collapsible) {
151
+ return (
152
+ <Collapsible
153
+ defaultOpen={false}
154
+ className="border-t border-border/60"
155
+ data-testid={testId}
156
+ >
157
+ <CollapsibleTrigger className="group flex w-full items-center justify-between gap-2 py-3 text-left text-sm font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring">
158
+ {t("agentChat.onboarding.builderIncludedServices", {
159
+ defaultValue: "Included services",
160
+ })}
161
+ <IconChevronDown
162
+ aria-hidden="true"
163
+ className="size-4 shrink-0 transition-transform group-data-[state=open]:rotate-180"
164
+ />
165
+ </CollapsibleTrigger>
166
+ <CollapsibleContent className="grid gap-1 pb-1">
167
+ {content}
168
+ </CollapsibleContent>
169
+ </Collapsible>
170
+ );
171
+ }
172
+
173
+ return (
174
+ <div className="flex flex-col gap-1" data-testid={testId}>
175
+ <p className="text-sm text-muted-foreground/70">What&rsquo;s included</p>
176
+ <CapabilityRows capabilities={capabilities} />
177
+ </div>
178
+ );
179
+ }
180
+
181
+ export function BuilderIncludedBenefitsDisclosure({
182
+ capabilities,
183
+ includedLabel,
184
+ creditsLabel,
185
+ loading = false,
186
+ loadingLabel,
187
+ error,
188
+ testId,
189
+ }: {
190
+ capabilities: OnboardingCapability[];
191
+ includedLabel: string;
192
+ creditsLabel: string;
193
+ loading?: boolean;
194
+ loadingLabel: string;
195
+ error?: string | null;
196
+ testId?: string;
197
+ }) {
198
+ const t = useT();
199
+ const additionalServices = capabilities.filter(
200
+ (capability) => capability.id !== "llm" && capability.service !== "model",
201
+ );
202
+ const canExpand = loading || !!error || additionalServices.length > 0;
203
+ const serviceList = loading ? (
204
+ <div className="grid gap-2 py-1" aria-label={loadingLabel}>
205
+ <Skeleton className="h-5 w-4/5" />
206
+ <Skeleton className="h-5 w-2/3" />
207
+ <Skeleton className="h-5 w-3/4" />
208
+ </div>
209
+ ) : error ? (
210
+ <p role="status" className="py-1 text-xs text-muted-foreground">
211
+ {error}
212
+ </p>
213
+ ) : (
214
+ <CapabilityRows capabilities={additionalServices} />
215
+ );
216
+ const moreServicesLabel = t("agentChat.onboarding.builderMoreServices", {
217
+ defaultValue: "+ {{count}} more services",
218
+ count: additionalServices.length,
219
+ });
220
+
221
+ const summary = (
222
+ <span className="flex min-w-0 flex-1 flex-col gap-1">
223
+ <span className="text-[13px] font-semibold text-foreground">
224
+ {includedLabel}
225
+ </span>
226
+ <span className="text-xs font-medium text-emerald-700 dark:text-emerald-400">
227
+ {creditsLabel}
228
+ </span>
229
+ {additionalServices.length > 0 ? (
230
+ <span className="text-xs font-medium text-foreground dark:text-white">
231
+ {moreServicesLabel}
232
+ </span>
233
+ ) : null}
234
+ </span>
235
+ );
236
+
237
+ if (!canExpand) {
238
+ return (
239
+ <div className="rounded-[10px] bg-emerald-50 px-4 py-3 dark:bg-emerald-950/30">
240
+ {summary}
241
+ </div>
242
+ );
243
+ }
244
+
245
+ return (
246
+ <Collapsible
247
+ defaultOpen={false}
248
+ className="overflow-hidden rounded-[10px] bg-emerald-50 dark:bg-emerald-950/30"
249
+ data-testid={testId}
250
+ >
251
+ <CollapsibleTrigger className="group flex w-full items-start justify-between gap-3 px-4 py-3 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring">
252
+ {summary}
253
+ <IconChevronDown
254
+ aria-hidden="true"
255
+ className="mt-0.5 size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180"
256
+ />
257
+ </CollapsibleTrigger>
258
+ <CollapsibleContent className="border-0 bg-muted px-2 py-2">
259
+ {serviceList}
260
+ </CollapsibleContent>
261
+ </Collapsible>
262
+ );
263
+ }
@@ -109,6 +109,7 @@ describe("FirstRunOnboarding", () => {
109
109
  required: true,
110
110
  builderIncluded: true,
111
111
  keySummary: "LLM provider key",
112
+ whyKey: "agentChat.onboarding.capability.llm.why",
112
113
  why: "Needed for chat",
113
114
  },
114
115
  {
@@ -844,7 +845,7 @@ describe("FirstRunOnboarding", () => {
844
845
  );
845
846
  });
846
847
 
847
- it("lists the included Builder.io services from the app profile", () => {
848
+ it("shows included Builder.io services with descriptions in tooltips", () => {
848
849
  act(() => {
849
850
  root.render(
850
851
  <TooltipProvider>
@@ -862,9 +863,11 @@ describe("FirstRunOnboarding", () => {
862
863
  const builderCard = document.body
863
864
  .querySelector("[data-testid='first-run-builder-create-account']")
864
865
  ?.closest("section");
865
- const included = [...(builderCard?.querySelectorAll("span") ?? [])].map(
866
- (node) => node.textContent?.trim(),
866
+ const services = builderCard?.querySelector<HTMLElement>(
867
+ "[data-testid='first-run-builder-services']",
867
868
  );
869
+ expect(services?.querySelector("details, summary")).toBeNull();
870
+ const included = services?.textContent ?? "";
868
871
  // Every shared service, whether or not the app recommends it, plus the
869
872
  // app's own headline capability Builder.io covers.
870
873
  for (const service of [
@@ -878,6 +881,23 @@ describe("FirstRunOnboarding", () => {
878
881
  ]) {
879
882
  expect(included).toContain(service);
880
883
  }
884
+ expect(included).not.toContain("Needed for chat");
885
+ expect(included).not.toContain("Turns speech into text");
886
+ expect(
887
+ services?.querySelectorAll("button[aria-label^='About']").length,
888
+ ).toBe(1);
889
+ const manualCard = document.body
890
+ .querySelector("[data-testid='first-run-open-key-settings']")
891
+ ?.closest("section");
892
+ expect(builderCard?.textContent).not.toContain(
893
+ "Configure using Builder.io and use your account credits to power the app’s services.",
894
+ );
895
+ expect(manualCard?.textContent).not.toContain(
896
+ "Configure your own API keys and credentials to power the app’s services.",
897
+ );
898
+ expect(manualCard?.textContent).not.toContain("Design system intelligence");
899
+ expect(manualCard?.textContent).not.toContain("Background agents");
900
+ expect(manualCard?.querySelector("svg.tabler-icon-x")).toBeNull();
881
901
  // Not Builder.io capabilities, and no per-app extras.
882
902
  for (const missing of [
883
903
  "Connected agents",
@@ -886,11 +906,6 @@ describe("FirstRunOnboarding", () => {
886
906
  ]) {
887
907
  expect(document.body.textContent).not.toContain(missing);
888
908
  }
889
- expect(
890
- [...document.body.querySelectorAll("button")].find((button) =>
891
- button.textContent?.trim().endsWith("more"),
892
- ),
893
- ).toBeUndefined();
894
909
  });
895
910
 
896
911
  it("does not ask Mail users to connect Gmail again in manual setup", () => {
@@ -28,20 +28,8 @@ import type {
28
28
  } from "@agent-native/core/onboarding/types";
29
29
  import { Badge } from "@agent-native/toolkit/ui/badge";
30
30
  import { Skeleton } from "@agent-native/toolkit/ui/skeleton";
31
- import {
32
- Tooltip,
33
- TooltipContent,
34
- TooltipTrigger,
35
- } from "@agent-native/toolkit/ui/tooltip";
36
31
  import { cn } from "@agent-native/toolkit/utils";
37
- import {
38
- IconArrowRight,
39
- IconCheck,
40
- IconInfoCircle,
41
- IconKey,
42
- IconLoader2,
43
- IconX,
44
- } from "@tabler/icons-react";
32
+ import { IconArrowRight, IconKey, IconLoader2 } from "@tabler/icons-react";
45
33
  import React, {
46
34
  useCallback,
47
35
  useEffect,
@@ -52,6 +40,11 @@ import React, {
52
40
  import { useLocation } from "react-router";
53
41
 
54
42
  import { useBuilderConnectFlow } from "../settings/useBuilderStatus.js";
43
+ import {
44
+ BuilderIncludedServices,
45
+ CapabilityInfoButton,
46
+ getBuilderIncludedCapabilities,
47
+ } from "./BuilderIncludedServices.js";
55
48
  import {
56
49
  ONBOARDING_PRIMARY_BUTTON_CLASS,
57
50
  OnboardingStepLayout,
@@ -523,10 +516,8 @@ export function FirstRunOnboarding({
523
516
 
524
517
  // Every shared service Builder.io powers, the same list Infrastructure
525
518
  // shows, plus the app's own headline capabilities it covers.
526
- const builderCapabilities = profile.capabilities.filter(
527
- (capability) =>
528
- capability.builderIncluded &&
529
- (!!capability.service || isHeadlineCapability(capability)),
519
+ const builderCapabilities = getBuilderIncludedCapabilities(
520
+ profile.capabilities,
530
521
  );
531
522
  const handleBuilder = (provisionAccount = canActivateBuilderFreeCredits) => {
532
523
  if (previewMode) {
@@ -720,10 +711,6 @@ export function FirstRunOnboarding({
720
711
  Recommended
721
712
  </Badge>
722
713
  </div>
723
- <p className="text-sm text-muted-foreground">
724
- Configure using Builder.io and use your account credits to
725
- power the app&rsquo;s services.
726
- </p>
727
714
  </div>
728
715
  <div className="flex flex-col gap-1 rounded-[10px] bg-emerald-50 px-4 py-3 dark:bg-emerald-950/30">
729
716
  <p className="text-[13px] font-semibold text-foreground">
@@ -733,40 +720,10 @@ export function FirstRunOnboarding({
733
720
  {t("agentChat.onboarding.builderMonthlyCredits")}
734
721
  </p>
735
722
  </div>
736
- <div className="flex flex-col gap-1">
737
- <p className="text-sm text-muted-foreground/70">
738
- What&rsquo;s included
739
- </p>
740
- {builderCapabilities.map((capability) => {
741
- const copy = getCapabilityCopy(t, capability);
742
- return (
743
- <div
744
- key={capability.id}
745
- className="flex items-center gap-2 rounded-md px-2 py-1"
746
- >
747
- <IconCheck
748
- className="shrink-0 text-muted-foreground"
749
- size={15}
750
- />
751
- <span className="flex-1 text-xs text-foreground">
752
- {copy.label}
753
- </span>
754
- {capability.builderOnly && (
755
- <CapabilityInfoButton
756
- why={copy.why}
757
- ariaLabel={t(
758
- "agentChat.onboarding.capability.about",
759
- {
760
- defaultValue: "About {{label}}",
761
- label: copy.label,
762
- },
763
- )}
764
- />
765
- )}
766
- </div>
767
- );
768
- })}
769
- </div>
723
+ <BuilderIncludedServices
724
+ capabilities={builderCapabilities}
725
+ testId="first-run-builder-services"
726
+ />
770
727
  <div className="flex flex-col gap-2">
771
728
  {canActivateBuilderFreeCredits && (
772
729
  <button
@@ -805,10 +762,6 @@ export function FirstRunOnboarding({
805
762
  <h2 className="text-lg font-bold text-foreground">
806
763
  Configure manually
807
764
  </h2>
808
- <p className="text-sm text-muted-foreground">
809
- Configure your own API keys and credentials to power the
810
- app&rsquo;s services.
811
- </p>
812
765
  </div>
813
766
  <div className="flex flex-col gap-1 rounded-xl bg-muted px-4 py-3">
814
767
  <p className="text-[13px] font-semibold text-foreground">
@@ -1148,11 +1101,11 @@ type CapabilityTranslator = (
1148
1101
 
1149
1102
  type CapabilityCopy = Pick<
1150
1103
  OnboardingCapability,
1151
- "id" | "required" | "suggested" | "builderOnly"
1104
+ "id" | "required" | "suggested"
1152
1105
  > & {
1153
1106
  label: string;
1154
1107
  keySummary: string;
1155
- why: string;
1108
+ why: string | null;
1156
1109
  };
1157
1110
 
1158
1111
  function getCapabilityCopy(
@@ -1163,7 +1116,6 @@ function getCapabilityCopy(
1163
1116
  id: capability.id,
1164
1117
  required: capability.required,
1165
1118
  suggested: capability.suggested,
1166
- builderOnly: capability.builderOnly,
1167
1119
  label: capability.labelKey
1168
1120
  ? t(capability.labelKey, { defaultValue: capability.label })
1169
1121
  : capability.label,
@@ -1172,7 +1124,7 @@ function getCapabilityCopy(
1172
1124
  : capability.keySummary,
1173
1125
  why: capability.whyKey
1174
1126
  ? t(capability.whyKey, { defaultValue: capability.why })
1175
- : capability.why,
1127
+ : null,
1176
1128
  };
1177
1129
  }
1178
1130
 
@@ -1187,16 +1139,15 @@ function CapabilityList({
1187
1139
  const visibleCapabilities = useMemo(() => {
1188
1140
  const headline = capabilities.filter(
1189
1141
  (capability) =>
1190
- !capability.satisfiedBySignIn && isHeadlineCapability(capability),
1142
+ !capability.satisfiedBySignIn &&
1143
+ !capability.builderOnly &&
1144
+ (capability.required || capability.suggested),
1191
1145
  );
1192
1146
  const required = headline.filter((capability) => capability.required);
1193
1147
  const suggested = headline.filter(
1194
1148
  (capability) => !capability.required && capability.suggested,
1195
1149
  );
1196
- const noManualPath = headline.filter(
1197
- (capability) => !capability.required && !capability.suggested,
1198
- );
1199
- return [...required, ...suggested, ...noManualPath];
1150
+ return [...required, ...suggested];
1200
1151
  }, [capabilities]);
1201
1152
 
1202
1153
  return (
@@ -1216,24 +1167,7 @@ function CapabilityList({
1216
1167
  );
1217
1168
  }
1218
1169
 
1219
- function isHeadlineCapability(capability: OnboardingCapability): boolean {
1220
- return (
1221
- capability.required || !!capability.suggested || !!capability.builderOnly
1222
- );
1223
- }
1224
-
1225
1170
  function CapabilityRow({ copy }: { copy: CapabilityCopy }) {
1226
- // Builder-only services have no bring-your-own path, so the manual list
1227
- // shows them crossed out with no Required/Recommended tag instead of
1228
- // mislabeling them "Optional".
1229
- if (copy.builderOnly) {
1230
- return (
1231
- <div className="flex items-center gap-2 rounded-md px-2 py-1">
1232
- <IconX className="shrink-0 text-muted-foreground" size={14} />
1233
- <span className="flex-1 text-xs text-foreground">{copy.label}</span>
1234
- </div>
1235
- );
1236
- }
1237
1171
  return (
1238
1172
  <div className="flex items-center gap-2 rounded-md px-2 py-1">
1239
1173
  <IconKey className="shrink-0 text-muted-foreground" size={14} />
@@ -1243,6 +1177,9 @@ function CapabilityRow({ copy }: { copy: CapabilityCopy }) {
1243
1177
  >
1244
1178
  {copy.keySummary}
1245
1179
  </span>
1180
+ {copy.why ? (
1181
+ <CapabilityInfoButton label={copy.label} why={copy.why} />
1182
+ ) : null}
1246
1183
  <span className="shrink-0 text-xs text-muted-foreground">
1247
1184
  {copy.required ? "Required" : "Recommended"}
1248
1185
  </span>
@@ -1250,33 +1187,6 @@ function CapabilityRow({ copy }: { copy: CapabilityCopy }) {
1250
1187
  );
1251
1188
  }
1252
1189
 
1253
- function CapabilityInfoButton({
1254
- why,
1255
- ariaLabel,
1256
- }: {
1257
- why: string;
1258
- ariaLabel: string;
1259
- }) {
1260
- return (
1261
- <Tooltip>
1262
- <TooltipTrigger asChild>
1263
- <button
1264
- type="button"
1265
- aria-label={ariaLabel}
1266
- className="inline-flex size-4 items-center justify-center rounded-full text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1267
- onClick={(event) => event.stopPropagation()}
1268
- onKeyDown={(event) => event.stopPropagation()}
1269
- >
1270
- <IconInfoCircle size={13} />
1271
- </button>
1272
- </TooltipTrigger>
1273
- <TooltipContent side="top" className="max-w-xs text-xs">
1274
- {why}
1275
- </TooltipContent>
1276
- </Tooltip>
1277
- );
1278
- }
1279
-
1280
1190
  function FirstRunCompletionError({
1281
1191
  message,
1282
1192
  onRetry,
@@ -117,6 +117,7 @@ describe("McpConnectionSuggestion render", () => {
117
117
  } else {
118
118
  Reflect.deleteProperty(window, "localStorage");
119
119
  }
120
+ vi.useRealTimers();
120
121
  vi.restoreAllMocks();
121
122
  vi.unstubAllGlobals();
122
123
  });
@@ -142,6 +143,57 @@ describe("McpConnectionSuggestion render", () => {
142
143
 
143
144
  act(() => (dismiss as HTMLButtonElement).click());
144
145
 
146
+ expect(container.querySelector("[data-mcp-connection-suggestion]")).toBe(
147
+ null,
148
+ );
149
+ const dismissals = JSON.parse(
150
+ window.localStorage.getItem(
151
+ "agent-native:mcp-connection-suggestions-dismissed",
152
+ ) ?? "{}",
153
+ ) as Record<string, number>;
154
+ expect(dismissals["test-integration"]).toEqual(expect.any(Number));
155
+
156
+ act(() => root.unmount());
157
+ root = createRoot(container);
158
+ renderSuggestion();
159
+ expect(container.querySelector("[data-mcp-connection-suggestion]")).toBe(
160
+ null,
161
+ );
162
+ });
163
+
164
+ it("shows a dismissed suggestion again after one week", () => {
165
+ vi.useFakeTimers();
166
+ const dismissedAt = new Date("2026-10-01T00:00:00.000Z").getTime();
167
+ vi.setSystemTime(dismissedAt);
168
+ window.localStorage.setItem(
169
+ "agent-native:mcp-connection-suggestions-dismissed",
170
+ JSON.stringify({ "test-integration": dismissedAt }),
171
+ );
172
+
173
+ renderSuggestion();
174
+
175
+ expect(container.querySelector("[data-mcp-connection-suggestion]")).toBe(
176
+ null,
177
+ );
178
+
179
+ act(() => vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000));
180
+
181
+ expect(
182
+ container.querySelector("[data-mcp-connection-suggestion]"),
183
+ ).not.toBeNull();
184
+ });
185
+
186
+ it("migrates existing dismissal IDs and preserves them across mounts", () => {
187
+ vi.useFakeTimers();
188
+ const dismissedAt = new Date("2026-10-01T00:00:00.000Z").getTime();
189
+ vi.setSystemTime(dismissedAt);
190
+ window.localStorage.setItem(
191
+ "agent-native:mcp-connection-suggestions-dismissed",
192
+ JSON.stringify(["test-integration"]),
193
+ );
194
+
195
+ renderSuggestion();
196
+
145
197
  expect(container.querySelector("[data-mcp-connection-suggestion]")).toBe(
146
198
  null,
147
199
  );
@@ -149,9 +201,9 @@ describe("McpConnectionSuggestion render", () => {
149
201
  JSON.parse(
150
202
  window.localStorage.getItem(
151
203
  "agent-native:mcp-connection-suggestions-dismissed",
152
- ) ?? "[]",
204
+ ) ?? "{}",
153
205
  ),
154
- ).toEqual(["test-integration"]);
206
+ ).toEqual({ "test-integration": dismissedAt });
155
207
 
156
208
  act(() => root.unmount());
157
209
  root = createRoot(container);