Compare commits

...

8 Commits

Author SHA1 Message Date
Zhongying Qiao 6e8f7ef629 Merge branch 'account-sync' of github.com:cline/cline into account-sync 2025-12-10 13:44:42 -08:00
Zhongying Qiao 9be15b8cc4 attempt to fix e2e test 2025-12-10 13:44:26 -08:00
Zhongying Qiao 312e229b08 Merge branch 'main' into account-sync 2025-12-10 13:01:13 -08:00
Zhongying Qiao 6db409864b Merge branch 'account-sync' of github.com:cline/cline into account-sync 2025-12-08 15:04:08 -08:00
Zhongying Qiao b01fe70655 fix e2e test 2025-12-08 15:03:30 -08:00
Zhongying Qiao 1b7352e936 Merge branch 'main' into account-sync 2025-12-08 14:45:27 -08:00
Zhongying Qiao 56b647be0c add new method to test mock 2025-12-08 13:40:51 -08:00
Zhongying Qiao c40cda6d3c ensure extension displays new org after creating them on dashboard 2025-12-08 12:57:31 -08:00
8 changed files with 84 additions and 4 deletions
+4
View File
@@ -254,6 +254,10 @@ async function ensureUserInOrgWithRemoteConfig(controller: Controller): Promise<
*/
export async function fetchRemoteConfig(controller: Controller) {
try {
await controller.authService.refreshUserInfo().catch((err) => {
console.debug("User info refresh failed (using cached data):", err)
})
await ensureUserInOrgWithRemoteConfig(controller)
} catch (error) {
console.error("Failed to fetch remote config", error)
+2 -2
View File
@@ -240,8 +240,8 @@ export class ClineAccountService {
console.error("Error switching account:", error)
throw error
} finally {
// After user switches account, we will force a refresh of the id token by calling this function that restores the refresh token and retrieves new auth info
await this._authService.restoreRefreshTokenAndRetrieveAuthInfo()
// After user switches account, force a refresh of user info to ensure organization list is up-to-date
await this._authService.refreshUserInfo()
}
}
+26
View File
@@ -347,6 +347,32 @@ export class AuthService {
}
}
/**
* Forces a refresh of the user info from the API, even if the token is still valid.
* This is useful when user data (like organization membership) may have changed on the backend.
* @returns Promise that resolves when the refresh is complete
*/
async refreshUserInfo(): Promise<void> {
if (!this._provider) {
throw new Error("Auth provider is not set")
}
if (!this._clineAuthInfo) {
console.warn("Cannot refresh user info: not authenticated")
return
}
try {
const updatedAuthInfo = await this._provider.fetchAndUpdateUserInfo(this._controller, this._clineAuthInfo)
if (updatedAuthInfo) {
this._clineAuthInfo = updatedAuthInfo
await this.sendAuthStatusUpdate()
}
} catch (error) {
console.error("Error refreshing user info:", error)
}
}
private async retrieveAuthInfo(): Promise<ClineAuthInfo | null> {
if (!this._provider) {
throw new Error("Auth provider is not set")
+13
View File
@@ -146,4 +146,17 @@ export class AuthServiceMock extends AuthService {
return
}
}
override async refreshUserInfo(): Promise<void> {
if (!this._clineAuthInfo) {
console.warn("Cannot refresh user info: not authenticated")
return
}
try {
await this.sendAuthStatusUpdate()
} catch (error) {
console.error("Error refreshing user info (mock):", error)
}
}
}
@@ -445,6 +445,40 @@ export class ClineAuthProvider implements IAuthProvider {
}
}
/**
* Fetches fresh user info from the API and updates the stored auth info.
* This is useful when user data (like organization membership) may have changed on the backend.
* @param controller - The controller instance to access stored secrets.
* @param currentAuthInfo - The current auth info with a valid access token.
* @returns {Promise<ClineAuthInfo | null>} Updated auth info or null if failed.
*/
async fetchAndUpdateUserInfo(controller: Controller, currentAuthInfo: ClineAuthInfo): Promise<ClineAuthInfo | null> {
try {
const userResponse = await axios.get(`${ClineEnv.config().apiBaseUrl}/api/v1/users/me`, {
headers: {
Authorization: `Bearer workos:${currentAuthInfo.idToken}`,
},
...getAxiosSettings(),
})
const freshUserInfo: ClineAccountUserInfo = userResponse.data.data
const updatedAuthInfo: ClineAuthInfo = {
...currentAuthInfo,
userInfo: freshUserInfo,
}
const updatedAuthInfoString = JSON.stringify(updatedAuthInfo)
controller.stateManager.setSecret("cline:clineAccountId", updatedAuthInfoString)
Logger.debug("User info refreshed successfully")
return updatedAuthInfo
} catch (error) {
Logger.error("Error fetching and updating user info:", error)
return null
}
}
private async fetchRemoteUserInfo(tokenData: ClineAuthApiTokenExchangeResponse["data"]): Promise<ClineAccountUserInfo> {
try {
const userResponse = await axios.get(`${ClineEnv.config().apiBaseUrl}/api/v1/users/me`, {
@@ -10,4 +10,5 @@ export interface IAuthProvider {
refreshToken(refreshToken: string, storedData: ClineAuthInfo): Promise<Partial<ClineAuthInfo>>
getAuthRequest(callbackUrl: string): Promise<string>
signIn(controller: Controller, authorizationCode: string, provider: string): Promise<ClineAuthInfo | null>
fetchAndUpdateUserInfo(controller: Controller, currentAuthInfo: ClineAuthInfo): Promise<ClineAuthInfo | null>
}
+2 -1
View File
@@ -11,7 +11,8 @@ e2e("Chat - can send messages and switch between modes", async ({ helper, sideba
await inputbox.fill("Hello, Cline!")
await expect(inputbox).toHaveValue("Hello, Cline!")
await sidebar.getByTestId("send-button").click()
await expect(inputbox).toHaveValue("")
// Wait for input to be cleared after sending (may take a moment for state to update)
await expect(inputbox).toHaveValue("", { timeout: 10000 })
// Starting a new task should clear the current chat view and show the recent tasks
await sidebar.getByRole("button", { name: "New Task", exact: true }).first().click()
+2 -1
View File
@@ -14,7 +14,8 @@ e2e.describe("Diff Editor", () => {
await inputbox.fill("[diff.test.ts] Hello, Cline!")
await expect(inputbox).toHaveValue("[diff.test.ts] Hello, Cline!")
await sidebar.getByTestId("send-button").click()
await expect(inputbox).toHaveValue("")
// Wait for input to be cleared after sending (may take a moment for state to update)
await expect(inputbox).toHaveValue("", { timeout: 10000 })
// Loading State initially
await expect(sidebar.getByText("API Request...")).toBeVisible({ timeout: 10000 })