Skip to content

Commit 2ccdf60

Browse files
committed
feat: add CloudAccountSwitcher component for switching between cloud accounts
- Created CloudAccountSwitcher component that displays current account icon - Added tooltip with 'Switch Roo Cloud Account' text (localized) - Component only visible when user has 2+ accounts (multiple orgs or org + personal) - Integrated into ChatTextArea next to IndexingStatusBadge - Added rooCloudAccountSwitch message handler for account switching - Exposed organization memberships through CloudService and ClineProvider - Added translation keys for all supported locales - Account switching redirects to /switch landing page after logout - Fixed all linting issues
1 parent 6c2aa63 commit 2ccdf60

File tree

26 files changed

+187
-21
lines changed

26 files changed

+187
-21
lines changed

packages/cloud/src/CloudService.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
AuthService,
99
SettingsService,
1010
CloudUserInfo,
11+
CloudOrganizationMembership,
1112
OrganizationAllowList,
1213
OrganizationSettings,
1314
ShareVisibility,
@@ -200,6 +201,14 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
200201
return this.authService!.getUserInfo()
201202
}
202203

204+
public async getOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
205+
this.ensureInitialized()
206+
if (this.authService && "getOrganizationMemberships" in this.authService) {
207+
return (this.authService as WebAuthService).getOrganizationMemberships()
208+
}
209+
return []
210+
}
211+
203212
public getOrganizationId(): string | null {
204213
this.ensureInitialized()
205214
const userInfo = this.authService!.getUserInfo()

packages/cloud/src/WebAuthService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
581581
// We have organization context info
582582
if (storedOrgId !== null) {
583583
// User is in organization context - fetch user's memberships and filter
584-
const orgMemberships = await this.clerkGetOrganizationMemberships()
584+
const orgMemberships = await this.getOrganizationMemberships()
585585
const userMembership = this.findOrganizationMembership(orgMemberships, storedOrgId)
586586

587587
if (userMembership) {
@@ -602,7 +602,7 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
602602
}
603603
} else {
604604
// Old credentials without organization context - fetch organization info to determine context
605-
const orgMemberships = await this.clerkGetOrganizationMemberships()
605+
const orgMemberships = await this.getOrganizationMemberships()
606606
const primaryOrgMembership = this.findPrimaryOrganizationMembership(orgMemberships)
607607

608608
if (primaryOrgMembership) {
@@ -652,7 +652,7 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
652652
userInfo.organizationImageUrl = membership.organization.image_url
653653
}
654654

655-
private async clerkGetOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
655+
public async getOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
656656
const response = await fetch(`${getClerkBaseUrl()}/v1/me/organization_memberships`, {
657657
headers: {
658658
Authorization: `Bearer ${this.credentials!.clientToken}`,

src/core/webview/ClineProvider.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
type TerminalActionPromptType,
3131
type HistoryItem,
3232
type CloudUserInfo,
33+
type CloudOrganizationMembership,
3334
type CreateTaskOptions,
3435
type TokenUsage,
3536
RooCodeEventName,
@@ -1821,6 +1822,18 @@ export class ClineProvider
18211822
const mergedDeniedCommands = this.mergeDeniedCommands(deniedCommands)
18221823
const cwd = this.cwd
18231824

1825+
// Get organization memberships for the account switcher
1826+
let cloudOrganizationMemberships: CloudOrganizationMembership[] = []
1827+
try {
1828+
if (CloudService.hasInstance() && CloudService.instance.isAuthenticated()) {
1829+
cloudOrganizationMemberships = await CloudService.instance.getOrganizationMemberships()
1830+
}
1831+
} catch (error) {
1832+
console.error(
1833+
`[getStateToPostToWebview] failed to get organization memberships: ${error instanceof Error ? error.message : String(error)}`,
1834+
)
1835+
}
1836+
18241837
// Check if there's a system prompt override for the current mode
18251838
const currentMode = mode ?? defaultModeSlug
18261839
const hasSystemPromptOverride = await this.hasFileBasedSystemPromptOverride(currentMode)
@@ -1915,6 +1928,7 @@ export class ClineProvider
19151928
hasSystemPromptOverride,
19161929
historyPreviewCollapsed: historyPreviewCollapsed ?? false,
19171930
cloudUserInfo,
1931+
cloudOrganizationMemberships,
19181932
cloudIsAuthenticated: cloudIsAuthenticated ?? false,
19191933
sharingEnabled: sharingEnabled ?? false,
19201934
organizationAllowList,

src/core/webview/webviewMessageHandler.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2337,6 +2337,18 @@ export const webviewMessageHandler = async (
23372337

23382338
break
23392339
}
2340+
case "rooCloudAccountSwitch": {
2341+
try {
2342+
// Account switching flow: logout and redirect to switch page
2343+
await CloudService.instance.logout()
2344+
// Login with switch landing page to allow account selection
2345+
await CloudService.instance.login("switch")
2346+
} catch (error) {
2347+
provider.log(`CloudService#switchAccount failed: ${error}`)
2348+
vscode.window.showErrorMessage("Account switch failed.")
2349+
}
2350+
break
2351+
}
23402352
case "rooCloudManualUrl": {
23412353
try {
23422354
if (!message.text) {

src/shared/ExtensionMessage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
MarketplaceItem,
1111
TodoItem,
1212
CloudUserInfo,
13+
CloudOrganizationMembership,
1314
OrganizationAllowList,
1415
ShareVisibility,
1516
QueuedMessage,
@@ -187,6 +188,7 @@ export interface ExtensionMessage {
187188
hasContent?: boolean // For checkRulesDirectoryResult
188189
items?: MarketplaceItem[]
189190
userInfo?: CloudUserInfo
191+
organizationMemberships?: CloudOrganizationMembership[]
190192
organizationAllowList?: OrganizationAllowList
191193
tab?: string
192194
marketplaceItems?: MarketplaceItem[]
@@ -325,6 +327,7 @@ export type ExtensionState = Pick<
325327
historyPreviewCollapsed?: boolean
326328

327329
cloudUserInfo: CloudUserInfo | null
330+
cloudOrganizationMemberships?: CloudOrganizationMembership[]
328331
cloudIsAuthenticated: boolean
329332
cloudApiUrl?: string
330333
sharingEnabled: boolean

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ export interface WebviewMessage {
183183
| "rooCloudSignIn"
184184
| "cloudLandingPageSignIn"
185185
| "rooCloudSignOut"
186+
| "rooCloudAccountSwitch"
186187
| "rooCloudManualUrl"
187188
| "condenseTaskContextRequest"
188189
| "requestIndexingStatus"

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { AutoApproveDropdown } from "./AutoApproveDropdown"
3030
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
3131
import ContextMenu from "./ContextMenu"
3232
import { IndexingStatusBadge } from "./IndexingStatusBadge"
33+
import { CloudAccountSwitcher } from "../cloud/CloudAccountSwitcher"
3334
import { usePromptHistory } from "./hooks/usePromptHistory"
3435

3536
interface ChatTextAreaProps {
@@ -87,6 +88,8 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
8788
taskHistory,
8889
clineMessages,
8990
commands,
91+
cloudUserInfo,
92+
cloudOrganizationMemberships,
9093
} = useExtensionState()
9194

9295
// Find the ID and display text for the currently selected API configuration.
@@ -1253,7 +1256,17 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
12531256
</button>
12541257
</StandardTooltip>
12551258
)}
1256-
{!isEditMode ? <IndexingStatusBadge /> : null}
1259+
{!isEditMode ? (
1260+
<>
1261+
<IndexingStatusBadge />
1262+
{cloudUserInfo && (
1263+
<CloudAccountSwitcher
1264+
userInfo={cloudUserInfo}
1265+
organizationMemberships={cloudOrganizationMemberships}
1266+
/>
1267+
)}
1268+
</>
1269+
) : null}
12571270
</div>
12581271
</div>
12591272
</div>
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import React, { useMemo } from "react"
2+
import { User } from "lucide-react"
3+
4+
import { cn } from "@src/lib/utils"
5+
import { vscode } from "@src/utils/vscode"
6+
import { useAppTranslation } from "@/i18n/TranslationContext"
7+
8+
import type { CloudUserInfo, CloudOrganizationMembership } from "@roo-code/types"
9+
10+
import { StandardTooltip, Button } from "@src/components/ui"
11+
12+
interface CloudAccountSwitcherProps {
13+
className?: string
14+
userInfo: CloudUserInfo | null
15+
organizationMemberships?: CloudOrganizationMembership[]
16+
}
17+
18+
export const CloudAccountSwitcher: React.FC<CloudAccountSwitcherProps> = ({
19+
className,
20+
userInfo,
21+
organizationMemberships = [],
22+
}) => {
23+
const { t } = useAppTranslation()
24+
25+
// Calculate if user has multiple accounts
26+
// User has multiple accounts if:
27+
// 1. They have 2+ organization memberships, OR
28+
// 2. They have 1+ organization membership and can also use personal account
29+
const hasMultipleAccounts = useMemo(() => {
30+
if (!userInfo) return false
31+
32+
// If user has multiple org memberships
33+
if (organizationMemberships.length >= 2) return true
34+
35+
// If user has at least one org membership, they also have personal account access
36+
if (organizationMemberships.length >= 1) return true
37+
38+
return false
39+
}, [userInfo, organizationMemberships])
40+
41+
// Don't render if user doesn't have multiple accounts
42+
if (!hasMultipleAccounts) {
43+
return null
44+
}
45+
46+
const handleAccountSwitch = () => {
47+
vscode.postMessage({ type: "rooCloudAccountSwitch" })
48+
}
49+
50+
// Determine which icon to show
51+
const renderAccountIcon = () => {
52+
// If in organization context and has org image
53+
if (userInfo?.organizationId && userInfo?.organizationImageUrl) {
54+
return (
55+
<img
56+
src={userInfo.organizationImageUrl}
57+
alt={userInfo.organizationName || "Organization"}
58+
className="w-4 h-4 rounded-full object-cover"
59+
/>
60+
)
61+
}
62+
63+
// If user has profile picture
64+
if (userInfo?.picture) {
65+
return (
66+
<img
67+
src={userInfo.picture}
68+
alt={userInfo.name || userInfo.email || "User"}
69+
className="w-4 h-4 rounded-full object-cover"
70+
/>
71+
)
72+
}
73+
74+
// Default user icon
75+
return <User className="w-4 h-4" />
76+
}
77+
78+
return (
79+
<StandardTooltip content={t("cloud:switchAccount")}>
80+
<Button
81+
variant="ghost"
82+
size="sm"
83+
onClick={handleAccountSwitch}
84+
aria-label={t("cloud:switchAccount")}
85+
className={cn(
86+
"relative h-5 w-5 p-0",
87+
"text-vscode-foreground opacity-85",
88+
"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)]",
89+
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
90+
className,
91+
)}>
92+
{renderAccountIcon()}
93+
</Button>
94+
</StandardTooltip>
95+
)
96+
}

webview-ui/src/i18n/locales/ca/cloud.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/de/cloud.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)