Skip to content
Merged
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
42 changes: 20 additions & 22 deletions packages/cloud/src/AuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import * as vscode from "vscode"

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

import { CloudServiceCallbacks } from "./types"
import { getClerkBaseUrl, getRooCodeApiUrl } from "./Config"
import { RefreshTimer } from "./RefreshTimer"

export interface AuthServiceEvents {
"active-session": [data: { previousState: AuthState }]
"logged-out": [data: { previousState: AuthState }]
"user-info": [data: { userInfo: CloudUserInfo }]
}

const CLIENT_TOKEN_KEY = "clerk-client-token"
Expand All @@ -23,19 +23,18 @@ type AuthState = "initializing" | "logged-out" | "active-session" | "inactive-se

export class AuthService extends EventEmitter<AuthServiceEvents> {
private context: vscode.ExtensionContext
private userChanged: CloudServiceCallbacks["userChanged"]
private timer: RefreshTimer
private state: AuthState = "initializing"

private clientToken: string | null = null
private sessionToken: string | null = null
private sessionId: string | null = null
private userInfo: CloudUserInfo | null = null

constructor(context: vscode.ExtensionContext, userChanged: CloudServiceCallbacks["userChanged"]) {
constructor(context: vscode.ExtensionContext) {
super()

this.context = context
this.userChanged = userChanged

this.timer = new RefreshTimer({
callback: async () => {
Expand Down Expand Up @@ -140,9 +139,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
this.emit("active-session", { previousState })
this.timer.start()

if (this.userChanged) {
this.getUserInfo().then(this.userChanged)
}
this.fetchUserInfo()

vscode.window.showInformationMessage("Successfully authenticated with Roo Code Cloud")
console.log("[auth] Successfully authenticated with Roo Code Cloud")
Expand Down Expand Up @@ -174,6 +171,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
this.clientToken = null
this.sessionToken = null
this.sessionId = null
this.userInfo = null
const previousState = this.state
this.state = "logged-out"
this.emit("logged-out", { previousState })
Expand All @@ -182,9 +180,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
await this.clerkLogout(oldClientToken, oldSessionId)
}

if (this.userChanged) {
this.getUserInfo().then(this.userChanged)
}
this.fetchUserInfo()

vscode.window.showInformationMessage("Logged out from Roo Code Cloud")
console.log("[auth] Logged out from Roo Code Cloud")
Expand Down Expand Up @@ -224,7 +220,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
*
* This method refreshes the session token using the client token.
*/
private async refreshSession() {
private async refreshSession(): Promise<void> {
if (!this.sessionId || !this.clientToken) {
console.log("[auth] Cannot refresh session: missing session ID or token")
this.state = "inactive-session"
Expand All @@ -237,24 +233,26 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {

if (previousState !== "active-session") {
this.emit("active-session", { previousState })
this.fetchUserInfo()
}
}

if (this.userChanged) {
this.getUserInfo().then(this.userChanged)
}
private async fetchUserInfo(): Promise<void> {
if (!this.clientToken) {
return
}

this.userInfo = await this.clerkMe()
this.emit("user-info", { userInfo: this.userInfo })
}

/**
* Extract user information from the ID token
*
* @returns User information from ID token claims or null if no ID token available
*/
public async getUserInfo(): Promise<CloudUserInfo | undefined> {
if (!this.clientToken) {
return undefined
}

return await this.clerkMe()
public getUserInfo(): CloudUserInfo | null {
return this.userInfo
}

private async clerkSignIn(
Expand Down Expand Up @@ -383,12 +381,12 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
return this._instance
}

static async createInstance(context: vscode.ExtensionContext, userChanged: CloudServiceCallbacks["userChanged"]) {
static async createInstance(context: vscode.ExtensionContext) {
if (this._instance) {
throw new Error("AuthService instance already created")
}

this._instance = new AuthService(context, userChanged)
this._instance = new AuthService(context)
await this._instance.initialize()
return this._instance
}
Expand Down
21 changes: 16 additions & 5 deletions packages/cloud/src/CloudService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class CloudService {

private context: vscode.ExtensionContext
private callbacks: CloudServiceCallbacks
private authListener: () => void
private authService: AuthService | null = null
private settingsService: SettingsService | null = null
private telemetryClient: TelemetryClient | null = null
Expand All @@ -21,6 +22,9 @@ export class CloudService {
private constructor(context: vscode.ExtensionContext, callbacks: CloudServiceCallbacks) {
this.context = context
this.callbacks = callbacks
this.authListener = () => {
this.callbacks.stateChanged?.()
}
}

public async initialize(): Promise<void> {
Expand All @@ -29,12 +33,14 @@ export class CloudService {
}

try {
this.authService = await AuthService.createInstance(this.context, (userInfo) => {
this.callbacks.userChanged?.(userInfo)
})
this.authService = await AuthService.createInstance(this.context)

this.authService.on("active-session", this.authListener)
this.authService.on("logged-out", this.authListener)
this.authService.on("user-info", this.authListener)

this.settingsService = await SettingsService.createInstance(this.context, () =>
this.callbacks.settingsChanged?.(),
this.callbacks.stateChanged?.(),
)

this.telemetryClient = new TelemetryClient(this.authService)
Expand Down Expand Up @@ -74,7 +80,7 @@ export class CloudService {
return this.authService!.hasActiveSession()
}

public async getUserInfo(): Promise<CloudUserInfo | undefined> {
public getUserInfo(): CloudUserInfo | null {
this.ensureInitialized()
return this.authService!.getUserInfo()
}
Expand Down Expand Up @@ -106,6 +112,11 @@ export class CloudService {
// Lifecycle

public dispose(): void {
if (this.authService) {
this.authService.off("active-session", this.authListener)
this.authService.off("logged-out", this.authListener)
this.authService.off("user-info", this.authListener)
}
if (this.settingsService) {
this.settingsService.dispose()
}
Expand Down
9 changes: 6 additions & 3 deletions packages/cloud/src/__tests__/CloudService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,14 @@ describe("CloudService", () => {

describe("createInstance", () => {
it("should create and initialize CloudService instance", async () => {
const callbacks = { userChanged: vi.fn(), settingsChanged: vi.fn() }
const callbacks = {
stateChanged: vi.fn(),
}

const cloudService = await CloudService.createInstance(mockContext, callbacks)

expect(cloudService).toBeInstanceOf(CloudService)
expect(AuthService.createInstance).toHaveBeenCalledWith(mockContext, expect.any(Function))
expect(AuthService.createInstance).toHaveBeenCalledWith(mockContext)
expect(SettingsService.createInstance).toHaveBeenCalledWith(mockContext, expect.any(Function))
})

Expand All @@ -150,7 +153,7 @@ describe("CloudService", () => {
let callbacks: CloudServiceCallbacks

beforeEach(async () => {
callbacks = { userChanged: vi.fn(), settingsChanged: vi.fn() }
callbacks = { stateChanged: vi.fn() }
cloudService = await CloudService.createInstance(mockContext, callbacks)
})

Expand Down
5 changes: 1 addition & 4 deletions packages/cloud/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { CloudUserInfo } from "@roo-code/types"

export interface CloudServiceCallbacks {
userChanged?: (userInfo: CloudUserInfo | undefined) => void
settingsChanged?: () => void
stateChanged?: () => void
}
14 changes: 14 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
type TerminalActionPromptType,
type HistoryItem,
ORGANIZATION_ALLOW_ALL,
CloudUserInfo,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { CloudService } from "@roo-code/cloud"
Expand Down Expand Up @@ -1296,6 +1297,7 @@ export class ClineProvider
maxReadFileLine,
terminalCompressProgressBar,
historyPreviewCollapsed,
cloudUserInfo,
organizationAllowList,
condensingApiConfigId,
customCondensingPrompt,
Expand Down Expand Up @@ -1391,6 +1393,7 @@ export class ClineProvider
terminalCompressProgressBar: terminalCompressProgressBar ?? true,
hasSystemPromptOverride,
historyPreviewCollapsed: historyPreviewCollapsed ?? false,
cloudUserInfo,
organizationAllowList,
condensingApiConfigId,
customCondensingPrompt,
Expand Down Expand Up @@ -1436,6 +1439,16 @@ export class ClineProvider
)
}

let cloudUserInfo: CloudUserInfo | null = null

try {
cloudUserInfo = CloudService.instance.getUserInfo()
} catch (error) {
console.error(
`[getState] failed to get cloud user info: ${error instanceof Error ? error.message : String(error)}`,
)
}

// Return the same structure as before
return {
apiConfiguration: providerSettings,
Expand Down Expand Up @@ -1504,6 +1517,7 @@ export class ClineProvider
showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? true,
maxReadFileLine: stateValues.maxReadFileLine ?? -1,
historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
cloudUserInfo,
organizationAllowList,
// Explicitly add condensing settings
condensingApiConfigId: stateValues.condensingApiConfigId,
Expand Down
1 change: 1 addition & 0 deletions src/core/webview/__tests__/ClineProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ describe("ClineProvider", () => {
showRooIgnoredFiles: true,
renderContext: "sidebar",
maxReadFileLine: 500,
cloudUserInfo: null,
organizationAllowList: ORGANIZATION_ALLOW_ALL,
autoCondenseContext: true,
autoCondenseContextPercent: 100,
Expand Down
10 changes: 0 additions & 10 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,16 +106,6 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
await Promise.all([
await updateGlobalState("listApiConfigMeta", listApiConfig),
await provider.postMessageToWebview({ type: "listApiConfig", listApiConfig }),
async () => {
try {
if (CloudService.instance.hasActiveSession()) {
const userInfo = await CloudService.instance.getUserInfo()
provider.postMessageToWebview({ type: "authenticatedUser", userInfo })
}
} catch (error) {
provider.log(`AuthService#getUserInfo failed: ${error}`)
}
},
])
})
.catch((error) =>
Expand Down
4 changes: 1 addition & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ export async function activate(context: vscode.ExtensionContext) {

// Initialize Roo Code Cloud service.
await CloudService.createInstance(context, {
userChanged: (userInfo) =>
ClineProvider.getVisibleInstance()?.postMessageToWebview({ type: "authenticatedUser", userInfo }),
settingsChanged: () => ClineProvider.getVisibleInstance()?.postStateToWebview(),
stateChanged: () => ClineProvider.getVisibleInstance()?.postStateToWebview(),
})

// Initialize i18n for internationalization support
Expand Down
1 change: 1 addition & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ export type ExtensionState = Pick<
settingsImportedAt?: number
historyPreviewCollapsed?: boolean

cloudUserInfo: CloudUserInfo | null
organizationAllowList: OrganizationAllowList

autoCondenseContext: boolean
Expand Down
19 changes: 10 additions & 9 deletions webview-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useCallback, useEffect, useRef, useState } from "react"
import { useEvent } from "react-use"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"

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

import TranslationProvider from "./i18n/TranslationContext"
Expand Down Expand Up @@ -30,12 +29,18 @@ const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]
}

const App = () => {
const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, telemetryKey, machineId } =
useExtensionState()
const {
didHydrateState,
showWelcome,
shouldShowAnnouncement,
telemetrySetting,
telemetryKey,
machineId,
cloudUserInfo,
} = useExtensionState()

const [showAnnouncement, setShowAnnouncement] = useState(false)
const [tab, setTab] = useState<Tab>("chat")
const [userInfo, setUserInfo] = useState<CloudUserInfo | null>(null)

const [humanRelayDialogState, setHumanRelayDialogState] = useState<{
isOpen: boolean
Expand Down Expand Up @@ -84,10 +89,6 @@ const App = () => {
if (message.type === "acceptInput") {
chatViewRef.current?.acceptInput()
}

if (message.type === "authenticatedUser") {
setUserInfo(message.userInfo || null)
}
},
[switchTab],
)
Expand Down Expand Up @@ -126,7 +127,7 @@ const App = () => {
{tab === "settings" && (
<SettingsView ref={settingsRef} onDone={() => setTab("chat")} targetSection={currentSection} />
)}
{tab === "account" && <AccountView userInfo={userInfo} onDone={() => switchTab("chat")} />}
{tab === "account" && <AccountView userInfo={cloudUserInfo} onDone={() => switchTab("chat")} />}
<ChatView
ref={chatViewRef}
isHidden={tab !== "chat"}
Expand Down
1 change: 1 addition & 0 deletions webview-ui/src/context/ExtensionStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
terminalZdotdir: false, // Default ZDOTDIR handling setting
terminalCompressProgressBar: true, // Default to compress progress bar output
historyPreviewCollapsed: false, // Initialize the new state (default to expanded)
cloudUserInfo: null,
organizationAllowList: ORGANIZATION_ALLOW_ALL,
autoCondenseContext: true,
autoCondenseContextPercent: 100,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ describe("mergeExtensionState", () => {
showRooIgnoredFiles: true,
renderContext: "sidebar",
maxReadFileLine: 500,
cloudUserInfo: null,
organizationAllowList: { allowAll: true, providers: {} },
autoCondenseContext: true,
autoCondenseContextPercent: 100,
Expand Down
Loading