Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/cloud/src/CloudService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
AuthService,
SettingsService,
CloudUserInfo,
CloudOrganizationMembership,
OrganizationAllowList,
OrganizationSettings,
ShareVisibility,
Expand Down Expand Up @@ -200,6 +201,14 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
return this.authService!.getUserInfo()
}

public async getOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
this.ensureInitialized()
if (this.authService && "getOrganizationMemberships" in this.authService) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type safety: Consider using a more explicit interface check instead of duck typing with the 'in' operator. You could define an interface that includes getOrganizationMemberships method.

return (this.authService as WebAuthService).getOrganizationMemberships()
}
return []
}

public getOrganizationId(): string | null {
this.ensureInitialized()
const userInfo = this.authService!.getUserInfo()
Expand Down
6 changes: 3 additions & 3 deletions packages/cloud/src/WebAuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
// We have organization context info
if (storedOrgId !== null) {
// User is in organization context - fetch user's memberships and filter
const orgMemberships = await this.clerkGetOrganizationMemberships()
const orgMemberships = await this.getOrganizationMemberships()
const userMembership = this.findOrganizationMembership(orgMemberships, storedOrgId)

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

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

private async clerkGetOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
public async getOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
const response = await fetch(`${getClerkBaseUrl()}/v1/me/organization_memberships`, {
headers: {
Authorization: `Bearer ${this.credentials!.clientToken}`,
Expand Down
14 changes: 14 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
type TerminalActionPromptType,
type HistoryItem,
type CloudUserInfo,
type CloudOrganizationMembership,
type CreateTaskOptions,
type TokenUsage,
RooCodeEventName,
Expand Down Expand Up @@ -1821,6 +1822,18 @@ export class ClineProvider
const mergedDeniedCommands = this.mergeDeniedCommands(deniedCommands)
const cwd = this.cwd

// Get organization memberships for the account switcher
let cloudOrganizationMemberships: CloudOrganizationMembership[] = []
try {
if (CloudService.hasInstance() && CloudService.instance.isAuthenticated()) {
cloudOrganizationMemberships = await CloudService.instance.getOrganizationMemberships()
}
} catch (error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error handling: Consider showing a degraded UI state if fetching organization memberships fails, rather than just logging the error. Users might wonder why the account switcher isn't appearing.

console.error(
`[getStateToPostToWebview] failed to get organization memberships: ${error instanceof Error ? error.message : String(error)}`,
)
}

// Check if there's a system prompt override for the current mode
const currentMode = mode ?? defaultModeSlug
const hasSystemPromptOverride = await this.hasFileBasedSystemPromptOverride(currentMode)
Expand Down Expand Up @@ -1915,6 +1928,7 @@ export class ClineProvider
hasSystemPromptOverride,
historyPreviewCollapsed: historyPreviewCollapsed ?? false,
cloudUserInfo,
cloudOrganizationMemberships,
cloudIsAuthenticated: cloudIsAuthenticated ?? false,
sharingEnabled: sharingEnabled ?? false,
organizationAllowList,
Expand Down
12 changes: 12 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2337,6 +2337,18 @@ export const webviewMessageHandler = async (

break
}
case "rooCloudAccountSwitch": {
try {
// Account switching flow: logout and redirect to switch page
await CloudService.instance.logout()
// Login with switch landing page to allow account selection
await CloudService.instance.login("switch")
} catch (error) {
provider.log(`CloudService#switchAccount failed: ${error}`)
vscode.window.showErrorMessage("Account switch failed.")
}
break
}
case "rooCloudManualUrl": {
try {
if (!message.text) {
Expand Down
3 changes: 3 additions & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
MarketplaceItem,
TodoItem,
CloudUserInfo,
CloudOrganizationMembership,
OrganizationAllowList,
ShareVisibility,
QueuedMessage,
Expand Down Expand Up @@ -187,6 +188,7 @@ export interface ExtensionMessage {
hasContent?: boolean // For checkRulesDirectoryResult
items?: MarketplaceItem[]
userInfo?: CloudUserInfo
organizationMemberships?: CloudOrganizationMembership[]
organizationAllowList?: OrganizationAllowList
tab?: string
marketplaceItems?: MarketplaceItem[]
Expand Down Expand Up @@ -325,6 +327,7 @@ export type ExtensionState = Pick<
historyPreviewCollapsed?: boolean

cloudUserInfo: CloudUserInfo | null
cloudOrganizationMemberships?: CloudOrganizationMembership[]
cloudIsAuthenticated: boolean
cloudApiUrl?: string
sharingEnabled: boolean
Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export interface WebviewMessage {
| "rooCloudSignIn"
| "cloudLandingPageSignIn"
| "rooCloudSignOut"
| "rooCloudAccountSwitch"
| "rooCloudManualUrl"
| "condenseTaskContextRequest"
| "requestIndexingStatus"
Expand Down
15 changes: 14 additions & 1 deletion webview-ui/src/components/chat/ChatTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { AutoApproveDropdown } from "./AutoApproveDropdown"
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
import ContextMenu from "./ContextMenu"
import { IndexingStatusBadge } from "./IndexingStatusBadge"
import { CloudAccountSwitcher } from "../cloud/CloudAccountSwitcher"
import { usePromptHistory } from "./hooks/usePromptHistory"

interface ChatTextAreaProps {
Expand Down Expand Up @@ -87,6 +88,8 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
taskHistory,
clineMessages,
commands,
cloudUserInfo,
cloudOrganizationMemberships,
} = useExtensionState()

// Find the ID and display text for the currently selected API configuration.
Expand Down Expand Up @@ -1253,7 +1256,17 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
</button>
</StandardTooltip>
)}
{!isEditMode ? <IndexingStatusBadge /> : null}
{!isEditMode ? (
<>
<IndexingStatusBadge />
{cloudUserInfo && (
<CloudAccountSwitcher
userInfo={cloudUserInfo}
organizationMemberships={cloudOrganizationMemberships}
/>
)}
</>
) : null}
</div>
</div>
</div>
Expand Down
96 changes: 96 additions & 0 deletions webview-ui/src/components/cloud/CloudAccountSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useMemo } from "react"
import { User } from "lucide-react"

import { cn } from "@src/lib/utils"
import { vscode } from "@src/utils/vscode"
import { useAppTranslation } from "@/i18n/TranslationContext"

import type { CloudUserInfo, CloudOrganizationMembership } from "@roo-code/types"

import { StandardTooltip, Button } from "@src/components/ui"

interface CloudAccountSwitcherProps {
className?: string
userInfo: CloudUserInfo | null
organizationMemberships?: CloudOrganizationMembership[]
}

export const CloudAccountSwitcher: React.FC<CloudAccountSwitcherProps> = ({
className,
userInfo,
organizationMemberships = [],
}) => {
const { t } = useAppTranslation()

// Calculate if user has multiple accounts
// User has multiple accounts if:
// 1. They have 2+ organization memberships, OR
// 2. They have 1+ organization membership and can also use personal account
const hasMultipleAccounts = useMemo(() => {
if (!userInfo) return false

// If user has multiple org memberships
if (organizationMemberships.length >= 2) return true

// If user has at least one org membership, they also have personal account access
if (organizationMemberships.length >= 1) return true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Visibility logic concern: This component will show for users with just one organization membership, even if they can't actually switch to a personal account. Consider adding a check to verify if users can actually switch between accounts (e.g., check if personal account access is available).

Suggested change
if (organizationMemberships.length >= 1) return true
// If user has at least one org membership, they also have personal account access
if (organizationMemberships.length >= 1) {
// TODO: Verify if user can actually switch to personal account
// This assumes all users with org memberships also have personal account access
return true
}


return false
}, [userInfo, organizationMemberships])

// Don't render if user doesn't have multiple accounts
if (!hasMultipleAccounts) {
return null
}

const handleAccountSwitch = () => {
vscode.postMessage({ type: "rooCloudAccountSwitch" })
}

// Determine which icon to show
const renderAccountIcon = () => {
// If in organization context and has org image
if (userInfo?.organizationId && userInfo?.organizationImageUrl) {
return (
<img
src={userInfo.organizationImageUrl}
alt={userInfo.organizationName || "Organization"}
className="w-4 h-4 rounded-full object-cover"
/>
)
}

// If user has profile picture
if (userInfo?.picture) {
return (
<img
src={userInfo.picture}
alt={userInfo.name || userInfo.email || "User"}
className="w-4 h-4 rounded-full object-cover"
/>
)
}

// Default user icon
return <User className="w-4 h-4" />
}

return (
<StandardTooltip content={t("cloud:switchAccount")}>
<Button
variant="ghost"
size="sm"
onClick={handleAccountSwitch}
aria-label={t("cloud:switchAccount")}
className={cn(
"relative h-5 w-5 p-0",
"text-vscode-foreground opacity-85",
"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)]",
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
className,
)}>
{renderAccountIcon()}
</Button>
</StandardTooltip>
)
}
3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/ca/cloud.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/de/cloud.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/en/cloud.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"testApiAuthentication": "Test API Authentication",
"signIn": "Connect to Roo Code Cloud",
"connect": "Get started",
"switchAccount": "Switch Roo Cloud Account",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typographical suggestion: The new label "Switch Roo Cloud Account" appears to be inconsistent with other entries that reference "Roo Code Cloud" (e.g., "Connect to Roo Code Cloud"). If this is unintentional, consider updating it to "Switch Roo Code Cloud Account" for consistency.

Suggested change
"switchAccount": "Switch Roo Cloud Account",
"switchAccount": "Switch Roo Code Cloud Account",

This comment was generated because it violated the following rules: irule_C0ez7Rji6ANcGkkX and irule_VrRKWqywZ2YV2SOE.

"cloudBenefitsTitle": "Try Roo Code Cloud",
"cloudBenefitWalkaway": "Follow and control tasks from anywhere (including your phone)",
"cloudBenefitSharing": "Share tasks with others",
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/es/cloud.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/fr/cloud.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/hi/cloud.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/id/cloud.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/it/cloud.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/ja/cloud.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/ko/cloud.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/nl/cloud.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/pl/cloud.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/pt-BR/cloud.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/ru/cloud.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/tr/cloud.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/vi/cloud.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading