fix(vscode-ide-companion): slight delay to ensure auth state settlement

This commit is contained in:
yiliang114
2025-12-12 01:14:28 +08:00
parent c20df192a8
commit 25261ab88d
5 changed files with 93 additions and 175 deletions

View File

@@ -21,8 +21,8 @@ export class AuthStateManager {
private static context: vscode.ExtensionContext | null = null; private static context: vscode.ExtensionContext | null = null;
private static readonly AUTH_STATE_KEY = 'qwen.authState'; private static readonly AUTH_STATE_KEY = 'qwen.authState';
private static readonly AUTH_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours private static readonly AUTH_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
// Deduplicate concurrent auth flows (e.g., multiple tabs prompting login) // Deduplicate concurrent auth processes (e.g., multiple tabs prompting login)
private static authFlowInFlight: Promise<unknown> | null = null; private static authProcessInFlight: Promise<unknown> | null = null;
private constructor() {} private constructor() {}
/** /**
@@ -42,24 +42,38 @@ export class AuthStateManager {
} }
/** /**
* Run an auth-related flow exclusively. If another flow is already running, * Run an auth-related flow with optional queueing.
* return the same promise to prevent duplicate login prompts. * - 默认:复用在跑的 promise避免重复弹窗。
* - forceNew: true 时,等待当前 flow 结束后再串行启动新的,用于强制重登。
*/ */
static runExclusiveAuth<T>(task: () => Promise<T>): Promise<T> { static runExclusiveAuth<T>(
if (AuthStateManager.authFlowInFlight) { task: () => Promise<T>,
return AuthStateManager.authFlowInFlight as Promise<T>; options?: { forceNew?: boolean },
): Promise<T> {
if (AuthStateManager.authProcessInFlight) {
if (!options?.forceNew) {
return AuthStateManager.authProcessInFlight as Promise<T>;
}
// queue a new flow after current finishes
const next = AuthStateManager.authProcessInFlight
.catch(() => {
/* ignore previous failure for next run */
})
.then(() =>
AuthStateManager.runExclusiveAuth(task, { forceNew: false }),
);
return next as Promise<T>;
} }
const p = Promise.resolve() const p = Promise.resolve()
.then(task) .then(task)
.finally(() => { .finally(() => {
// Clear only if this promise is still the active one if (AuthStateManager.authProcessInFlight === p) {
if (AuthStateManager.authFlowInFlight === p) { AuthStateManager.authProcessInFlight = null;
AuthStateManager.authFlowInFlight = null;
} }
}); });
AuthStateManager.authFlowInFlight = p; AuthStateManager.authProcessInFlight = p;
return p as Promise<T>; return p as Promise<T>;
} }

View File

@@ -1222,6 +1222,7 @@ export class QwenAgentManager {
if (effectiveAuth) { if (effectiveAuth) {
await effectiveAuth.saveAuthState(workingDir, authMethod); await effectiveAuth.saveAuthState(workingDir, authMethod);
} }
await setTimeout(() => Promise.resolve(), 100); // slight delay to ensure auth state is settled
await this.connection.newSession(workingDir); await this.connection.newSession(workingDir);
} catch (reauthErr) { } catch (reauthErr) {
// Clear potentially stale cache on failure and rethrow // Clear potentially stale cache on failure and rethrow

View File

@@ -119,11 +119,6 @@ export class QwenConnectionHandler {
// Save auth state // Save auth state
if (authStateManager) { if (authStateManager) {
console.log(
'[QwenAgentManager] Saving auth state after successful authentication',
);
console.log('[QwenAgentManager] Working dir for save:', workingDir);
console.log('[QwenAgentManager] Auth method for save:', authMethod);
await authStateManager.saveAuthState(workingDir, authMethod); await authStateManager.saveAuthState(workingDir, authMethod);
console.log('[QwenAgentManager] Auth state save completed'); console.log('[QwenAgentManager] Auth state save completed');
} }
@@ -145,6 +140,7 @@ export class QwenConnectionHandler {
} }
try { try {
await setTimeout(() => Promise.resolve(), 100); // slight delay to ensure auth state is settled
console.log( console.log(
'[QwenAgentManager] Creating new session after authentication...', '[QwenAgentManager] Creating new session after authentication...',
); );

View File

@@ -134,7 +134,7 @@ export class WebViewProvider {
// Note: Tool call updates are handled in handleSessionUpdate within QwenAgentManager // Note: Tool call updates are handled in handleSessionUpdate within QwenAgentManager
// and sent via onStreamChunk callback // and sent via onStreamChunk callback
this.agentManager.onToolCall((update) => { this.agentManager.onToolCall((update) => {
// Always surface tool calls; they are part of the live assistant flow. // Always surface tool calls; they are part of the live assistant process.
// Cast update to access sessionUpdate property // Cast update to access sessionUpdate property
const updateData = update as unknown as Record<string, unknown>; const updateData = update as unknown as Record<string, unknown>;
@@ -673,7 +673,7 @@ export class WebViewProvider {
!!this.authStateManager, !!this.authStateManager,
); );
// If a login/connection flow is already running, reuse it to avoid double prompts // If a login/connection process is already running, reuse it to avoid double prompts
const p = Promise.resolve( const p = Promise.resolve(
vscode.window.withProgress( vscode.window.withProgress(
{ {

View File

@@ -149,6 +149,50 @@ export class SessionMessageHandler extends BaseMessageHandler {
return this.isSavingCheckpoint; return this.isSavingCheckpoint;
} }
/**
* Prompt user to login and invoke the registered login handler/command.
* Returns true if a login was initiated.
*/
private async promptLogin(message: string): Promise<boolean> {
const result = await vscode.window.showWarningMessage(message, 'Login Now');
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
return true;
}
return false;
}
/**
* Prompt user to login or view offline. Returns 'login', 'offline', or 'dismiss'.
* When login is chosen, it triggers the login handler/command.
*/
private async promptLoginOrOffline(
message: string,
): Promise<'login' | 'offline' | 'dismiss'> {
const selection = await vscode.window.showWarningMessage(
message,
'Login Now',
'View Offline',
);
if (selection === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
return 'login';
}
if (selection === 'View Offline') {
return 'offline';
}
return 'dismiss';
}
/** /**
* Handle send message request * Handle send message request
*/ */
@@ -271,23 +315,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
console.warn('[SessionMessageHandler] Agent not connected'); console.warn('[SessionMessageHandler] Agent not connected');
// Show non-modal notification with Login button // Show non-modal notification with Login button
const result = await vscode.window.showWarningMessage( await this.promptLogin('You need to login first to use Qwen Code.');
'You need to login first to use Qwen Code.',
'Login Now',
);
if (result === 'Login Now') {
// Use login handler directly
if (this.loginHandler) {
await this.loginHandler();
} else {
// Fallback to command
vscode.window.showInformationMessage(
'Please wait while we connect to Qwen Code...',
);
await vscode.commands.executeCommand('qwen-code.login');
}
}
return; return;
} }
@@ -308,17 +336,9 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('Authentication required') || errorMsg.includes('Authentication required') ||
errorMsg.includes('(code: -32000)') errorMsg.includes('(code: -32000)')
) { ) {
const result = await vscode.window.showWarningMessage( await this.promptLogin(
'Your login session has expired or is invalid. Please login again to continue using Qwen Code.', 'Your login session has expired or is invalid. Please login again to continue using Qwen Code.',
'Login Now',
); );
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
return; return;
} }
vscode.window.showErrorMessage(`Failed to create session: ${errorMsg}`); vscode.window.showErrorMessage(`Failed to create session: ${errorMsg}`);
@@ -426,19 +446,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('Invalid token') errorMsg.includes('Invalid token')
) { ) {
// Show a more user-friendly error message for expired sessions // Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage( await this.promptLogin(
'Your login session has expired or is invalid. Please login again to continue using Qwen Code.', 'Your login session has expired or is invalid. Please login again to continue using Qwen Code.',
'Login Now',
); );
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling // Send a specific error to the webview for better UI handling
this.sendToWebView({ this.sendToWebView({
type: 'sessionExpired', type: 'sessionExpired',
@@ -463,17 +474,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
// Ensure connection (login) before creating a new session // Ensure connection (login) before creating a new session
if (!this.agentManager.isConnected) { if (!this.agentManager.isConnected) {
const result = await vscode.window.showWarningMessage( const proceeded = await this.promptLogin(
'You need to login before creating a new session.', 'You need to login before creating a new session.',
'Login Now',
); );
if (result === 'Login Now') { if (!proceeded) {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
} else {
return; return;
} }
} }
@@ -524,19 +528,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('No active ACP session') errorMsg.includes('No active ACP session')
) { ) {
// Show a more user-friendly error message for expired sessions // Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage( await this.promptLogin(
'Your login session has expired or is invalid. Please login again to create a new session.', 'Your login session has expired or is invalid. Please login again to create a new session.',
'Login Now',
); );
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling // Send a specific error to the webview for better UI handling
this.sendToWebView({ this.sendToWebView({
type: 'sessionExpired', type: 'sessionExpired',
@@ -560,19 +555,11 @@ export class SessionMessageHandler extends BaseMessageHandler {
// If not connected yet, offer to login or view offline // If not connected yet, offer to login or view offline
if (!this.agentManager.isConnected) { if (!this.agentManager.isConnected) {
const selection = await vscode.window.showWarningMessage( const choice = await this.promptLoginOrOffline(
'You are not logged in. Login now to fully restore this session, or view it offline.', 'You are not logged in. Login now to fully restore this session, or view it offline.',
'Login Now',
'View Offline',
); );
if (selection === 'Login Now') { if (choice === 'offline') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
} else if (selection === 'View Offline') {
// Show messages from local cache only // Show messages from local cache only
const messages = const messages =
await this.agentManager.getSessionMessages(sessionId); await this.agentManager.getSessionMessages(sessionId);
@@ -585,7 +572,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
'Showing cached session content. Login to interact with the AI.', 'Showing cached session content. Login to interact with the AI.',
); );
return; return;
} else { } else if (choice !== 'login') {
// User dismissed; do nothing // User dismissed; do nothing
return; return;
} }
@@ -672,19 +659,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('No active ACP session') errorMsg.includes('No active ACP session')
) { ) {
// Show a more user-friendly error message for expired sessions // Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage( await this.promptLogin(
'Your login session has expired or is invalid. Please login again to switch sessions.', 'Your login session has expired or is invalid. Please login again to switch sessions.',
'Login Now',
); );
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling // Send a specific error to the webview for better UI handling
this.sendToWebView({ this.sendToWebView({
type: 'sessionExpired', type: 'sessionExpired',
@@ -741,19 +719,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
createErrorMsg.includes('No active ACP session') createErrorMsg.includes('No active ACP session')
) { ) {
// Show a more user-friendly error message for expired sessions // Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage( await this.promptLogin(
'Your login session has expired or is invalid. Please login again to switch sessions.', 'Your login session has expired or is invalid. Please login again to switch sessions.',
'Login Now',
); );
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling // Send a specific error to the webview for better UI handling
this.sendToWebView({ this.sendToWebView({
type: 'sessionExpired', type: 'sessionExpired',
@@ -790,19 +759,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('No active ACP session') errorMsg.includes('No active ACP session')
) { ) {
// Show a more user-friendly error message for expired sessions // Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage( await this.promptLogin(
'Your login session has expired or is invalid. Please login again to switch sessions.', 'Your login session has expired or is invalid. Please login again to switch sessions.',
'Login Now',
); );
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling // Send a specific error to the webview for better UI handling
this.sendToWebView({ this.sendToWebView({
type: 'sessionExpired', type: 'sessionExpired',
@@ -854,19 +814,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('No active ACP session') errorMsg.includes('No active ACP session')
) { ) {
// Show a more user-friendly error message for expired sessions // Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage( await this.promptLogin(
'Your login session has expired or is invalid. Please login again to view sessions.', 'Your login session has expired or is invalid. Please login again to view sessions.',
'Login Now',
); );
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling // Send a specific error to the webview for better UI handling
this.sendToWebView({ this.sendToWebView({
type: 'sessionExpired', type: 'sessionExpired',
@@ -918,19 +869,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('No active ACP session') errorMsg.includes('No active ACP session')
) { ) {
// Show a more user-friendly error message for expired sessions // Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage( await this.promptLogin(
'Your login session has expired or is invalid. Please login again to save sessions.', 'Your login session has expired or is invalid. Please login again to save sessions.',
'Login Now',
); );
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling // Send a specific error to the webview for better UI handling
this.sendToWebView({ this.sendToWebView({
type: 'sessionExpired', type: 'sessionExpired',
@@ -966,19 +908,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('No active ACP session') errorMsg.includes('No active ACP session')
) { ) {
// Show a more user-friendly error message for expired sessions // Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage( await this.promptLogin(
'Your login session has expired or is invalid. Please login again to save sessions.', 'Your login session has expired or is invalid. Please login again to save sessions.',
'Login Now',
); );
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling // Send a specific error to the webview for better UI handling
this.sendToWebView({ this.sendToWebView({
type: 'sessionExpired', type: 'sessionExpired',
@@ -1031,19 +964,11 @@ export class SessionMessageHandler extends BaseMessageHandler {
try { try {
// If not connected, offer to login or view offline // If not connected, offer to login or view offline
if (!this.agentManager.isConnected) { if (!this.agentManager.isConnected) {
const selection = await vscode.window.showWarningMessage( const choice = await this.promptLoginOrOffline(
'You are not logged in. Login now to fully restore this session, or view it offline.', 'You are not logged in. Login now to fully restore this session, or view it offline.',
'Login Now',
'View Offline',
); );
if (selection === 'Login Now') { if (choice === 'offline') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
} else if (selection === 'View Offline') {
const messages = const messages =
await this.agentManager.getSessionMessages(sessionId); await this.agentManager.getSessionMessages(sessionId);
this.currentConversationId = sessionId; this.currentConversationId = sessionId;
@@ -1055,7 +980,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
'Showing cached session content. Login to interact with the AI.', 'Showing cached session content. Login to interact with the AI.',
); );
return; return;
} else { } else if (choice !== 'login') {
return; return;
} }
} }
@@ -1089,19 +1014,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('No active ACP session') errorMsg.includes('No active ACP session')
) { ) {
// Show a more user-friendly error message for expired sessions // Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage( await this.promptLogin(
'Your login session has expired or is invalid. Please login again to resume sessions.', 'Your login session has expired or is invalid. Please login again to resume sessions.',
'Login Now',
); );
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling // Send a specific error to the webview for better UI handling
this.sendToWebView({ this.sendToWebView({
type: 'sessionExpired', type: 'sessionExpired',
@@ -1140,19 +1056,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('No active ACP session') errorMsg.includes('No active ACP session')
) { ) {
// Show a more user-friendly error message for expired sessions // Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage( await this.promptLogin(
'Your login session has expired or is invalid. Please login again to resume sessions.', 'Your login session has expired or is invalid. Please login again to resume sessions.',
'Login Now',
); );
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling // Send a specific error to the webview for better UI handling
this.sendToWebView({ this.sendToWebView({
type: 'sessionExpired', type: 'sessionExpired',