Skip to content

Commit 80e1a80

Browse files
committed
Cloud account switcher
1 parent 6c2aa63 commit 80e1a80

30 files changed

+455
-13
lines changed

packages/cloud/src/CloudService.ts

Lines changed: 30 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,
@@ -242,6 +243,35 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
242243
return this.authService!.handleCallback(code, state, organizationId)
243244
}
244245

246+
public async switchOrganization(organizationId: string | null): Promise<void> {
247+
this.ensureInitialized()
248+
249+
// Check if we have a WebAuthService (not StaticTokenAuthService)
250+
if (!(this.authService instanceof WebAuthService)) {
251+
throw new Error("Organization switching is only available with web authentication")
252+
}
253+
254+
const webAuthService = this.authService as WebAuthService
255+
256+
// Perform the organization switch
257+
await webAuthService.switchOrganization(organizationId)
258+
259+
// Broadcast the change
260+
await this.authService.broadcast()
261+
}
262+
263+
public async getOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
264+
this.ensureInitialized()
265+
266+
// Check if we have a WebAuthService (not StaticTokenAuthService)
267+
if (!(this.authService instanceof WebAuthService)) {
268+
return []
269+
}
270+
271+
const webAuthService = this.authService as WebAuthService
272+
return await webAuthService.getOrganizationMemberships()
273+
}
274+
245275
// SettingsService
246276

247277
public getAllowList(): OrganizationAllowList {

packages/cloud/src/WebAuthService.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,60 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
469469
return this.credentials?.organizationId || null
470470
}
471471

472+
/**
473+
* Switch to a different organization context
474+
* @param organizationId The organization ID to switch to, or null for personal account
475+
*/
476+
public async switchOrganization(organizationId: string | null): Promise<void> {
477+
if (!this.credentials) {
478+
throw new Error("Cannot switch organization: not authenticated")
479+
}
480+
481+
// Update the stored credentials with the new organization ID
482+
const updatedCredentials: AuthCredentials = {
483+
...this.credentials,
484+
organizationId: organizationId,
485+
}
486+
487+
// Store the updated credentials
488+
await this.storeCredentials(updatedCredentials)
489+
490+
// Update the local credentials
491+
this.credentials = updatedCredentials
492+
493+
// Clear the current session token to force a refresh with new org context
494+
this.sessionToken = null
495+
496+
// Trigger a session refresh to get a new token with the correct org context
497+
try {
498+
await this.refreshSession()
499+
// Fetch updated user info after organization switch to reflect new context
500+
await this.fetchUserInfo()
501+
} catch (error) {
502+
this.log(`[auth] Failed to refresh session after organization switch: ${error}`)
503+
// Even if refresh fails, the credentials are updated for next attempt
504+
}
505+
506+
this.log(`[auth] Switched organization context to: ${organizationId || "personal account"}`)
507+
}
508+
509+
/**
510+
* Get all organization memberships for the current user
511+
* @returns Array of organization memberships
512+
*/
513+
public async getOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
514+
if (!this.credentials) {
515+
return []
516+
}
517+
518+
try {
519+
return await this.clerkGetOrganizationMemberships()
520+
} catch (error) {
521+
this.log(`[auth] Failed to get organization memberships: ${error}`)
522+
return []
523+
}
524+
}
525+
472526
private async clerkSignIn(ticket: string): Promise<AuthCredentials> {
473527
const formData = new URLSearchParams()
474528
formData.append("strategy", "ticket")

src/core/webview/ClineProvider.ts

Lines changed: 12 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,
@@ -1815,6 +1816,16 @@ export class ClineProvider
18151816
featureRoomoteControlEnabled,
18161817
} = await this.getState()
18171818

1819+
let cloudOrganizations: CloudOrganizationMembership[] = []
1820+
1821+
try {
1822+
cloudOrganizations = await CloudService.instance.getOrganizationMemberships()
1823+
} catch (error) {
1824+
console.error(
1825+
`[getStateToPostToWebview] failed to get cloud organizations: ${error instanceof Error ? error.message : String(error)}`,
1826+
)
1827+
}
1828+
18181829
const telemetryKey = process.env.POSTHOG_API_KEY
18191830
const machineId = vscode.env.machineId
18201831
const mergedAllowedCommands = this.mergeAllowedCommands(allowedCommands)
@@ -1916,6 +1927,7 @@ export class ClineProvider
19161927
historyPreviewCollapsed: historyPreviewCollapsed ?? false,
19171928
cloudUserInfo,
19181929
cloudIsAuthenticated: cloudIsAuthenticated ?? false,
1930+
cloudOrganizations,
19191931
sharingEnabled: sharingEnabled ?? false,
19201932
organizationAllowList,
19211933
organizationSettingsVersion,

