@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.
- package/agent-native.eject.json +16 -1
- package/dist/app/agentkit/react/components.d.ts +5 -3
- package/dist/app/agentkit/react/components.d.ts.map +1 -1
- package/dist/app/agentkit/react/components.js +103 -42
- package/dist/app/agentkit/react/components.js.map +1 -1
- package/dist/app/agentkit/react/styles.css +93 -65
- package/dist/app/agentkit/react/voice-transcripts.d.ts +22 -1
- package/dist/app/agentkit/react/voice-transcripts.d.ts.map +1 -1
- package/dist/app/agentkit/react/voice-transcripts.js +209 -8
- package/dist/app/agentkit/react/voice-transcripts.js.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +55 -40
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/chat/agentkit-chat/guided-questions.d.ts +1 -0
- package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
- package/dist/app/chat/agentkit-chat/guided-questions.js +18 -6
- package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
- package/dist/app/chat/agentkit-chat/index.d.ts +1 -1
- package/dist/app/chat/agentkit-chat/index.d.ts.map +1 -1
- package/dist/app/chat/agentkit-chat/index.js +1 -1
- package/dist/app/chat/agentkit-chat/index.js.map +1 -1
- package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
- package/dist/app/chat/chat/run-recovery.js +5 -1
- package/dist/app/chat/chat/run-recovery.js.map +1 -1
- package/dist/app/chat/composer/runtime-adapters.d.ts.map +1 -1
- package/dist/app/chat/composer/runtime-adapters.js +2 -0
- package/dist/app/chat/composer/runtime-adapters.js.map +1 -1
- package/dist/app/chat/reply-connection-suggestion.d.ts +29 -0
- package/dist/app/chat/reply-connection-suggestion.d.ts.map +1 -0
- package/dist/app/chat/reply-connection-suggestion.js +42 -0
- package/dist/app/chat/reply-connection-suggestion.js.map +1 -0
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +7 -0
- package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.js +7 -0
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +7 -0
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +7 -0
- package/dist/app/i18n/catalogs/en-US.js.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.js +7 -0
- package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.js +7 -0
- package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.js +7 -0
- package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.js +7 -0
- package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.js +7 -0
- package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.js +7 -0
- package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.js +7 -0
- package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.js +7 -0
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/app/mcp-approval/McpApprovalReview.d.ts +25 -0
- package/dist/app/mcp-approval/McpApprovalReview.d.ts.map +1 -0
- package/dist/app/mcp-approval/McpApprovalReview.js +342 -0
- package/dist/app/mcp-approval/McpApprovalReview.js.map +1 -0
- package/dist/app/onboarding/FirstRunOnboarding.d.ts.map +1 -1
- package/dist/app/onboarding/FirstRunOnboarding.js +299 -71
- package/dist/app/onboarding/FirstRunOnboarding.js.map +1 -1
- package/dist/app/onboarding/GettingStartedCard.js +1 -1
- package/dist/app/onboarding/GettingStartedCard.js.map +1 -1
- package/dist/app/onboarding/first-run-builder-credits-flag.d.ts +2 -0
- package/dist/app/onboarding/first-run-builder-credits-flag.d.ts.map +1 -0
- package/dist/app/onboarding/first-run-builder-credits-flag.js +8 -0
- package/dist/app/onboarding/first-run-builder-credits-flag.js.map +1 -0
- package/dist/app/providers/AppProviders.d.ts.map +1 -1
- package/dist/app/providers/AppProviders.js +14 -1
- package/dist/app/providers/AppProviders.js.map +1 -1
- package/dist/app/resources/McpConnectionSuggestion.d.ts +2 -4
- package/dist/app/resources/McpConnectionSuggestion.d.ts.map +1 -1
- package/dist/app/resources/McpConnectionSuggestion.js +59 -36
- package/dist/app/resources/McpConnectionSuggestion.js.map +1 -1
- package/dist/app/settings/account/PreferencesSettings.d.ts.map +1 -1
- package/dist/app/settings/account/PreferencesSettings.js +14 -1
- package/dist/app/settings/account/PreferencesSettings.js.map +1 -1
- package/dist/app/settings/account/ThemeFamilySettings.js +2 -2
- package/dist/app/settings/account/ThemeFamilySettings.js.map +1 -1
- package/dist/app/settings/account/search-entries.d.ts +1 -0
- package/dist/app/settings/account/search-entries.d.ts.map +1 -1
- package/dist/app/settings/account/search-entries.js +5 -0
- package/dist/app/settings/account/search-entries.js.map +1 -1
- package/dist/app/settings/shell/SettingsShell.d.ts.map +1 -1
- package/dist/app/settings/shell/SettingsShell.js +26 -2
- package/dist/app/settings/shell/SettingsShell.js.map +1 -1
- package/dist/app/shared/AppShellSkeleton.d.ts.map +1 -1
- package/dist/app/shared/AppShellSkeleton.js +4 -10
- package/dist/app/shared/AppShellSkeleton.js.map +1 -1
- package/dist/app/shared/EnvironmentBadge.d.ts +2 -1
- package/dist/app/shared/EnvironmentBadge.d.ts.map +1 -1
- package/dist/app/shared/EnvironmentBadge.js +2 -3
- package/dist/app/shared/EnvironmentBadge.js.map +1 -1
- package/dist/app/shared/is-builder-io-employee.d.ts +8 -0
- package/dist/app/shared/is-builder-io-employee.d.ts.map +1 -0
- package/dist/app/shared/is-builder-io-employee.js +10 -0
- package/dist/app/shared/is-builder-io-employee.js.map +1 -0
- package/dist/app/styles/agent-conversation.css +30 -17
- package/dist/app/styles/theme-families.css +22 -5
- package/dist/app/styles/tokens/agent-kit.css +3 -3
- package/dist/composer/PromptComposer.d.ts +15 -3
- package/dist/composer/PromptComposer.d.ts.map +1 -1
- package/dist/composer/PromptComposer.js +74 -16
- package/dist/composer/PromptComposer.js.map +1 -1
- package/dist/composer/RealtimeVoiceMode.d.ts +7 -0
- package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/RealtimeVoiceMode.js +9 -2
- package/dist/composer/RealtimeVoiceMode.js.map +1 -1
- package/dist/composer/TiptapComposer.d.ts +7 -0
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +12 -4
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/dist/composer/VoiceButton.d.ts +2 -0
- package/dist/composer/VoiceButton.d.ts.map +1 -1
- package/dist/composer/VoiceButton.js +5 -4
- package/dist/composer/VoiceButton.js.map +1 -1
- package/dist/composer/index.d.ts +2 -2
- package/dist/composer/index.d.ts.map +1 -1
- package/dist/composer/index.js +1 -1
- package/dist/composer/index.js.map +1 -1
- package/dist/composer/prompt-attachments.d.ts +20 -0
- package/dist/composer/prompt-attachments.d.ts.map +1 -1
- package/dist/composer/prompt-attachments.js +188 -11
- package/dist/composer/prompt-attachments.js.map +1 -1
- package/dist/composer/realtime-voice-transcript.d.ts +27 -0
- package/dist/composer/realtime-voice-transcript.d.ts.map +1 -1
- package/dist/composer/realtime-voice-transcript.js +34 -1
- package/dist/composer/realtime-voice-transcript.js.map +1 -1
- package/dist/composer/realtime-voice-turn-metrics.d.ts +43 -0
- package/dist/composer/realtime-voice-turn-metrics.d.ts.map +1 -0
- package/dist/composer/realtime-voice-turn-metrics.js +132 -0
- package/dist/composer/realtime-voice-turn-metrics.js.map +1 -0
- package/dist/composer/runtime-adapters.d.ts +4 -1
- package/dist/composer/runtime-adapters.d.ts.map +1 -1
- package/dist/composer/runtime-adapters.js +2 -1
- package/dist/composer/runtime-adapters.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.d.ts +61 -9
- package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.js +536 -99
- package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
- package/dist/composer/voice-chat-icon.d.ts +7 -0
- package/dist/composer/voice-chat-icon.d.ts.map +1 -0
- package/dist/composer/voice-chat-icon.js +14 -0
- package/dist/composer/voice-chat-icon.js.map +1 -0
- package/dist/editor/DragHandle.d.ts +12 -1
- package/dist/editor/DragHandle.d.ts.map +1 -1
- package/dist/editor/DragHandle.js +165 -41
- package/dist/editor/DragHandle.js.map +1 -1
- package/dist/editor/RegistryBlockNode.d.ts +20 -0
- package/dist/editor/RegistryBlockNode.d.ts.map +1 -1
- package/dist/editor/RegistryBlockNode.js +58 -13
- package/dist/editor/RegistryBlockNode.js.map +1 -1
- package/dist/editor/index.d.ts +2 -2
- package/dist/editor/index.d.ts.map +1 -1
- package/dist/editor/index.js +1 -1
- package/dist/editor/index.js.map +1 -1
- package/dist/styles.css +2 -0
- package/dist/ui/dropdown-menu.d.ts.map +1 -1
- package/dist/ui/dropdown-menu.js +5 -1
- package/dist/ui/dropdown-menu.js.map +1 -1
- package/package.json +14 -4
- package/src/app/agentkit/react/components.regenerate-readiness.spec.tsx +3 -1
- package/src/app/agentkit/react/components.spec.tsx +24 -4
- package/src/app/agentkit/react/components.tsx +174 -44
- package/src/app/agentkit/react/composer-context.spec.tsx +3 -0
- package/src/app/agentkit/react/disclosures.spec.tsx +124 -0
- package/src/app/agentkit/react/lifecycle.spec.tsx +8 -2
- package/src/app/agentkit/react/message-actions.spec.tsx +161 -5
- package/src/app/agentkit/react/saved-reply-reload.spec.tsx +206 -0
- package/src/app/agentkit/react/styles.css +93 -65
- package/src/app/agentkit/react/voice-transcripts.spec.tsx +239 -4
- package/src/app/agentkit/react/voice-transcripts.ts +245 -8
- package/src/app/chat/AgentKitAssistantChat.spec.tsx +59 -11
- package/src/app/chat/AgentKitAssistantChat.tsx +74 -62
- package/src/app/chat/agentkit-chat/guided-answers-context.spec.ts +9 -0
- package/src/app/chat/agentkit-chat/guided-questions.tsx +17 -7
- package/src/app/chat/agentkit-chat/index.ts +1 -0
- package/src/app/chat/chat/run-recovery.spec.tsx +45 -0
- package/src/app/chat/chat/run-recovery.tsx +6 -1
- package/src/app/chat/composer/runtime-adapters.tsx +2 -0
- package/src/app/chat/reply-connection-suggestion.spec.ts +121 -0
- package/src/app/chat/reply-connection-suggestion.ts +76 -0
- package/src/app/i18n/catalogs/ar-SA.ts +10 -0
- package/src/app/i18n/catalogs/de-DE.ts +10 -0
- package/src/app/i18n/catalogs/en-US.ts +10 -0
- package/src/app/i18n/catalogs/es-ES.ts +10 -0
- package/src/app/i18n/catalogs/fr-FR.ts +10 -0
- package/src/app/i18n/catalogs/hi-IN.ts +10 -0
- package/src/app/i18n/catalogs/ja-JP.ts +10 -0
- package/src/app/i18n/catalogs/ko-KR.ts +10 -0
- package/src/app/i18n/catalogs/pt-BR.ts +10 -0
- package/src/app/i18n/catalogs/zh-CN.ts +10 -0
- package/src/app/i18n/catalogs/zh-TW.ts +10 -0
- package/src/app/mcp-approval/McpApprovalReview.spec.tsx +687 -0
- package/src/app/mcp-approval/McpApprovalReview.tsx +913 -0
- package/src/app/onboarding/FirstRunOnboarding.spec.tsx +736 -26
- package/src/app/onboarding/FirstRunOnboarding.tsx +473 -145
- package/src/app/onboarding/GettingStartedCard.tsx +1 -1
- package/src/app/onboarding/first-run-builder-credits-flag.spec.ts +13 -0
- package/src/app/onboarding/first-run-builder-credits-flag.ts +9 -0
- package/src/app/providers/AppProviders.spec.tsx +109 -0
- package/src/app/providers/AppProviders.tsx +53 -1
- package/src/app/resources/McpConnectionSuggestion.spec.ts +77 -12
- package/src/app/resources/McpConnectionSuggestion.tsx +69 -51
- package/src/app/settings/VoiceChatSettings.spec.tsx +6 -1
- package/src/app/settings/account/PreferencesSettings.spec.tsx +73 -0
- package/src/app/settings/account/PreferencesSettings.tsx +24 -7
- package/src/app/settings/account/ThemeFamilySettings.tsx +2 -2
- package/src/app/settings/account/search-entries.spec.ts +14 -0
- package/src/app/settings/account/search-entries.ts +9 -0
- package/src/app/settings/shell/SettingsShell.tsx +29 -2
- package/src/app/shared/AppShellSkeleton.spec.tsx +2 -2
- package/src/app/shared/AppShellSkeleton.tsx +3 -16
- package/src/app/shared/EnvironmentBadge.tsx +3 -4
- package/src/app/shared/is-builder-io-employee.ts +17 -0
- package/src/app/styles/agent-conversation.css +30 -17
- package/src/app/styles/theme-families.css +22 -5
- package/src/app/styles/theme-families.spec.ts +73 -0
- package/src/app/styles/tokens/agent-kit.css +3 -3
- package/src/composer/AgentSuggestionBar.overflow.spec.tsx +1 -1
- package/src/composer/AttachmentChip.spec.tsx +87 -0
- package/src/composer/PromptComposer.context.spec.tsx +8 -4
- package/src/composer/PromptComposer.setup-card.spec.tsx +170 -0
- package/src/composer/PromptComposer.tsx +125 -14
- package/src/composer/RealtimeVoiceMode.spec.tsx +48 -0
- package/src/composer/RealtimeVoiceMode.tsx +45 -2
- package/src/composer/TiptapComposer.tsx +25 -0
- package/src/composer/TiptapComposer.voice-toolbar.spec.tsx +4 -1
- package/src/composer/VoiceButton.tsx +8 -3
- package/src/composer/index.ts +4 -0
- package/src/composer/prompt-attachments.spec.ts +160 -8
- package/src/composer/prompt-attachments.ts +221 -12
- package/src/composer/realtime-voice-transcript.spec.ts +36 -0
- package/src/composer/realtime-voice-transcript.ts +56 -1
- package/src/composer/realtime-voice-turn-metrics.spec.ts +175 -0
- package/src/composer/realtime-voice-turn-metrics.ts +192 -0
- package/src/composer/runtime-adapters.tsx +5 -1
- package/src/composer/useRealtimeVoiceMode.session.spec.tsx +487 -2
- package/src/composer/useRealtimeVoiceMode.spec.ts +267 -65
- package/src/composer/useRealtimeVoiceMode.tsx +716 -139
- package/src/composer/voice-chat-icon.tsx +19 -0
- package/src/editor/DragHandle.spec.ts +217 -0
- package/src/editor/DragHandle.ts +214 -55
- package/src/editor/RegistryBlockNode.spec.ts +172 -2
- package/src/editor/RegistryBlockNode.tsx +91 -13
- package/src/editor/index.ts +4 -0
- package/src/styles.css +2 -0
- 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-
|
|
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
|
|
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: "
|
|
79
|
-
|
|
80
|
-
|
|
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("
|
|
106
|
+
).toBe("notion");
|
|
84
107
|
});
|
|
85
108
|
|
|
86
|
-
it("
|
|
109
|
+
it("selects the provider a real agent reply asks to connect", () => {
|
|
87
110
|
expect(
|
|
88
111
|
findMcpConnectionSuggestionIntegration({
|
|
89
|
-
text: "
|
|
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("
|
|
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
|
|
124
|
+
it("selects the provider a sent message asks to connect", () => {
|
|
97
125
|
expect(
|
|
98
126
|
findMcpConnectionSuggestionIntegration({
|
|
99
|
-
text: "
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
{
|
|
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) {
|