@agent-native/toolkit 0.221.0-same-host-mcp-20261027-20261027081850 → 0.221.0

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 (259) hide show
  1. package/agent-native.eject.json +16 -1
  2. package/dist/app/agentkit/react/components.d.ts +5 -3
  3. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  4. package/dist/app/agentkit/react/components.js +103 -42
  5. package/dist/app/agentkit/react/components.js.map +1 -1
  6. package/dist/app/agentkit/react/styles.css +93 -65
  7. package/dist/app/agentkit/react/voice-transcripts.d.ts +22 -1
  8. package/dist/app/agentkit/react/voice-transcripts.d.ts.map +1 -1
  9. package/dist/app/agentkit/react/voice-transcripts.js +209 -8
  10. package/dist/app/agentkit/react/voice-transcripts.js.map +1 -1
  11. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  12. package/dist/app/chat/AgentKitAssistantChat.js +55 -40
  13. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  14. package/dist/app/chat/agentkit-chat/guided-questions.d.ts +1 -0
  15. package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
  16. package/dist/app/chat/agentkit-chat/guided-questions.js +18 -6
  17. package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
  18. package/dist/app/chat/agentkit-chat/index.d.ts +1 -1
  19. package/dist/app/chat/agentkit-chat/index.d.ts.map +1 -1
  20. package/dist/app/chat/agentkit-chat/index.js +1 -1
  21. package/dist/app/chat/agentkit-chat/index.js.map +1 -1
  22. package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
  23. package/dist/app/chat/chat/run-recovery.js +5 -1
  24. package/dist/app/chat/chat/run-recovery.js.map +1 -1
  25. package/dist/app/chat/composer/runtime-adapters.d.ts.map +1 -1
  26. package/dist/app/chat/composer/runtime-adapters.js +2 -0
  27. package/dist/app/chat/composer/runtime-adapters.js.map +1 -1
  28. package/dist/app/chat/reply-connection-suggestion.d.ts +29 -0
  29. package/dist/app/chat/reply-connection-suggestion.d.ts.map +1 -0
  30. package/dist/app/chat/reply-connection-suggestion.js +42 -0
  31. package/dist/app/chat/reply-connection-suggestion.js.map +1 -0
  32. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  33. package/dist/app/i18n/catalogs/ar-SA.js +7 -0
  34. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  35. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  36. package/dist/app/i18n/catalogs/de-DE.js +7 -0
  37. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  38. package/dist/app/i18n/catalogs/en-US.d.ts +7 -0
  39. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  40. package/dist/app/i18n/catalogs/en-US.js +7 -0
  41. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  42. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  43. package/dist/app/i18n/catalogs/es-ES.js +7 -0
  44. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  45. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  46. package/dist/app/i18n/catalogs/fr-FR.js +7 -0
  47. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  48. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  49. package/dist/app/i18n/catalogs/hi-IN.js +7 -0
  50. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  51. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  52. package/dist/app/i18n/catalogs/ja-JP.js +7 -0
  53. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  54. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  55. package/dist/app/i18n/catalogs/ko-KR.js +7 -0
  56. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  57. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  58. package/dist/app/i18n/catalogs/pt-BR.js +7 -0
  59. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  60. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  61. package/dist/app/i18n/catalogs/zh-CN.js +7 -0
  62. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  63. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  64. package/dist/app/i18n/catalogs/zh-TW.js +7 -0
  65. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  66. package/dist/app/mcp-approval/McpApprovalReview.d.ts +25 -0
  67. package/dist/app/mcp-approval/McpApprovalReview.d.ts.map +1 -0
  68. package/dist/app/mcp-approval/McpApprovalReview.js +342 -0
  69. package/dist/app/mcp-approval/McpApprovalReview.js.map +1 -0
  70. package/dist/app/onboarding/FirstRunOnboarding.d.ts.map +1 -1
  71. package/dist/app/onboarding/FirstRunOnboarding.js +299 -71
  72. package/dist/app/onboarding/FirstRunOnboarding.js.map +1 -1
  73. package/dist/app/onboarding/GettingStartedCard.js +1 -1
  74. package/dist/app/onboarding/GettingStartedCard.js.map +1 -1
  75. package/dist/app/onboarding/first-run-builder-credits-flag.d.ts +2 -0
  76. package/dist/app/onboarding/first-run-builder-credits-flag.d.ts.map +1 -0
  77. package/dist/app/onboarding/first-run-builder-credits-flag.js +8 -0
  78. package/dist/app/onboarding/first-run-builder-credits-flag.js.map +1 -0
  79. package/dist/app/providers/AppProviders.d.ts.map +1 -1
  80. package/dist/app/providers/AppProviders.js +14 -1
  81. package/dist/app/providers/AppProviders.js.map +1 -1
  82. package/dist/app/resources/McpConnectionSuggestion.d.ts +2 -4
  83. package/dist/app/resources/McpConnectionSuggestion.d.ts.map +1 -1
  84. package/dist/app/resources/McpConnectionSuggestion.js +59 -36
  85. package/dist/app/resources/McpConnectionSuggestion.js.map +1 -1
  86. package/dist/app/settings/account/PreferencesSettings.d.ts.map +1 -1
  87. package/dist/app/settings/account/PreferencesSettings.js +14 -1
  88. package/dist/app/settings/account/PreferencesSettings.js.map +1 -1
  89. package/dist/app/settings/account/ThemeFamilySettings.js +2 -2
  90. package/dist/app/settings/account/ThemeFamilySettings.js.map +1 -1
  91. package/dist/app/settings/account/search-entries.d.ts +1 -0
  92. package/dist/app/settings/account/search-entries.d.ts.map +1 -1
  93. package/dist/app/settings/account/search-entries.js +5 -0
  94. package/dist/app/settings/account/search-entries.js.map +1 -1
  95. package/dist/app/settings/shell/SettingsShell.d.ts.map +1 -1
  96. package/dist/app/settings/shell/SettingsShell.js +26 -2
  97. package/dist/app/settings/shell/SettingsShell.js.map +1 -1
  98. package/dist/app/shared/AppShellSkeleton.d.ts.map +1 -1
  99. package/dist/app/shared/AppShellSkeleton.js +4 -10
  100. package/dist/app/shared/AppShellSkeleton.js.map +1 -1
  101. package/dist/app/shared/EnvironmentBadge.d.ts +2 -1
  102. package/dist/app/shared/EnvironmentBadge.d.ts.map +1 -1
  103. package/dist/app/shared/EnvironmentBadge.js +2 -3
  104. package/dist/app/shared/EnvironmentBadge.js.map +1 -1
  105. package/dist/app/shared/is-builder-io-employee.d.ts +8 -0
  106. package/dist/app/shared/is-builder-io-employee.d.ts.map +1 -0
  107. package/dist/app/shared/is-builder-io-employee.js +10 -0
  108. package/dist/app/shared/is-builder-io-employee.js.map +1 -0
  109. package/dist/app/styles/agent-conversation.css +30 -17
  110. package/dist/app/styles/theme-families.css +22 -5
  111. package/dist/app/styles/tokens/agent-kit.css +3 -3
  112. package/dist/composer/PromptComposer.d.ts +15 -3
  113. package/dist/composer/PromptComposer.d.ts.map +1 -1
  114. package/dist/composer/PromptComposer.js +74 -16
  115. package/dist/composer/PromptComposer.js.map +1 -1
  116. package/dist/composer/RealtimeVoiceMode.d.ts +7 -0
  117. package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
  118. package/dist/composer/RealtimeVoiceMode.js +9 -2
  119. package/dist/composer/RealtimeVoiceMode.js.map +1 -1
  120. package/dist/composer/TiptapComposer.d.ts +7 -0
  121. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  122. package/dist/composer/TiptapComposer.js +12 -4
  123. package/dist/composer/TiptapComposer.js.map +1 -1
  124. package/dist/composer/VoiceButton.d.ts +2 -0
  125. package/dist/composer/VoiceButton.d.ts.map +1 -1
  126. package/dist/composer/VoiceButton.js +5 -4
  127. package/dist/composer/VoiceButton.js.map +1 -1
  128. package/dist/composer/index.d.ts +2 -2
  129. package/dist/composer/index.d.ts.map +1 -1
  130. package/dist/composer/index.js +1 -1
  131. package/dist/composer/index.js.map +1 -1
  132. package/dist/composer/prompt-attachments.d.ts +20 -0
  133. package/dist/composer/prompt-attachments.d.ts.map +1 -1
  134. package/dist/composer/prompt-attachments.js +188 -11
  135. package/dist/composer/prompt-attachments.js.map +1 -1
  136. package/dist/composer/realtime-voice-transcript.d.ts +27 -0
  137. package/dist/composer/realtime-voice-transcript.d.ts.map +1 -1
  138. package/dist/composer/realtime-voice-transcript.js +34 -1
  139. package/dist/composer/realtime-voice-transcript.js.map +1 -1
  140. package/dist/composer/realtime-voice-turn-metrics.d.ts +43 -0
  141. package/dist/composer/realtime-voice-turn-metrics.d.ts.map +1 -0
  142. package/dist/composer/realtime-voice-turn-metrics.js +132 -0
  143. package/dist/composer/realtime-voice-turn-metrics.js.map +1 -0
  144. package/dist/composer/runtime-adapters.d.ts +4 -1
  145. package/dist/composer/runtime-adapters.d.ts.map +1 -1
  146. package/dist/composer/runtime-adapters.js +2 -1
  147. package/dist/composer/runtime-adapters.js.map +1 -1
  148. package/dist/composer/useRealtimeVoiceMode.d.ts +61 -9
  149. package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  150. package/dist/composer/useRealtimeVoiceMode.js +536 -99
  151. package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
  152. package/dist/composer/voice-chat-icon.d.ts +7 -0
  153. package/dist/composer/voice-chat-icon.d.ts.map +1 -0
  154. package/dist/composer/voice-chat-icon.js +14 -0
  155. package/dist/composer/voice-chat-icon.js.map +1 -0
  156. package/dist/editor/DragHandle.d.ts +12 -1
  157. package/dist/editor/DragHandle.d.ts.map +1 -1
  158. package/dist/editor/DragHandle.js +165 -41
  159. package/dist/editor/DragHandle.js.map +1 -1
  160. package/dist/editor/RegistryBlockNode.d.ts +20 -0
  161. package/dist/editor/RegistryBlockNode.d.ts.map +1 -1
  162. package/dist/editor/RegistryBlockNode.js +58 -13
  163. package/dist/editor/RegistryBlockNode.js.map +1 -1
  164. package/dist/editor/index.d.ts +2 -2
  165. package/dist/editor/index.d.ts.map +1 -1
  166. package/dist/editor/index.js +1 -1
  167. package/dist/editor/index.js.map +1 -1
  168. package/dist/styles.css +2 -0
  169. package/dist/ui/dropdown-menu.d.ts.map +1 -1
  170. package/dist/ui/dropdown-menu.js +5 -1
  171. package/dist/ui/dropdown-menu.js.map +1 -1
  172. package/package.json +14 -4
  173. package/src/app/agentkit/react/components.regenerate-readiness.spec.tsx +3 -1
  174. package/src/app/agentkit/react/components.spec.tsx +24 -4
  175. package/src/app/agentkit/react/components.tsx +174 -44
  176. package/src/app/agentkit/react/composer-context.spec.tsx +3 -0
  177. package/src/app/agentkit/react/disclosures.spec.tsx +124 -0
  178. package/src/app/agentkit/react/lifecycle.spec.tsx +8 -2
  179. package/src/app/agentkit/react/message-actions.spec.tsx +161 -5
  180. package/src/app/agentkit/react/saved-reply-reload.spec.tsx +206 -0
  181. package/src/app/agentkit/react/styles.css +93 -65
  182. package/src/app/agentkit/react/voice-transcripts.spec.tsx +239 -4
  183. package/src/app/agentkit/react/voice-transcripts.ts +245 -8
  184. package/src/app/chat/AgentKitAssistantChat.spec.tsx +59 -11
  185. package/src/app/chat/AgentKitAssistantChat.tsx +74 -62
  186. package/src/app/chat/agentkit-chat/guided-answers-context.spec.ts +9 -0
  187. package/src/app/chat/agentkit-chat/guided-questions.tsx +17 -7
  188. package/src/app/chat/agentkit-chat/index.ts +1 -0
  189. package/src/app/chat/chat/run-recovery.spec.tsx +45 -0
  190. package/src/app/chat/chat/run-recovery.tsx +6 -1
  191. package/src/app/chat/composer/runtime-adapters.tsx +2 -0
  192. package/src/app/chat/reply-connection-suggestion.spec.ts +121 -0
  193. package/src/app/chat/reply-connection-suggestion.ts +76 -0
  194. package/src/app/i18n/catalogs/ar-SA.ts +10 -0
  195. package/src/app/i18n/catalogs/de-DE.ts +10 -0
  196. package/src/app/i18n/catalogs/en-US.ts +10 -0
  197. package/src/app/i18n/catalogs/es-ES.ts +10 -0
  198. package/src/app/i18n/catalogs/fr-FR.ts +10 -0
  199. package/src/app/i18n/catalogs/hi-IN.ts +10 -0
  200. package/src/app/i18n/catalogs/ja-JP.ts +10 -0
  201. package/src/app/i18n/catalogs/ko-KR.ts +10 -0
  202. package/src/app/i18n/catalogs/pt-BR.ts +10 -0
  203. package/src/app/i18n/catalogs/zh-CN.ts +10 -0
  204. package/src/app/i18n/catalogs/zh-TW.ts +10 -0
  205. package/src/app/mcp-approval/McpApprovalReview.spec.tsx +687 -0
  206. package/src/app/mcp-approval/McpApprovalReview.tsx +913 -0
  207. package/src/app/onboarding/FirstRunOnboarding.spec.tsx +736 -26
  208. package/src/app/onboarding/FirstRunOnboarding.tsx +473 -145
  209. package/src/app/onboarding/GettingStartedCard.tsx +1 -1
  210. package/src/app/onboarding/first-run-builder-credits-flag.spec.ts +13 -0
  211. package/src/app/onboarding/first-run-builder-credits-flag.ts +9 -0
  212. package/src/app/providers/AppProviders.spec.tsx +109 -0
  213. package/src/app/providers/AppProviders.tsx +53 -1
  214. package/src/app/resources/McpConnectionSuggestion.spec.ts +77 -12
  215. package/src/app/resources/McpConnectionSuggestion.tsx +69 -51
  216. package/src/app/settings/VoiceChatSettings.spec.tsx +6 -1
  217. package/src/app/settings/account/PreferencesSettings.spec.tsx +73 -0
  218. package/src/app/settings/account/PreferencesSettings.tsx +24 -7
  219. package/src/app/settings/account/ThemeFamilySettings.tsx +2 -2
  220. package/src/app/settings/account/search-entries.spec.ts +14 -0
  221. package/src/app/settings/account/search-entries.ts +9 -0
  222. package/src/app/settings/shell/SettingsShell.tsx +29 -2
  223. package/src/app/shared/AppShellSkeleton.spec.tsx +2 -2
  224. package/src/app/shared/AppShellSkeleton.tsx +3 -16
  225. package/src/app/shared/EnvironmentBadge.tsx +3 -4
  226. package/src/app/shared/is-builder-io-employee.ts +17 -0
  227. package/src/app/styles/agent-conversation.css +30 -17
  228. package/src/app/styles/theme-families.css +22 -5
  229. package/src/app/styles/theme-families.spec.ts +73 -0
  230. package/src/app/styles/tokens/agent-kit.css +3 -3
  231. package/src/composer/AgentSuggestionBar.overflow.spec.tsx +1 -1
  232. package/src/composer/AttachmentChip.spec.tsx +87 -0
  233. package/src/composer/PromptComposer.context.spec.tsx +8 -4
  234. package/src/composer/PromptComposer.setup-card.spec.tsx +170 -0
  235. package/src/composer/PromptComposer.tsx +125 -14
  236. package/src/composer/RealtimeVoiceMode.spec.tsx +48 -0
  237. package/src/composer/RealtimeVoiceMode.tsx +45 -2
  238. package/src/composer/TiptapComposer.tsx +25 -0
  239. package/src/composer/TiptapComposer.voice-toolbar.spec.tsx +4 -1
  240. package/src/composer/VoiceButton.tsx +8 -3
  241. package/src/composer/index.ts +4 -0
  242. package/src/composer/prompt-attachments.spec.ts +160 -8
  243. package/src/composer/prompt-attachments.ts +221 -12
  244. package/src/composer/realtime-voice-transcript.spec.ts +36 -0
  245. package/src/composer/realtime-voice-transcript.ts +56 -1
  246. package/src/composer/realtime-voice-turn-metrics.spec.ts +175 -0
  247. package/src/composer/realtime-voice-turn-metrics.ts +192 -0
  248. package/src/composer/runtime-adapters.tsx +5 -1
  249. package/src/composer/useRealtimeVoiceMode.session.spec.tsx +487 -2
  250. package/src/composer/useRealtimeVoiceMode.spec.ts +267 -65
  251. package/src/composer/useRealtimeVoiceMode.tsx +716 -139
  252. package/src/composer/voice-chat-icon.tsx +19 -0
  253. package/src/editor/DragHandle.spec.ts +217 -0
  254. package/src/editor/DragHandle.ts +214 -55
  255. package/src/editor/RegistryBlockNode.spec.ts +172 -2
  256. package/src/editor/RegistryBlockNode.tsx +91 -13
  257. package/src/editor/index.ts +4 -0
  258. package/src/styles.css +2 -0
  259. package/src/ui/dropdown-menu.tsx +4 -0