src/core/webview/webviewMessageHandler.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2379,6 +2379,23 @@ export const webviewMessageHandler = async (
23792379

23802380
break
23812381
}
2382+
case "switchOrganization": {
2383+
try {
2384+
const organizationId = message.organizationId ?? null
2385+
2386+
// Switch to the new organization context
2387+
await CloudService.instance.switchOrganization(organizationId)
2388+
2389+
// Refresh the state to update UI
2390+
await provider.postStateToWebview()
2391+
} catch (error) {
2392+
provider.log(`Organization switch failed: ${error}`)
2393+
vscode.window.showErrorMessage(
2394+
`Failed to switch organization: ${error instanceof Error ? error.message : String(error)}`,
2395+
)
2396+
}
2397+
break
2398+
}
23822399

23832400
case "saveCodeIndexSettingsAtomic": {
23842401
if (!message.codeIndexSettings) {

src/shared/ExtensionMessage.ts

Lines changed: 2 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,
@@ -327,6 +328,7 @@ export type ExtensionState = Pick<
327328
cloudUserInfo: CloudUserInfo | null
328329
cloudIsAuthenticated: boolean
329330
cloudApiUrl?: string
331+
cloudOrganizations?: CloudOrganizationMembership[]
330332
sharingEnabled: boolean
331333
organizationAllowList: OrganizationAllowList
332334
organizationSettingsVersion?: number

src/shared/WebviewMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ export interface WebviewMessage {
184184
| "cloudLandingPageSignIn"
185185
| "rooCloudSignOut"
186186
| "rooCloudManualUrl"
187+
| "switchOrganization"
187188
| "condenseTaskContextRequest"
188189
| "requestIndexingStatus"
189190
| "startIndexing"
@@ -274,6 +275,7 @@ export interface WebviewMessage {
274275
checkOnly?: boolean // For deleteCustomMode check
275276
upsellId?: string // For dismissUpsell
276277
list?: string[] // For dismissedUpsells response
278+
organizationId?: string | null // For organization switching
277279
codeIndexSettings?: {
278280
// Global state settings
279281
codebaseIndexEnabled: boolean

webview-ui/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const App = () => {
7676
cloudUserInfo,
7777
cloudIsAuthenticated,
7878
cloudApiUrl,
79+
cloudOrganizations,
7980
renderContext,
8081
mdmCompliant,
8182
} = useExtensionState()
@@ -267,6 +268,7 @@ const App = () => {
267268
userInfo={cloudUserInfo}
268269
isAuthenticated={cloudIsAuthenticated}
269270
cloudApiUrl={cloudApiUrl}
271+
organizations={cloudOrganizations}
270272
onDone={() => switchTab("chat")}
271273
/>
272274
)}

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
3131
import ContextMenu from "./ContextMenu"
3232
import { IndexingStatusBadge } from "./IndexingStatusBadge"
3333
import { usePromptHistory } from "./hooks/usePromptHistory"
34+
import { CloudAccountSwitcher } from "../cloud/CloudAccountSwitcher"
3435

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

9295
// Find the ID and display text for the currently selected API configuration.
@@ -1232,7 +1235,13 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
12321235
/>
12331236
<AutoApproveDropdown triggerClassName="min-w-[28px] text-ellipsis overflow-hidden flex-shrink" />
12341237
</div>
1235-
<div className="flex flex-shrink-0 items-center gap-0.5 pr-2">
1238+
<div
1239+
className={cn(
1240+
"flex flex-shrink-0 items-center gap-0.5",
1241+
!isEditMode && cloudOrganizations && cloudOrganizations.length > 0 && cloudUserInfo
1242+
? ""
1243+
: "pr-2",
1244+
)}>
12361245
{isTtsPlaying && (
12371246
<StandardTooltip content={t("chat:stopTts")}>
12381247
<button
@@ -1254,6 +1263,9 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
12541263
</StandardTooltip>
12551264
)}
12561265
{!isEditMode ? <IndexingStatusBadge /> : null}
1266+
{!isEditMode && cloudOrganizations && cloudOrganizations.length > 0 && cloudUserInfo && (
1267+
<CloudAccountSwitcher />
1268+
)}
12571269
</div>
12581270
</div>
12591271
</div>

0 commit comments

Comments
 (0)