diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 048141704e..9c3f54a343 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -930,6 +930,9 @@ importers: webview-ui: dependencies: + '@clerk/clerk-react': + specifier: ^5.47.0 + version: 5.47.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-alert-dialog': specifier: ^1.1.6 version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1593,6 +1596,29 @@ packages: '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + '@clerk/clerk-react@5.47.0': + resolution: {integrity: sha512-of2Y6dg36eL7TwAP4DbGOMWW6DJpJSIuCn6g1jJqJkh4NGljHC7vz3H18OERRM5UQXmBG3twjC8CNAQxQrquRA==} + engines: {node: '>=18.17.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + + '@clerk/shared@3.25.0': + resolution: {integrity: sha512-2Vb6NQqBA+1g7kfGct/OlSFmzU54/s4BQp3qeHwDqW1FgaU4MuXbqfBClI6AatxOC8Ux8W16Rvf705ViwFSxlw==} + engines: {node: '>=18.17.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@clerk/types@4.86.0': + resolution: {integrity: sha512-YFaOYIAZWbpXehAmtgUB0YNf1v5b/hlwePvdqxlD5vdwrNsap28RpupWZat0hp1+PTtb9uAwSa5AFCOxkYLUJQ==} + engines: {node: '>=18.17.0'} + '@corex/deepmerge@4.0.43': resolution: {integrity: sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==} @@ -6203,6 +6229,9 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true @@ -6830,6 +6859,10 @@ packages: js-cookie@2.2.1: resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-message@1.0.7: resolution: {integrity: sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==} engines: {node: '>=0.6.0'} @@ -9169,6 +9202,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + swr@2.3.4: + resolution: {integrity: sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -9649,6 +9687,11 @@ packages: peerDependencies: react: '>=16.8' + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + utf8-byte-length@1.0.5: resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} @@ -11052,6 +11095,30 @@ snapshots: '@chevrotain/utils@11.0.3': {} + '@clerk/clerk-react@5.47.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@clerk/shared': 3.25.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/types': 4.86.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + + '@clerk/shared@3.25.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@clerk/types': 4.86.0 + dequal: 2.0.3 + glob-to-regexp: 0.4.1 + js-cookie: 3.0.5 + std-env: 3.9.0 + swr: 2.3.4(react@18.3.1) + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@clerk/types@4.86.0': + dependencies: + csstype: 3.1.3 + '@corex/deepmerge@4.0.43': {} '@csstools/color-helpers@5.0.2': {} @@ -16048,6 +16115,8 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-to-regexp@0.4.1: {} + glob@10.4.5: dependencies: foreground-child: 3.3.1 @@ -16731,6 +16800,8 @@ snapshots: js-cookie@2.2.1: {} + js-cookie@3.0.5: {} + js-message@1.0.7: {} js-queue@2.0.2: @@ -19581,6 +19652,12 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + swr@2.3.4(react@18.3.1): + dependencies: + dequal: 2.0.3 + react: 18.3.1 + use-sync-external-store: 1.5.0(react@18.3.1) + symbol-tree@3.2.4: {} tabbable@5.3.3: {} @@ -20104,6 +20181,10 @@ snapshots: howler: 2.2.4 react: 18.3.1 + use-sync-external-store@1.5.0(react@18.3.1): + dependencies: + react: 18.3.1 + utf8-byte-length@1.0.5: {} util-deprecate@1.0.2: {} diff --git a/webview-ui/package.json b/webview-ui/package.json index 4035e20d64..0182fa7513 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -15,6 +15,7 @@ "clean": "rimraf ../src/webview-ui ../apps/vscode-nightly/build/webview-ui tsconfig.tsbuildinfo .turbo" }, "dependencies": { + "@clerk/clerk-react": "^5.47.0", "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-checkbox": "^1.1.5", "@radix-ui/react-collapsible": "^1.1.3", diff --git a/webview-ui/src/components/cloud/ClerkProvider.tsx b/webview-ui/src/components/cloud/ClerkProvider.tsx new file mode 100644 index 0000000000..e52aef8780 --- /dev/null +++ b/webview-ui/src/components/cloud/ClerkProvider.tsx @@ -0,0 +1,40 @@ +import React from "react" + +interface ClerkProviderProps { + children: React.ReactNode + userInfo?: { + id?: string + organizationId?: string + } | null +} + +export const ClerkProvider: React.FC = ({ children }) => { + // Since we're in a VSCode extension webview, we don't have direct access to Clerk auth + // The authentication is handled by the backend WebAuthService + // We'll render the OrganizationSwitcher as a UI component only + + // For now, we'll just pass through the children + // The actual Clerk integration would require backend support + return <>{children} +} + +export const AccountSwitcher: React.FC<{ + organizationId?: string + organizationName?: string +}> = ({ organizationId, organizationName }) => { + // Since we can't directly use Clerk's OrganizationSwitcher without proper auth setup, + // we'll create a placeholder that shows the current organization + // In a full implementation, this would integrate with the backend auth service + + if (!organizationId || !organizationName) { + return null + } + + return ( +
+ Organization: + {organizationName} + (Switch via Roo Code Cloud) +
+ ) +} diff --git a/webview-ui/src/components/cloud/CloudView.tsx b/webview-ui/src/components/cloud/CloudView.tsx index 78dfba7494..d88a401763 100644 --- a/webview-ui/src/components/cloud/CloudView.tsx +++ b/webview-ui/src/components/cloud/CloudView.tsx @@ -13,6 +13,7 @@ import { TriangleAlert } from "lucide-react" import { cn } from "@/lib/utils" import { Tab, TabContent, TabHeader } from "../common/Tab" import { Button } from "@/components/ui/button" +import { AccountSwitcher } from "./ClerkProvider" // Define the production URL constant locally to avoid importing from cloud package in tests const PRODUCTION_ROO_CODE_API_URL = "https://app.roocode.com" @@ -186,15 +187,21 @@ export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: Cl