@@ -157,7 +157,7 @@ export function GettingStartedCard({
157
157
  <span
158
158
  aria-hidden
159
159
  className={cn(
160
- "flex size-4 shrink-0 items-center justify-center rounded-full border",
160
+ "flex size-4 shrink-0 items-center justify-center rounded-sm border",
161
161
  step.complete
162
162
  ? "border-primary bg-primary text-primary-foreground"
163
163
  : "border-muted-foreground/60",
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { FIRST_RUN_BUILDER_CREDITS_V2_FLAG } from "./first-run-builder-credits-flag.js";
4
+
5
+ describe("first-run Builder credits v2 feature flag", () => {
6
+ it("uses one default-off definition for every onboarding app", () => {
7
+ expect(FIRST_RUN_BUILDER_CREDITS_V2_FLAG).toMatchObject({
8
+ key: "clips-first-run-credits-v2",
9
+ defaultValue: false,
10
+ displayName: "First-run Builder credits v2",
11
+ });
12
+ });
13
+ });
@@ -0,0 +1,9 @@
1
+ import { defineFeatureFlag } from "@agent-native/core/feature-flags/registry";
2
+ import { FIRST_RUN_BUILDER_CREDITS_V2_FLAG_KEY } from "@agent-native/core/shared";
3
+
4
+ export const FIRST_RUN_BUILDER_CREDITS_V2_FLAG = defineFeatureFlag({
5
+ key: FIRST_RUN_BUILDER_CREDITS_V2_FLAG_KEY,
6
+ displayName: "First-run Builder credits v2",
7
+ description:
8
+ "Enable the account-assigned v2 credits flow across Design, Slides, and Clips.",
9
+ });
@@ -735,3 +735,112 @@ describe("AppProviders session gate", () => {
735
735
  await vi.waitFor(() => expect(document.title).toBe("Manage agent"));
736
736
  });
737
737
  });
738
+
739
+ describe("AppProviders MCP approval review", () => {
740
+ const ID = "0b7f4c1e-2f4a-4d1c-9a43-5f1e8f0d2c6b";
741
+
742
+ beforeEach(() => {
743
+ (window.location as { hash: string }).hash = `#mcpApproval=${ID}`;
744
+ });
745
+
746
+ function reviewShown() {
747
+ return vi
748
+ .mocked(window.fetch)
749
+ .mock.calls.some(([url]) => url === `/_agent-native/mcp/approvals/${ID}`);
750
+ }
751
+
752
+ it("shows the review the URL names in place of the app", () => {
753
+ useSessionMock.mockReturnValue(SIGNED_IN_SESSION);
754
+
755
+ renderProviders({ disableWebMcp: true });
756
+
757
+ expect(reviewShown()).toBe(true);
758
+ expect(container.querySelector('[data-testid="app-content"]')).toBeNull();
759
+ });
760
+
761
+ it("sends a signed-out reader to sign in and back to the same review", () => {
762
+ useSessionMock.mockReturnValue(SIGNED_OUT_SESSION);
763
+
764
+ renderProviders({});
765
+
766
+ expect(replaceMock).toHaveBeenCalledWith(
767
+ `/sign-in?c=${encodeContinuation(`/inbox#mcpApproval=${ID}`)}`,
768
+ );
769
+ });
770
+
771
+ it("lets a phone decide, where the app itself hands off to a larger screen", () => {
772
+ useSessionMock.mockReturnValue(SIGNED_IN_SESSION);
773
+ const matchMedia = window.matchMedia;
774
+ const screen = Object.getOwnPropertyDescriptors(window.screen);
775
+ window.matchMedia = ((query: string) => ({
776
+ matches: true,
777
+ media: query,
778
+ addEventListener: () => {},
779
+ removeEventListener: () => {},
780
+ addListener: () => {},
781
+ removeListener: () => {},
782
+ })) as unknown as typeof window.matchMedia;
783
+ Object.defineProperty(window.screen, "width", {
784
+ value: 390,
785
+ configurable: true,
786
+ });
787
+ Object.defineProperty(window.screen, "height", {
788
+ value: 844,
789
+ configurable: true,
790
+ });
791
+ try {
792
+ act(() => {
793
+ root.render(
794
+ <AppProviders
795
+ queryClient={new QueryClient()}
796
+ i18n={false}
797
+ toaster={null}
798
+ disableWebMcp
799
+ unsupportedDevice={{ appName: "Slides" }}
800
+ >
801
+ <div data-testid="app-content">content</div>
802
+ </AppProviders>,
803
+ );
804
+ });
805
+
806
+ expect(reviewShown()).toBe(true);
807
+ } finally {
808
+ window.matchMedia = matchMedia;
809
+ for (const key of ["width", "height"] as const) {
810
+ if (screen[key]) Object.defineProperty(window.screen, key, screen[key]);
811
+ else delete (window.screen as unknown as Record<string, unknown>)[key];
812
+ }
813
+ }
814
+ });
815
+
816
+ it("stays out of the server render, which has no window to read the review from", () => {
817
+ useSessionMock.mockReturnValue(SIGNED_IN_SESSION);
818
+ vi.stubGlobal("window", undefined);
819
+ let markup: string;
820
+ try {
821
+ markup = renderToStaticMarkup(
822
+ <AppProviders queryClient={new QueryClient()} i18n={false}>
823
+ <div>content</div>
824
+ </AppProviders>,
825
+ );
826
+ } finally {
827
+ vi.unstubAllGlobals();
828
+ }
829
+
830
+ expect(markup).toContain('data-agent-native-app-skeleton="true"');
831
+ });
832
+
833
+ it.each([
834
+ ["a token-authenticated surface", { sessionBypass: true }],
835
+ ["a public page", { isPublicPath: true }],
836
+ ])("never shows a review on %s", (_label, props) => {
837
+ useSessionMock.mockReturnValue(SIGNED_IN_SESSION);
838
+
839
+ renderProviders({ ...props, disableWebMcp: true });
840
+
841
+ expect(reviewShown()).toBe(false);
842
+ expect(
843
+ container.querySelector('[data-testid="app-content"]'),
844
+ ).not.toBeNull();
845
+ });
846
+ });
@@ -33,6 +33,10 @@ import React, { useContext, useEffect, useRef } from "react";
33
33
  import { useInRouterContext } from "react-router";
34
34
 
35
35
  import { RequireSession } from "../auth/RequireSession.js";
36
+ import {
37
+ McpApprovalReviewGate,
38
+ McpApprovalReviewSkeleton,
39
+ } from "../mcp-approval/McpApprovalReview.js";
36
40
  import { FirstRunOnboardingStartupGate } from "../onboarding/first-run-startup-gate.js";
37
41
  import {
38
42
  AppShellSkeleton,
@@ -416,6 +420,40 @@ function AgentRunAwareness({ persistUnread }: { persistUnread?: boolean }) {
416
420
  );
417
421
  }
418
422
 
423
+ /**
424
+ * An MCP approval review the URL names replaces the app. It sits outside the
425
+ * device gate, so a phone can decide, and outside first-run onboarding, so a
426
+ * new user can decide before setting up the app.
427
+ */
428
+ function McpApprovalReviewBoundary({
429
+ appName,
430
+ themeFamily,
431
+ children,
432
+ }: {
433
+ appName: string | undefined;
434
+ themeFamily: boolean;
435
+ children: React.ReactNode;
436
+ }) {
437
+ const name = appName ?? readDocumentTitleFallback();
438
+ return (
439
+ <McpApprovalReviewGate
440
+ appName={name}
441
+ wrap={(review) => (
442
+ <RequireSession fallback={<McpApprovalReviewSkeleton appName={name} />}>
443
+ {themeFamily ? (
444
+ <React.Suspense fallback={null}>
445
+ <ThemeFamilySync />
446
+ </React.Suspense>
447
+ ) : null}
448
+ {review}
449
+ </RequireSession>
450
+ )}
451
+ >
452
+ {children}
453
+ </McpApprovalReviewGate>
454
+ );
455
+ }
456
+
419
457
  function ProvidersInner({
420
458
  queryClient,
421
459
  defaultTheme = "system",
@@ -589,7 +627,7 @@ export function AppProviders({
589
627
  </RequireSession>
590
628
  );
591
629
 
592
- const gated = unsupportedDevice ? (
630
+ const deviceGated = unsupportedDevice ? (
593
631
  <UnsupportedDeviceGate {...unsupportedDevice}>
594
632
  {sessionGated}
595
633
  </UnsupportedDeviceGate>
@@ -597,6 +635,20 @@ export function AppProviders({
597
635
  sessionGated
598
636
  );
599
637
 
638
+ // A token-authenticated embed or an app inside another app's document
639
+ // never shows a review: neither is the reader's own top-level page.
640
+ const gated =
641
+ sessionBypass || !ownsDocument ? (
642
+ deviceGated
643
+ ) : (
644
+ <McpApprovalReviewBoundary
645
+ appName={documentTitleFallback}
646
+ themeFamily={enableThemeFamily}
647
+ >
648
+ {deviceGated}
649
+ </McpApprovalReviewBoundary>
650
+ );
651
+
600
652
  return (
601
653
  <>
602
654
  {ownsDocument ? <McpAppWidgetBootScript /> : null}
@@ -73,34 +73,99 @@ describe("findMcpConnectionSuggestionIntegration", () => {
73
73
  variant: "response",
74
74
  })?.id,
75
75
  ).toBe("slack");
76
+ });
77
+
78
+ it("reads provider names through markdown emphasis", () => {
79
+ for (const [text, id] of [
80
+ ["Please connect **Slack** to continue.", "slack"],
81
+ ["**Slack** isn't connected.", "slack"],
82
+ ["You'll need to connect `Notion` first.", "notion"],
83
+ ] as const) {
84
+ expect(
85
+ findMcpConnectionSuggestionIntegration({ text, variant: "response" })
86
+ ?.id,
87
+ ).toBe(id);
88
+ }
89
+ });
90
+
91
+ it("does not read a provider name inside a longer word", () => {
76
92
  expect(
77
93
  findMcpConnectionSuggestionIntegration({
78
- text: "I couldn’t complete the Slack auth.test call because the Dispatch connection requires authentication.",
79
- contextText:
80
- "Use Slack auth.test and surface a connection request if needed.",
94
+ text: "The NonLinear isn't connected yet.",
95
+ variant: "response",
96
+ }),
97
+ ).toBeNull();
98
+ });
99
+
100
+ it("selects a provider whose access is required", () => {
101
+ expect(
102
+ findMcpConnectionSuggestionIntegration({
103
+ text: "Notion access is required to continue.",
81
104
  variant: "response",
82
105
  })?.id,
83
- ).toBe("slack");
106
+ ).toBe("notion");
84
107
  });
85
108
 
86
- it("uses the preceding user mention when the agent asks to connect it", () => {
109
+ it("selects the provider a real agent reply asks to connect", () => {
87
110
  expect(
88
111
  findMcpConnectionSuggestionIntegration({
89
- text: "I don't have access yet - please connect it to continue.",
90
- contextText: "Pull the latest HubSpot deals.",
112
+ text: "Slack isn’t connected, so `auth.test` couldn’t be run. I opened **Settings → Channels → Slack** to surface the connection setup. Connect a Slack workspace there, then I can test it.",
91
113
  variant: "response",
92
114
  })?.id,
93
- ).toBe("hubspot");
115
+ ).toBe("slack");
116
+ expect(
117
+ findMcpConnectionSuggestionIntegration({
118
+ text: "I opened **Settings → Integrations**. Connect Notion there, then tell me when it’s connected and which project page to open.",
119
+ variant: "response",
120
+ })?.id,
121
+ ).toBe("notion");
94
122
  });
95
123
 
96
- it("selects response connections only from the user's branded phrase", () => {
124
+ it("selects the provider a sent message asks to connect", () => {
97
125
  expect(
98
126
  findMcpConnectionSuggestionIntegration({
99
- text: "I cannot read that page.",
100
- contextText: "Connect Notion and open the project page.",
127
+ text: "Connect Notion and open the project page.",
101
128
  variant: "response",
102
129
  })?.id,
103
130
  ).toBe("notion");
131
+ expect(
132
+ findMcpConnectionSuggestionIntegration({
133
+ text: "Can you connect my Slack?",
134
+ variant: "response",
135
+ })?.id,
136
+ ).toBe("slack");
137
+ });
138
+
139
+ it("never offers a connection for a prompt that only mentions a provider or links to it", () => {
140
+ const prompt =
141
+ "You are designing a production-ready HTML email for Builder.io. Primary button: Continue your build (href https://www.builder.io/app/projects). Secondary link: More prompt tips. EMAIL-SAFE REQUIREMENTS (HubSpot). If you cannot extract a value, ask me.";
142
+ expect(
143
+ findMcpConnectionSuggestionIntegration({
144
+ text: prompt,
145
+ variant: "response",
146
+ }),
147
+ ).toBeNull();
148
+ expect(
149
+ findMcpConnectionSuggestionIntegration({
150
+ text: "I updated the secondary link. No changes to your copy.",
151
+ contextText: prompt,
152
+ variant: "response",
153
+ }),
154
+ ).toBeNull();
155
+ });
156
+
157
+ it("does not read product copy, quoted labels, or look-alike words as a request", () => {
158
+ for (const text of [
159
+ "Build an onboarding flow. Step 3: users can connect Slack from settings to get alerts.",
160
+ "The CTA reads “Connect Slack”.",
161
+ "No need to connect GitHub, here's the diff:",
162
+ "Connect the linear gradient to the hero background.",
163
+ "Connect Linear-style progress bars to each step.",
164
+ ]) {
165
+ expect(
166
+ findMcpConnectionSuggestionIntegration({ text, variant: "response" }),
167
+ ).toBeNull();
168
+ }
104
169
  });
105
170
 
106
171
  it("ignores provider names and URLs inside hidden composer context", () => {
@@ -156,7 +221,7 @@ describe("findMcpConnectionSuggestionIntegration", () => {
156
221
  expect(
157
222
  findMcpConnectionSuggestionIntegration({
158
223
  text: "Connect Gong so I can query calls.",
159
- requestedByUser: true,
224
+ variant: "response",
160
225
  excludedIntegrationIds: ["GONG"],
161
226
  }),
162
227
  ).toBeNull();
@@ -5,12 +5,9 @@ import {
5
5
  saveMcpConnectionResume,
6
6
  } from "@agent-native/core/client/resources/mcp-connection-resume";
7
7
  import {
8
- findMcpIntegrationForText,
9
8
  getMcpIntegrationApiFallback,
10
9
  getDefaultMcpIntegrations,
11
10
  isMcpIntegrationUrl,
12
- isMcpConnectionFailureText,
13
- isMcpConnectionSuggestionText,
14
11
  type DefaultMcpIntegration,
15
12
  } from "@agent-native/core/client/resources/mcp-integration-catalog";
16
13
  import {
@@ -33,8 +30,6 @@ export interface McpConnectionSuggestionProps {
33
30
  contextText?: string;
34
31
  variant?: McpConnectionSuggestionVariant;
35
32
  requestedByAgent?: boolean;
36
- /** Offer a composer connection card when the user names a known provider. */
37
- requestedByUser?: boolean;
38
33
  integrations?: DefaultMcpIntegration[];
39
34
  excludedIntegrationIds?: readonly string[];
40
35
  integrationId?: string;
@@ -119,12 +114,73 @@ function visibleUserAuthoredText(text: string): string {
119
114
  .trim();
120
115
  }
121
116
 
117
+ // A request reads "please connect Slack", "you'll need to reconnect Linear",
118
+ // or "Notion isn't connected". Product copy ("Users can connect Slack"), quoted
119
+ // button labels ("Connect Notion"), negations ("no need to connect GitHub"),
120
+ // and links never count: pasted prompts and drafted copy are full of them.
121
+ const CONNECT_REQUEST_LEAD = String.raw`(?:^|[.!?]\s+|\bplease\s+|\b(?:you|you['’]ll|you\s+will)\s+(?:need|have)\s+to\s+|\byou\s+(?:must|should)\s+|\bneed\s+you\s+to\s+|\b(?:can|could|would|will)\s+you(?:\s+please)?\s+|\b(?:if|once|when|after)\s+you\s+|\b(?:to\s+continue|first|then|just),?\s+|,\s+|\b(?:settings|integrations)\s+and\s+|\b(?:i|we)(?:\s+(?:want|need|would\s+like)|['’]d\s+like)\s+to\s+|\bhelp\s+(?:me|us)\s+|\bhow\s+(?:do|can)\s+(?:i|we)\s+|\blet['’]?s\s+)`;
122
+ const CONNECT_REQUEST_VERB = String.raw`(?:re-?)?(?:connect|authori[sz]e)|(?:sign|log)\s+in(?:to|\s+to)`;
123
+ const CONNECTION_MISSING = String.raw`(?:(?:is|has|was)(?:\s+not|n['’]t)(?:\s+(?:been|yet|currently))?\s+connected|(?:access|connection)\s+(?:(?:has\s+)?expired|(?:is\s+)?required))`;
124
+
125
+ function escapeRegExp(value: string): string {
126
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
127
+ }
128
+
129
+ function findRequestedMcpIntegration(
130
+ markdown: string,
131
+ integrations: readonly DefaultMcpIntegration[],
132
+ ): DefaultMcpIntegration | null {
133
+ // Agents bold or code-format the provider: "please connect **Slack**".
134
+ const text = markdown.replace(/\*\*|__|`/g, "");
135
+ if (!/connect|authori[sz]e|sign\s+in|log\s+in|expired|required/i.test(text)) {
136
+ return null;
137
+ }
138
+ const byName = new Map<
139
+ string,
140
+ { integration: DefaultMcpIntegration; name: string }
141
+ >();
142
+ for (const integration of integrations) {
143
+ for (const alias of [
144
+ ...(integration.promptAliases ?? [integration.name]),
145
+ ...(integration.brandAliases ?? []),
146
+ ]) {
147
+ const name = alias.replace(/^connect\s+/i, "");
148
+ byName.set(name.toLowerCase(), { integration, name });
149
+ }
150
+ }
151
+ if (byName.size === 0) return null;
152
+ const names = `(${[...byName.values()]
153
+ .map(({ name }) => name)
154
+ .sort((a, b) => b.length - a.length)
155
+ .map(escapeRegExp)
156
+ .join("|")})(?![\\w-])`;
157
+ const patterns = [
158
+ new RegExp(
159
+ `(?<=${CONNECT_REQUEST_LEAD})(?:${CONNECT_REQUEST_VERB})\\s+(?:to\\s+|with\\s+)?(?:my\\s+|our\\s+|your\\s+|the\\s+)?${names}`,
160
+ "gi",
161
+ ),
162
+ ];
163
+ if (/\b(?:connected|expired|required)\b/i.test(text)) {
164
+ patterns.push(
165
+ new RegExp(`(?<![\\w-])${names}\\s+${CONNECTION_MISSING}`, "gi"),
166
+ );
167
+ }
168
+ for (const pattern of patterns) {
169
+ for (const match of text.matchAll(pattern)) {
170
+ const found = match.at(1);
171
+ if (!found) continue;
172
+ const entry = byName.get(found.toLowerCase());
173
+ // Brand names are capitalized: "linear gradient" is not Linear.
174
+ if (entry && found[0] === entry.name[0]) return entry.integration;
175
+ }
176
+ }
177
+ return null;
178
+ }
179
+
122
180
  export function findMcpConnectionSuggestionIntegration({
123
181
  text,
124
- contextText = "",
125
182
  variant = "composer",
126
183
  requestedByAgent = false,
127
- requestedByUser = false,
128
184
  integrations = getDefaultMcpIntegrations(),
129
185
  excludedIntegrationIds,
130
186
  integrationId,
@@ -147,37 +203,11 @@ export function findMcpConnectionSuggestionIntegration({
147
203
  ) ?? null
148
204
  );
149
205
  }
150
- const responseText = visibleUserAuthoredText(text);
151
- if (variant !== "response") {
152
- if (requestedByUser) {
153
- return findMcpIntegrationForText(responseText, availableIntegrations);
154
- }
155
- if (requestedByAgent) {
156
- if (
157
- !isMcpConnectionSuggestionText(responseText) &&
158
- !isMcpConnectionFailureText(responseText)
159
- ) {
160
- return null;
161
- }
162
- } else {
163
- return null;
164
- }
165
- }
166
-
167
- if (
168
- isMcpConnectionSuggestionText(responseText) ||
169
- isMcpConnectionFailureText(responseText)
170
- ) {
171
- return (
172
- findMcpIntegrationForText(responseText, availableIntegrations) ??
173
- findMcpIntegrationForText(
174
- visibleUserAuthoredText(contextText),
175
- availableIntegrations,
176
- )
177
- );
178
- }
179
-
180
- return null;
206
+ if (variant !== "response" && !requestedByAgent) return null;
207
+ return findRequestedMcpIntegration(
208
+ visibleUserAuthoredText(text),
209
+ availableIntegrations,
210
+ );
181
211
  }
182
212
 
183
213
  export function shouldRenderMcpIntegrationFallback(
@@ -209,7 +239,6 @@ export function McpConnectionSuggestion({
209
239
  contextText = "",
210
240
  variant = "composer",
211
241
  requestedByAgent = false,
212
- requestedByUser = false,
213
242
  integrations: integrationOptions,
214
243
  excludedIntegrationIds,
215
244
  integrationId,
@@ -238,21 +267,17 @@ export function McpConnectionSuggestion({
238
267
  () =>
239
268
  findMcpConnectionSuggestionIntegration({
240
269
  text,
241
- contextText,
242
270
  variant,
243
271
  requestedByAgent,
244
- requestedByUser,
245
272
  integrations,
246
273
  excludedIntegrationIds,
247
274
  integrationId,
248
275
  }),
249
276
  [
250
- contextText,
251
277
  excludedIntegrationIds,
252
278
  integrationId,
253
279
  integrations,
254
280
  requestedByAgent,
255
- requestedByUser,
256
281
  text,
257
282
  variant,
258
283
  ],
@@ -281,14 +306,7 @@ export function McpConnectionSuggestion({
281
306
  dismissedAt !== undefined &&
282
307
  Date.now() - dismissedAt < MCP_SUGGESTION_DISMISSAL_WINDOW_MS;
283
308
  const shouldSuggest =
284
- mcpServersQuery.isSuccess &&
285
- integration &&
286
- !connected &&
287
- !dismissed &&
288
- (Boolean(integrationId) ||
289
- variant === "composer" ||
290
- isMcpConnectionFailureText(text) ||
291
- isMcpConnectionSuggestionText(text));
309
+ mcpServersQuery.isSuccess && integration && !connected && !dismissed;
292
310
 
293
311
  useEffect(() => {
294
312
  if (integration && connected) onConnected?.();
@@ -165,7 +165,12 @@ describe("VoiceChatSettings", () => {
165
165
 
166
166
  expect(appState.set).toHaveBeenLastCalledWith(
167
167
  REALTIME_VOICE_PREFERENCES_KEY,
168
- { language: "ja", intelligence: "deep", voice: "marin" },
168
+ {
169
+ language: "ja",
170
+ intelligence: "deep",
171
+ voice: "marin",
172
+ intelligenceChosen: true,
173
+ },
169
174
  { requestSource: "realtime-voice" },
170
175
  );
171
176
  expect(select("Voice style")?.value).toBe("marin");
@@ -2,14 +2,35 @@
2
2
 
3
3
  import React, { act } from "react";
4
4
  import { createRoot, type Root } from "react-dom/client";
5
+ import { renderToString } from "react-dom/server";
5
6
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
7
 
7
8
  const localeState = vi.hoisted(() => ({ value: {} as object | null }));
9
+ const sessionState = vi.hoisted(() => ({
10
+ value: {
11
+ session: null as { email?: string } | null,
12
+ status: "loading" as "loading" | "authenticated" | "unauthenticated",
13
+ },
14
+ }));
15
+ const appConfigState = vi.hoisted(() => ({
16
+ value: {
17
+ themeFamily: {
18
+ families: ["original", "agent-native"],
19
+ defaultFamily: "agent-native",
20
+ },
21
+ } as object,
22
+ }));
8
23
 
9
24
  vi.mock("@agent-native/core/client/i18n", async (importOriginal) => ({
10
25
  ...(await importOriginal<typeof import("@agent-native/core/client/i18n")>()),
11
26
  useOptionalLocale: () => localeState.value,
12
27
  }));
28
+ vi.mock("@agent-native/core/client/use-session", () => ({
29
+ useSession: () => sessionState.value,
30
+ }));
31
+ vi.mock("@agent-native/core/client/app-config", () => ({
32
+ injectedAgentNativeConfig: () => appConfigState.value,
33
+ }));
13
34
  vi.mock("../../shared/index.js", () => ({
14
35
  LanguagePicker: ({ label }: { label?: string }) => (
15
36
  <button type="button" data-testid="language-picker">
@@ -52,6 +73,16 @@ describe("PreferencesSettings", () => {
52
73
  beforeEach(() => {
53
74
  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
54
75
  localeState.value = {};
76
+ sessionState.value = {
77
+ session: { email: "steve@builder.io" },
78
+ status: "authenticated",
79
+ };
80
+ appConfigState.value = {
81
+ themeFamily: {
82
+ families: ["original", "agent-native"],
83
+ defaultFamily: "agent-native",
84
+ },
85
+ };
55
86
  container = document.createElement("div");
56
87
  document.body.appendChild(container);
57
88
  root = createRoot(container);
@@ -125,6 +156,48 @@ describe("PreferencesSettings", () => {
125
156
  expect(container.querySelector("#timezone")).not.toBeNull();
126
157
  });
127
158
 
159
+ it("never renders the theme switch during server rendering", () => {
160
+ const html = renderToString(
161
+ <AgentNativeI18nProvider
162
+ catalog={toolkitI18nCatalog}
163
+ persistPreference={false}
164
+ >
165
+ <PreferencesSettings />
166
+ </AgentNativeI18nProvider>,
167
+ );
168
+
169
+ expect(html).not.toContain('id="theme-family"');
170
+ });
171
+
172
+ it("never shows the theme switch to a non-Builder account", async () => {
173
+ sessionState.value = {
174
+ session: { email: "person@example.com" },
175
+ status: "authenticated",
176
+ };
177
+ await render();
178
+
179
+ expect(container.querySelector("#theme-family")).toBeNull();
180
+ });
181
+
182
+ it("hides the theme switch when an app declares only its original family", async () => {
183
+ appConfigState.value = {
184
+ themeFamily: { families: ["original"], defaultFamily: "original" },
185
+ };
186
+ await render();
187
+
188
+ expect(container.querySelector("#theme-family")).toBeNull();
189
+ });
190
+
191
+ it("keeps the theme switch hidden until the signed-in session is loaded", async () => {
192
+ sessionState.value = {
193
+ session: null,
194
+ status: "loading",
195
+ };
196
+ await render();
197
+
198
+ expect(container.querySelector("#theme-family")).toBeNull();
199
+ });
200
+
128
201
  it("anchors every Preferences search entry to a row", async () => {
129
202
  await render();
130
203
  for (const entry of PREFERENCES_SEARCH_ENTRIES) {