{userInfo?.email}

)} {userInfo?.organizationName && ( -
- {userInfo.organizationImageUrl && ( - {userInfo.organizationName} - )} - {userInfo.organizationName} +
+
+ {userInfo.organizationImageUrl && ( + {userInfo.organizationName} + )} + {userInfo.organizationName} +
+
)}
diff --git a/webview-ui/src/components/cloud/__tests__/CloudView.spec.tsx b/webview-ui/src/components/cloud/__tests__/CloudView.spec.tsx index 439dbb9083..be7dccb7af 100644 --- a/webview-ui/src/components/cloud/__tests__/CloudView.spec.tsx +++ b/webview-ui/src/components/cloud/__tests__/CloudView.spec.tsx @@ -2,6 +2,14 @@ import { render, screen } from "@/utils/test-utils" import { CloudView } from "../CloudView" +// Mock the AccountSwitcher component +vi.mock("../ClerkProvider", () => ({ + AccountSwitcher: ({ organizationId, organizationName }: any) => { + if (!organizationId || !organizationName) return null + return
Organization: {organizationName} (Switch via Roo Code Cloud)
+ }, +})) + // Mock the translation context vi.mock("@src/i18n/TranslationContext", () => ({ useAppTranslation: () => ({ @@ -360,4 +368,47 @@ describe("CloudView", () => { expect(taskSyncToggle).toHaveAttribute("aria-checked", "true") expect(taskSyncToggle).toHaveAttribute("tabindex", "-1") }) + + it("should display AccountSwitcher when user has organization", () => { + const mockUserInfo = { + name: "Test User", + email: "test@example.com", + organizationId: "org-123", + organizationName: "Test Organization", + } + + render( + {}} + />, + ) + + // Check that the AccountSwitcher is displayed + const accountSwitcher = screen.getByTestId("account-switcher") + expect(accountSwitcher).toBeInTheDocument() + expect(accountSwitcher).toHaveTextContent("Organization: Test Organization (Switch via Roo Code Cloud)") + }) + + it("should not display AccountSwitcher when user has no organization", () => { + const mockUserInfo = { + name: "Test User", + email: "test@example.com", + // No organizationId or organizationName + } + + render( + {}} + />, + ) + + // Check that the AccountSwitcher is NOT displayed + expect(screen.queryByTestId("account-switcher")).not.toBeInTheDocument() + }) })