diff --git a/packages/vscode-ide-companion/src/extension.ts b/packages/vscode-ide-companion/src/extension.ts index 2adfaef1..08979099 100644 --- a/packages/vscode-ide-companion/src/extension.ts +++ b/packages/vscode-ide-companion/src/extension.ts @@ -8,7 +8,11 @@ import * as vscode from 'vscode'; import { IDEServer } from './ide-server.js'; import semver from 'semver'; import { DiffContentProvider, DiffManager } from './diff-manager.js'; -import { createLogger } from './utils/logger.js'; +import { + createLogger, + getConsoleLogger, + initSharedConsoleLogger, +} from './utils/logger.js'; import { detectIdeFromEnv, IDE_DEFINITIONS, @@ -105,6 +109,8 @@ async function checkForUpdates( export async function activate(context: vscode.ExtensionContext) { logger = vscode.window.createOutputChannel('Qwen Code Companion'); + initSharedConsoleLogger(context); + const consoleLog = getConsoleLogger(); log = createLogger(context, logger); log('Extension activated'); @@ -142,18 +148,18 @@ export async function activate(context: vscode.ExtensionContext) { webviewPanel: vscode.WebviewPanel, state: unknown, ) { - console.log( + consoleLog( '[Extension] Deserializing WebView panel with state:', state, ); // Create a new provider for the restored panel const provider = createWebViewProvider(); - console.log('[Extension] Provider created for deserialization'); + consoleLog('[Extension] Provider created for deserialization'); // Restore state if available BEFORE restoring the panel if (state && typeof state === 'object') { - console.log('[Extension] Restoring state:', state); + consoleLog('[Extension] Restoring state:', state); provider.restoreState( state as { conversationId: string | null; @@ -161,11 +167,11 @@ export async function activate(context: vscode.ExtensionContext) { }, ); } else { - console.log('[Extension] No state to restore or invalid state'); + consoleLog('[Extension] No state to restore or invalid state'); } await provider.restorePanel(webviewPanel); - console.log('[Extension] Panel restore completed'); + consoleLog('[Extension] Panel restore completed'); log('WebView panel restored from serialization'); }, @@ -206,7 +212,6 @@ export async function activate(context: vscode.ExtensionContext) { } catch (err) { console.warn('[Extension] Auto-allow on diff.accept failed:', err); } - console.log('[Extension] Diff accepted'); }), vscode.commands.registerCommand('qwen.diff.cancel', (uri?: vscode.Uri) => { const docUri = uri ?? vscode.window.activeTextEditor?.document.uri; @@ -223,7 +228,6 @@ export async function activate(context: vscode.ExtensionContext) { } catch (err) { console.warn('[Extension] Auto-reject on diff.cancel failed:', err); } - console.log('[Extension] Diff cancelled'); })), vscode.commands.registerCommand('qwen.diff.closeAll', async () => { try { diff --git a/packages/vscode-ide-companion/src/services/acpConnection.ts b/packages/vscode-ide-companion/src/services/acpConnection.ts index 5486e14d..4dd35b71 100644 --- a/packages/vscode-ide-companion/src/services/acpConnection.ts +++ b/packages/vscode-ide-companion/src/services/acpConnection.ts @@ -31,6 +31,8 @@ export class AcpConnection { private child: ChildProcess | null = null; private pendingRequests = new Map>(); private nextRequestId = { value: 0 }; + // Deduplicate concurrent authenticate calls (across retry paths) + private static authInFlight: Promise | null = null; // Remember the working dir provided at connect() so later ACP calls // that require cwd (e.g. session/list) can include it. private workingDir: string = process.cwd(); @@ -271,12 +273,23 @@ export class AcpConnection { * @returns Authentication response */ async authenticate(methodId?: string): Promise { - return this.sessionManager.authenticate( - methodId, - this.child, - this.pendingRequests, - this.nextRequestId, - ); + if (AcpConnection.authInFlight) { + return AcpConnection.authInFlight; + } + + const p = this.sessionManager + .authenticate( + methodId, + this.child, + this.pendingRequests, + this.nextRequestId, + ) + .finally(() => { + AcpConnection.authInFlight = null; + }); + + AcpConnection.authInFlight = p; + return p; } /** diff --git a/packages/vscode-ide-companion/src/services/authStateManager.ts b/packages/vscode-ide-companion/src/services/authStateManager.ts index 566a4afb..aa75fe36 100644 --- a/packages/vscode-ide-companion/src/services/authStateManager.ts +++ b/packages/vscode-ide-companion/src/services/authStateManager.ts @@ -5,6 +5,7 @@ */ import type * as vscode from 'vscode'; +import { createConsoleLogger, getConsoleLogger } from '../utils/logger.js'; interface AuthState { isAuthenticated: boolean; @@ -21,6 +22,9 @@ export class AuthStateManager { private static context: vscode.ExtensionContext | null = null; private static readonly AUTH_STATE_KEY = 'qwen.authState'; private static readonly AUTH_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours + private static consoleLog: (...args: unknown[]) => void = getConsoleLogger(); + // Deduplicate concurrent auth flows (e.g., multiple tabs prompting login) + private static authFlowInFlight: Promise | null = null; private constructor() {} /** @@ -34,11 +38,37 @@ export class AuthStateManager { // If a context is provided, update the static context if (context) { AuthStateManager.context = context; + AuthStateManager.consoleLog = createConsoleLogger( + context, + 'AuthStateManager', + ); } return AuthStateManager.instance; } + /** + * Run an auth-related flow exclusively. If another flow is already running, + * return the same promise to prevent duplicate login prompts. + */ + static runExclusiveAuth(task: () => Promise): Promise { + if (AuthStateManager.authFlowInFlight) { + return AuthStateManager.authFlowInFlight as Promise; + } + + const p = Promise.resolve() + .then(task) + .finally(() => { + // Clear only if this promise is still the active one + if (AuthStateManager.authFlowInFlight === p) { + AuthStateManager.authFlowInFlight = null; + } + }); + + AuthStateManager.authFlowInFlight = p; + return p as Promise; + } + /** * Check if there's a valid cached authentication */ @@ -46,17 +76,19 @@ export class AuthStateManager { const state = await this.getAuthState(); if (!state) { - console.log('[AuthStateManager] No cached auth state found'); + AuthStateManager.consoleLog( + '[AuthStateManager] No cached auth state found', + ); return false; } - console.log('[AuthStateManager] Found cached auth state:', { + AuthStateManager.consoleLog('[AuthStateManager] Found cached auth state:', { workingDir: state.workingDir, authMethod: state.authMethod, timestamp: new Date(state.timestamp).toISOString(), isAuthenticated: state.isAuthenticated, }); - console.log('[AuthStateManager] Checking against:', { + AuthStateManager.consoleLog('[AuthStateManager] Checking against:', { workingDir, authMethod, }); @@ -67,8 +99,8 @@ export class AuthStateManager { now - state.timestamp > AuthStateManager.AUTH_CACHE_DURATION; if (isExpired) { - console.log('[AuthStateManager] Cached auth expired'); - console.log( + AuthStateManager.consoleLog('[AuthStateManager] Cached auth expired'); + AuthStateManager.consoleLog( '[AuthStateManager] Cache age:', Math.floor((now - state.timestamp) / 1000 / 60), 'minutes', @@ -82,15 +114,29 @@ export class AuthStateManager { state.workingDir === workingDir && state.authMethod === authMethod; if (!isSameContext) { - console.log('[AuthStateManager] Working dir or auth method changed'); - console.log('[AuthStateManager] Cached workingDir:', state.workingDir); - console.log('[AuthStateManager] Current workingDir:', workingDir); - console.log('[AuthStateManager] Cached authMethod:', state.authMethod); - console.log('[AuthStateManager] Current authMethod:', authMethod); + AuthStateManager.consoleLog( + '[AuthStateManager] Working dir or auth method changed', + ); + AuthStateManager.consoleLog( + '[AuthStateManager] Cached workingDir:', + state.workingDir, + ); + AuthStateManager.consoleLog( + '[AuthStateManager] Current workingDir:', + workingDir, + ); + AuthStateManager.consoleLog( + '[AuthStateManager] Cached authMethod:', + state.authMethod, + ); + AuthStateManager.consoleLog( + '[AuthStateManager] Current authMethod:', + authMethod, + ); return false; } - console.log('[AuthStateManager] Valid cached auth found'); + AuthStateManager.consoleLog('[AuthStateManager] Valid cached auth found'); return state.isAuthenticated; } @@ -100,7 +146,10 @@ export class AuthStateManager { */ async debugAuthState(): Promise { const state = await this.getAuthState(); - console.log('[AuthStateManager] DEBUG - Current auth state:', state); + AuthStateManager.consoleLog( + '[AuthStateManager] DEBUG - Current auth state:', + state, + ); if (state) { const now = Date.now(); @@ -108,9 +157,16 @@ export class AuthStateManager { const isExpired = now - state.timestamp > AuthStateManager.AUTH_CACHE_DURATION; - console.log('[AuthStateManager] DEBUG - Auth state age:', age, 'minutes'); - console.log('[AuthStateManager] DEBUG - Auth state expired:', isExpired); - console.log( + AuthStateManager.consoleLog( + '[AuthStateManager] DEBUG - Auth state age:', + age, + 'minutes', + ); + AuthStateManager.consoleLog( + '[AuthStateManager] DEBUG - Auth state expired:', + isExpired, + ); + AuthStateManager.consoleLog( '[AuthStateManager] DEBUG - Auth state valid:', state.isAuthenticated, ); @@ -135,7 +191,7 @@ export class AuthStateManager { timestamp: Date.now(), }; - console.log('[AuthStateManager] Saving auth state:', { + AuthStateManager.consoleLog('[AuthStateManager] Saving auth state:', { workingDir, authMethod, timestamp: new Date(state.timestamp).toISOString(), @@ -145,11 +201,14 @@ export class AuthStateManager { AuthStateManager.AUTH_STATE_KEY, state, ); - console.log('[AuthStateManager] Auth state saved'); + AuthStateManager.consoleLog('[AuthStateManager] Auth state saved'); // Verify the state was saved correctly const savedState = await this.getAuthState(); - console.log('[AuthStateManager] Verified saved state:', savedState); + AuthStateManager.consoleLog( + '[AuthStateManager] Verified saved state:', + savedState, + ); } /** @@ -163,9 +222,9 @@ export class AuthStateManager { ); } - console.log('[AuthStateManager] Clearing auth state'); + AuthStateManager.consoleLog('[AuthStateManager] Clearing auth state'); const currentState = await this.getAuthState(); - console.log( + AuthStateManager.consoleLog( '[AuthStateManager] Current state before clearing:', currentState, ); @@ -174,11 +233,14 @@ export class AuthStateManager { AuthStateManager.AUTH_STATE_KEY, undefined, ); - console.log('[AuthStateManager] Auth state cleared'); + AuthStateManager.consoleLog('[AuthStateManager] Auth state cleared'); // Verify the state was cleared const newState = await this.getAuthState(); - console.log('[AuthStateManager] State after clearing:', newState); + AuthStateManager.consoleLog( + '[AuthStateManager] State after clearing:', + newState, + ); } /** @@ -187,17 +249,15 @@ export class AuthStateManager { private async getAuthState(): Promise { // Ensure we have a valid context if (!AuthStateManager.context) { - console.log( + AuthStateManager.consoleLog( '[AuthStateManager] No context available for getting auth state', ); return undefined; } - const a = AuthStateManager.context.globalState.get( + return AuthStateManager.context.globalState.get( AuthStateManager.AUTH_STATE_KEY, ); - console.log('[AuthStateManager] Auth state:', a); - return a; } /** diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index a57d15b7..b954aed8 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -23,6 +23,7 @@ import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js'; import { CliContextManager } from '../cli/cliContextManager.js'; import { authMethod } from '../types/acpTypes.js'; import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from '../cli/cliVersionManager.js'; +import { getConsoleLogger } from '../utils/logger.js'; export type { ChatMessage, PlanEntry, ToolCallUpdateData }; @@ -45,11 +46,15 @@ export class QwenAgentManager { // Cache the last used AuthStateManager so internal calls (e.g. fallback paths) // can reuse it and avoid forcing a fresh authentication unnecessarily. private defaultAuthStateManager?: AuthStateManager; + // Deduplicate concurrent session/new attempts + private sessionCreateInFlight: Promise | null = null; // Callback storage private callbacks: QwenAgentCallbacks = {}; + private consoleLog: (...args: unknown[]) => void; - constructor() { + constructor(consoleLogger = getConsoleLogger()) { + this.consoleLog = consoleLogger; this.connection = new AcpConnection(); this.sessionReader = new QwenSessionReader(); this.sessionManager = new QwenSessionManager(); @@ -76,7 +81,7 @@ export class QwenAgentManager { ).update; const text = update?.content?.text || ''; if (update?.sessionUpdate === 'user_message_chunk' && text) { - console.log( + this.consoleLog( '[QwenAgentManager] Rehydration: routing user message chunk', ); this.callbacks.onMessage?.({ @@ -87,7 +92,7 @@ export class QwenAgentManager { return; } if (update?.sessionUpdate === 'agent_message_chunk' && text) { - console.log( + this.consoleLog( '[QwenAgentManager] Rehydration: routing agent message chunk', ); this.callbacks.onMessage?.({ @@ -98,7 +103,7 @@ export class QwenAgentManager { return; } // For other types during rehydration, fall through to normal handler - console.log( + this.consoleLog( '[QwenAgentManager] Rehydration: non-text update, forwarding to handler', ); } @@ -257,7 +262,7 @@ export class QwenAgentManager { * @returns Session list */ async getSessionList(): Promise>> { - console.log( + this.consoleLog( '[QwenAgentManager] Getting session list with version-aware strategy', ); @@ -265,7 +270,7 @@ export class QwenAgentManager { const cliContextManager = CliContextManager.getInstance(); const supportsSessionList = cliContextManager.supportsSessionList(); - console.log( + this.consoleLog( '[QwenAgentManager] CLI supports session/list:', supportsSessionList, ); @@ -273,11 +278,14 @@ export class QwenAgentManager { // Try ACP method first if supported if (supportsSessionList) { try { - console.log( + this.consoleLog( '[QwenAgentManager] Attempting to get session list via ACP method', ); const response = await this.connection.listSessions(); - console.log('[QwenAgentManager] ACP session list response:', response); + this.consoleLog( + '[QwenAgentManager] ACP session list response:', + response, + ); // sendRequest resolves with the JSON-RPC "result" directly // Newer CLI returns an object: { items: [...], nextCursor?, hasMore } @@ -295,7 +303,7 @@ export class QwenAgentManager { : []; } - console.log( + this.consoleLog( '[QwenAgentManager] Sessions retrieved via ACP:', res, items.length, @@ -314,7 +322,7 @@ export class QwenAgentManager { cwd: item.cwd, })); - console.log( + this.consoleLog( '[QwenAgentManager] Sessions retrieved via ACP:', sessions.length, ); @@ -330,9 +338,11 @@ export class QwenAgentManager { // Always fall back to file system method try { - console.log('[QwenAgentManager] Getting session list from file system'); + this.consoleLog( + '[QwenAgentManager] Getting session list from file system', + ); const sessions = await this.sessionReader.getAllSessions(undefined, true); - console.log( + this.consoleLog( '[QwenAgentManager] Session list from file system (all projects):', sessions.length, ); @@ -350,7 +360,7 @@ export class QwenAgentManager { }), ); - console.log( + this.consoleLog( '[QwenAgentManager] Sessions retrieved from file system:', result.length, ); @@ -490,7 +500,7 @@ export class QwenAgentManager { const item = list.find( (s) => s.sessionId === sessionId || s.id === sessionId, ); - console.log( + this.consoleLog( '[QwenAgentManager] Session list item for filePath lookup:', item, ); @@ -561,7 +571,7 @@ export class QwenAgentManager { } } // Simple linear reconstruction: filter user/assistant and sort by timestamp - console.log( + this.consoleLog( '[QwenAgentManager] JSONL records read:', records.length, filePath, @@ -718,7 +728,7 @@ export class QwenAgentManager { // Handle other types if needed } - console.log( + this.consoleLog( '[QwenAgentManager] JSONL messages reconstructed:', msgs.length, ); @@ -856,7 +866,7 @@ export class QwenAgentManager { tag: string, ): Promise<{ success: boolean; message?: string }> { try { - console.log( + this.consoleLog( '[QwenAgentManager] Saving session via /chat save command:', sessionId, 'with tag:', @@ -867,7 +877,9 @@ export class QwenAgentManager { // The CLI will handle this as a special command await this.connection.sendPrompt(`/chat save "${tag}"`); - console.log('[QwenAgentManager] /chat save command sent successfully'); + this.consoleLog( + '[QwenAgentManager] /chat save command sent successfully', + ); return { success: true, message: `Session saved with tag: ${tag}`, @@ -914,14 +926,14 @@ export class QwenAgentManager { conversationId: string, ): Promise<{ success: boolean; tag?: string; message?: string }> { try { - console.log('[QwenAgentManager] ===== CHECKPOINT SAVE START ====='); - console.log('[QwenAgentManager] Conversation ID:', conversationId); - console.log('[QwenAgentManager] Message count:', messages.length); - console.log( + this.consoleLog('[QwenAgentManager] ===== CHECKPOINT SAVE START ====='); + this.consoleLog('[QwenAgentManager] Conversation ID:', conversationId); + this.consoleLog('[QwenAgentManager] Message count:', messages.length); + this.consoleLog( '[QwenAgentManager] Current working dir:', this.currentWorkingDir, ); - console.log( + this.consoleLog( '[QwenAgentManager] Current session ID (from CLI):', this.currentSessionId, ); @@ -998,11 +1010,11 @@ export class QwenAgentManager { try { // Route upcoming session/update messages as discrete messages for replay this.rehydratingSessionId = sessionId; - console.log( + this.consoleLog( '[QwenAgentManager] Rehydration start for session:', sessionId, ); - console.log( + this.consoleLog( '[QwenAgentManager] Attempting session/load via ACP for session:', sessionId, ); @@ -1010,7 +1022,7 @@ export class QwenAgentManager { sessionId, cwdOverride, ); - console.log( + this.consoleLog( '[QwenAgentManager] Session load succeeded. Response:', JSON.stringify(response).substring(0, 200), ); @@ -1050,7 +1062,10 @@ export class QwenAgentManager { throw error; } finally { // End rehydration routing regardless of outcome - console.log('[QwenAgentManager] Rehydration end for session:', sessionId); + this.consoleLog( + '[QwenAgentManager] Rehydration end for session:', + sessionId, + ); this.rehydratingSessionId = null; } } @@ -1063,7 +1078,7 @@ export class QwenAgentManager { * @returns Loaded session messages or null */ async loadSession(sessionId: string): Promise { - console.log( + this.consoleLog( '[QwenAgentManager] Loading session with version-aware strategy:', sessionId, ); @@ -1072,7 +1087,7 @@ export class QwenAgentManager { const cliContextManager = CliContextManager.getInstance(); const supportsSessionLoad = cliContextManager.supportsSessionLoad(); - console.log( + this.consoleLog( '[QwenAgentManager] CLI supports session/load:', supportsSessionLoad, ); @@ -1080,11 +1095,13 @@ export class QwenAgentManager { // Try ACP method first if supported if (supportsSessionLoad) { try { - console.log( + this.consoleLog( '[QwenAgentManager] Attempting to load session via ACP method', ); await this.loadSessionViaAcp(sessionId); - console.log('[QwenAgentManager] Session loaded successfully via ACP'); + this.consoleLog( + '[QwenAgentManager] Session loaded successfully via ACP', + ); // After loading via ACP, we still need to get messages from file system // In future, we might get them directly from the ACP response @@ -1098,11 +1115,11 @@ export class QwenAgentManager { // Always fall back to file system method try { - console.log( + this.consoleLog( '[QwenAgentManager] Loading session messages from file system', ); const messages = await this.loadSessionMessagesFromFile(sessionId); - console.log( + this.consoleLog( '[QwenAgentManager] Session messages loaded successfully from file system', ); return messages; @@ -1125,7 +1142,7 @@ export class QwenAgentManager { sessionId: string, ): Promise { try { - console.log( + this.consoleLog( '[QwenAgentManager] Loading session from file system:', sessionId, ); @@ -1137,7 +1154,7 @@ export class QwenAgentManager { ); if (!session) { - console.log( + this.consoleLog( '[QwenAgentManager] Session not found in file system:', sessionId, ); @@ -1183,93 +1200,67 @@ export class QwenAgentManager { workingDir: string, authStateManager?: AuthStateManager, ): Promise { - console.log('[QwenAgentManager] Creating new session...'); + // Reuse existing session if present + if (this.connection.currentSessionId) { + return this.connection.currentSessionId; + } + // Deduplicate concurrent session/new attempts + if (this.sessionCreateInFlight) { + return this.sessionCreateInFlight; + } - // Check if we have valid cached authentication - let hasValidAuth = false; + this.consoleLog('[QwenAgentManager] Creating new session...'); // Prefer the provided authStateManager, otherwise fall back to the one // remembered during connect(). This prevents accidental re-auth in // fallback paths (e.g. session switching) when the handler didn't pass it. const effectiveAuth = authStateManager || this.defaultAuthStateManager; - if (effectiveAuth) { - hasValidAuth = await effectiveAuth.hasValidAuth(workingDir, authMethod); - console.log( - '[QwenAgentManager] Has valid cached auth for new session:', - hasValidAuth, - ); - } - // Only authenticate if we don't have valid cached auth - if (!hasValidAuth) { - console.log( - '[QwenAgentManager] Authenticating before creating session...', - ); + this.sessionCreateInFlight = (async () => { try { - await this.connection.authenticate(authMethod); - console.log('[QwenAgentManager] Authentication successful'); - - // Save auth state - if (effectiveAuth) { - console.log( - '[QwenAgentManager] Saving auth state after successful authentication', - ); - await effectiveAuth.saveAuthState(workingDir, authMethod); - } - } catch (authError) { - console.error('[QwenAgentManager] Authentication failed:', authError); - // Clear potentially invalid cache - if (effectiveAuth) { - console.log( - '[QwenAgentManager] Clearing auth cache due to authentication failure', - ); - await effectiveAuth.clearAuthState(); - } - throw authError; - } - } else { - console.log( - '[QwenAgentManager] Skipping authentication - using valid cached auth', - ); - } - - // Try to create a new ACP session. If Qwen asks for auth despite our - // cached flag (e.g. fresh process or expired tokens), re-authenticate and retry. - try { - await this.connection.newSession(workingDir); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - const requiresAuth = - msg.includes('Authentication required') || - msg.includes('(code: -32000)'); - - if (requiresAuth) { - console.warn( - '[QwenAgentManager] session/new requires authentication. Retrying with authenticate...', - ); + // Try to create a new ACP session. If Qwen asks for auth despite our + // cached flag (e.g. fresh process or expired tokens), re-authenticate and retry. try { - await this.connection.authenticate(authMethod); - // Persist auth cache so subsequent calls can skip the web flow. - if (effectiveAuth) { - await effectiveAuth.saveAuthState(workingDir, authMethod); - } await this.connection.newSession(workingDir); - } catch (reauthErr) { - // Clear potentially stale cache on failure and rethrow - if (effectiveAuth) { - await effectiveAuth.clearAuthState(); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + const requiresAuth = + msg.includes('Authentication required') || + msg.includes('(code: -32000)'); + + if (requiresAuth) { + console.warn( + '[QwenAgentManager] session/new requires authentication. Retrying with authenticate...', + ); + try { + await this.connection.authenticate(authMethod); + // Persist auth cache so subsequent calls can skip the web flow. + if (effectiveAuth) { + await effectiveAuth.saveAuthState(workingDir, authMethod); + } + await this.connection.newSession(workingDir); + } catch (reauthErr) { + // Clear potentially stale cache on failure and rethrow + if (effectiveAuth) { + await effectiveAuth.clearAuthState(); + } + throw reauthErr; + } + } else { + throw err; } - throw reauthErr; } - } else { - throw err; + const newSessionId = this.connection.currentSessionId; + this.consoleLog( + '[QwenAgentManager] New session created with ID:', + newSessionId, + ); + return newSessionId; + } finally { + this.sessionCreateInFlight = null; } - } - const newSessionId = this.connection.currentSessionId; - console.log( - '[QwenAgentManager] New session created with ID:', - newSessionId, - ); - return newSessionId; + })(); + + return this.sessionCreateInFlight; } /** @@ -1285,7 +1276,7 @@ export class QwenAgentManager { * Cancel current prompt */ async cancelCurrentPrompt(): Promise { - console.log('[QwenAgentManager] Cancelling current prompt'); + this.consoleLog('[QwenAgentManager] Cancelling current prompt'); await this.connection.cancelSession(); } diff --git a/packages/vscode-ide-companion/src/utils/logger.ts b/packages/vscode-ide-companion/src/utils/logger.ts index b3f8ad1e..f92cfe8c 100644 --- a/packages/vscode-ide-companion/src/utils/logger.ts +++ b/packages/vscode-ide-companion/src/utils/logger.ts @@ -6,6 +6,11 @@ import * as vscode from 'vscode'; +type ConsoleLogger = (...args: unknown[]) => void; + +// Shared console logger instance, initialized during extension activation. +let sharedConsoleLogger: ConsoleLogger = () => {}; + export function createLogger( context: vscode.ExtensionContext, logger: vscode.OutputChannel, @@ -16,3 +21,40 @@ export function createLogger( } }; } + +/** + * Creates a dev-only logger that writes to the VS Code console (Developer Tools). + */ +export function createConsoleLogger( + context: vscode.ExtensionContext, + scope?: string, +): ConsoleLogger { + return (...args: unknown[]) => { + if (context.extensionMode !== vscode.ExtensionMode.Development) { + return; + } + if (scope) { + console.log(`[${scope}]`, ...args); + return; + } + console.log(...args); + }; +} + +/** + * Initialize the shared console logger so other modules can import it without + * threading the extension context everywhere. + */ +export function initSharedConsoleLogger( + context: vscode.ExtensionContext, + scope?: string, +) { + sharedConsoleLogger = createConsoleLogger(context, scope); +} + +/** + * Get the shared console logger (no-op until initialized). + */ +export function getConsoleLogger(): ConsoleLogger { + return sharedConsoleLogger; +} diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index 4b51d6b6..65d978fd 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -45,9 +45,11 @@ import { FileIcon, UserIcon } from './components/icons/index.js'; import { ApprovalMode, NEXT_APPROVAL_MODE } from '../types/acpTypes.js'; import type { ApprovalModeValue } from '../types/acpTypes.js'; import type { PlanEntry } from '../types/chatTypes.js'; +import { createWebviewConsoleLogger } from './utils/logger.js'; export const App: React.FC = () => { const vscode = useVSCode(); + const consoleLog = useMemo(() => createWebviewConsoleLogger('App'), []); // Core hooks const sessionManagement = useSessionManagement(vscode); @@ -167,7 +169,7 @@ export const App: React.FC = () => { }, [fileContext.workspaceFiles, completion.isOpen, completion.triggerChar]); // Message submission - const handleSubmit = useMessageSubmit({ + const { handleSubmit: submitMessage } = useMessageSubmit({ inputText, setInputText, messageHandling, @@ -487,6 +489,22 @@ export const App: React.FC = () => { setThinkingEnabled((prev) => !prev); }; + // When user sends a message after scrolling up, re-pin and jump to the bottom + const handleSubmitWithScroll = useCallback( + (e: React.FormEvent) => { + setPinnedToBottom(true); + + const container = messagesContainerRef.current; + if (container) { + const top = container.scrollHeight - container.clientHeight; + container.scrollTo({ top }); + } + + submitMessage(e); + }, + [submitMessage], + ); + // Create unified message array containing all types of messages and tool calls const allMessages = useMemo< Array<{ @@ -524,7 +542,7 @@ export const App: React.FC = () => { ); }, [messageHandling.messages, inProgressToolCalls, completedToolCalls]); - console.log('[App] Rendering messages:', allMessages); + consoleLog('[App] Rendering messages:', allMessages); // Render all messages and tool calls const renderMessages = useCallback<() => React.ReactNode>( @@ -686,7 +704,7 @@ export const App: React.FC = () => { onCompositionStart={() => setIsComposing(true)} onCompositionEnd={() => setIsComposing(false)} onKeyDown={() => {}} - onSubmit={handleSubmit.handleSubmit} + onSubmit={handleSubmitWithScroll} onCancel={handleCancel} onToggleEditMode={handleToggleEditMode} onToggleThinking={handleToggleThinking} diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 82629787..11054259 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -16,6 +16,7 @@ import { WebViewContent } from '../webview/WebViewContent.js'; import { CliInstaller } from '../cli/cliInstaller.js'; import { getFileName } from './utils/webviewUtils.js'; import { authMethod, type ApprovalModeValue } from '../types/acpTypes.js'; +import { createConsoleLogger } from '../utils/logger.js'; export class WebViewProvider { private panelManager: PanelManager; @@ -32,12 +33,15 @@ export class WebViewProvider { private pendingPermissionResolve: ((optionId: string) => void) | null = null; // Track current ACP mode id to influence permission/diff behavior private currentModeId: ApprovalModeValue | null = null; + private consoleLog: (...args: unknown[]) => void; constructor( context: vscode.ExtensionContext, private extensionUri: vscode.Uri, ) { - this.agentManager = new QwenAgentManager(); + const agentConsoleLogger = createConsoleLogger(context, 'QwenAgentManager'); + this.consoleLog = createConsoleLogger(context, 'WebViewProvider'); + this.agentManager = new QwenAgentManager(agentConsoleLogger); this.conversationStore = new ConversationStore(context); this.authStateManager = AuthStateManager.getInstance(context); this.panelManager = new PanelManager(extensionUri, () => { @@ -380,7 +384,7 @@ export class WebViewProvider { // Set up state serialization newPanel.onDidChangeViewState(() => { - console.log( + this.consoleLog( '[WebViewProvider] Panel view state changed, triggering serialization check', ); }); @@ -510,7 +514,7 @@ export class WebViewProvider { } // Attempt to restore authentication state and initialize connection - console.log( + this.consoleLog( '[WebViewProvider] Attempting to restore auth state and connection...', ); await this.attemptAuthStateRestoration(); @@ -532,23 +536,26 @@ export class WebViewProvider { workingDir, authMethod, ); - console.log('[WebViewProvider] Has valid cached auth:', hasValidAuth); + this.consoleLog( + '[WebViewProvider] Has valid cached auth:', + hasValidAuth, + ); if (hasValidAuth) { - console.log( + this.consoleLog( '[WebViewProvider] Valid auth found, attempting connection...', ); // Try to connect with cached auth await this.initializeAgentConnection(); } else { - console.log( + this.consoleLog( '[WebViewProvider] No valid auth found, rendering empty conversation', ); // Render the chat UI immediately without connecting await this.initializeEmptyConversation(); } } else { - console.log( + this.consoleLog( '[WebViewProvider] No auth state manager, rendering empty conversation', ); await this.initializeEmptyConversation(); @@ -565,84 +572,101 @@ export class WebViewProvider { * Can be called from show() or via /login command */ async initializeAgentConnection(): Promise { - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); - - console.log( - '[WebViewProvider] Starting initialization, workingDir:', - workingDir, - ); - console.log( - '[WebViewProvider] AuthStateManager available:', - !!this.authStateManager, + return AuthStateManager.runExclusiveAuth(() => + this.doInitializeAgentConnection(), ); + } - // Check if CLI is installed before attempting to connect - const cliDetection = await CliDetector.detectQwenCli(); + /** + * Internal: perform actual connection/initialization (no auth locking). + */ + private async doInitializeAgentConnection(): Promise { + const run = async () => { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); - if (!cliDetection.isInstalled) { - console.log( - '[WebViewProvider] Qwen CLI not detected, skipping agent connection', + this.consoleLog( + '[WebViewProvider] Starting initialization, workingDir:', + workingDir, ); - console.log('[WebViewProvider] CLI detection error:', cliDetection.error); - - // Show VSCode notification with installation option - await CliInstaller.promptInstallation(); - - // Initialize empty conversation (can still browse history) - await this.initializeEmptyConversation(); - } else { - console.log( - '[WebViewProvider] Qwen CLI detected, attempting connection...', + this.consoleLog( + '[WebViewProvider] AuthStateManager available:', + !!this.authStateManager, ); - console.log('[WebViewProvider] CLI path:', cliDetection.cliPath); - console.log('[WebViewProvider] CLI version:', cliDetection.version); - try { - console.log('[WebViewProvider] Connecting to agent...'); - console.log( - '[WebViewProvider] Using authStateManager:', - !!this.authStateManager, + // Check if CLI is installed before attempting to connect + const cliDetection = await CliDetector.detectQwenCli(); + + if (!cliDetection.isInstalled) { + this.consoleLog( + '[WebViewProvider] Qwen CLI not detected, skipping agent connection', ); - const authInfo = await this.authStateManager.getAuthInfo(); - console.log('[WebViewProvider] Auth cache status:', authInfo); - - // Pass the detected CLI path to ensure we use the correct installation - await this.agentManager.connect( - workingDir, - this.authStateManager, - cliDetection.cliPath, + this.consoleLog( + '[WebViewProvider] CLI detection error:', + cliDetection.error, ); - console.log('[WebViewProvider] Agent connected successfully'); - this.agentInitialized = true; - // Load messages from the current Qwen session - await this.loadCurrentSessionMessages(); + // Show VSCode notification with installation option + await CliInstaller.promptInstallation(); - // Notify webview that agent is connected - this.sendMessageToWebView({ - type: 'agentConnected', - data: {}, - }); - } catch (_error) { - console.error('[WebViewProvider] Agent connection error:', _error); - // Clear auth cache on error (might be auth issue) - await this.authStateManager.clearAuthState(); - vscode.window.showWarningMessage( - `Failed to connect to Qwen CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`, - ); - // Fallback to empty conversation + // Initialize empty conversation (can still browse history) await this.initializeEmptyConversation(); + } else { + this.consoleLog( + '[WebViewProvider] Qwen CLI detected, attempting connection...', + ); + this.consoleLog('[WebViewProvider] CLI path:', cliDetection.cliPath); + this.consoleLog('[WebViewProvider] CLI version:', cliDetection.version); - // Notify webview that agent connection failed - this.sendMessageToWebView({ - type: 'agentConnectionError', - data: { - message: _error instanceof Error ? _error.message : String(_error), - }, - }); + try { + this.consoleLog('[WebViewProvider] Connecting to agent...'); + this.consoleLog( + '[WebViewProvider] Using authStateManager:', + !!this.authStateManager, + ); + const authInfo = await this.authStateManager.getAuthInfo(); + this.consoleLog('[WebViewProvider] Auth cache status:', authInfo); + + // Pass the detected CLI path to ensure we use the correct installation + await this.agentManager.connect( + workingDir, + this.authStateManager, + cliDetection.cliPath, + ); + this.consoleLog('[WebViewProvider] Agent connected successfully'); + this.agentInitialized = true; + + // Load messages from the current Qwen session + await this.loadCurrentSessionMessages(); + + // Notify webview that agent is connected + this.sendMessageToWebView({ + type: 'agentConnected', + data: {}, + }); + } catch (_error) { + console.error('[WebViewProvider] Agent connection error:', _error); + // Clear auth cache on error (might be auth issue) + await this.authStateManager.clearAuthState(); + vscode.window.showWarningMessage( + `Failed to connect to Qwen CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`, + ); + // Fallback to empty conversation + await this.initializeEmptyConversation(); + + // Notify webview that agent connection failed + this.sendMessageToWebView({ + type: 'agentConnectionError', + data: { + message: + _error instanceof Error ? _error.message : String(_error), + }, + }); + } } - } + }; + + return run(); } /** @@ -650,86 +674,97 @@ export class WebViewProvider { * Called when user explicitly uses /login command */ async forceReLogin(): Promise { - console.log('[WebViewProvider] Force re-login requested'); - console.log( + this.consoleLog('[WebViewProvider] Force re-login requested'); + this.consoleLog( '[WebViewProvider] Current authStateManager:', !!this.authStateManager, ); - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: 'Logging in to Qwen Code... ', - cancellable: false, - }, - async (progress) => { - try { - progress.report({ message: 'Preparing sign-in...' }); + // If a login/connection flow is already running, reuse it to avoid double prompts + const p = Promise.resolve( + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + cancellable: false, + }, + async (progress) => { + try { + progress.report({ message: 'Preparing sign-in...' }); - // Clear existing auth cache - if (this.authStateManager) { - await this.authStateManager.clearAuthState(); - console.log('[WebViewProvider] Auth cache cleared'); - } else { - console.log('[WebViewProvider] No authStateManager to clear'); - } - - // Disconnect existing connection if any - if (this.agentInitialized) { - try { - this.agentManager.disconnect(); - console.log('[WebViewProvider] Existing connection disconnected'); - } catch (_error) { - console.log('[WebViewProvider] Error disconnecting:', _error); + // Clear existing auth cache + if (this.authStateManager) { + await this.authStateManager.clearAuthState(); + this.consoleLog('[WebViewProvider] Auth cache cleared'); + } else { + this.consoleLog('[WebViewProvider] No authStateManager to clear'); } - this.agentInitialized = false; + + // Disconnect existing connection if any + if (this.agentInitialized) { + try { + this.agentManager.disconnect(); + this.consoleLog( + '[WebViewProvider] Existing connection disconnected', + ); + } catch (_error) { + this.consoleLog( + '[WebViewProvider] Error disconnecting:', + _error, + ); + } + this.agentInitialized = false; + } + + // Wait a moment for cleanup to complete + await new Promise((resolve) => setTimeout(resolve, 300)); + + progress.report({ + message: 'Connecting to CLI and starting sign-in...', + }); + + // Reinitialize connection (will trigger fresh authentication) + await this.doInitializeAgentConnection(); + this.consoleLog( + '[WebViewProvider] Force re-login completed successfully', + ); + + // Ensure auth state is saved after successful re-login + if (this.authStateManager) { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); + await this.authStateManager.saveAuthState(workingDir, authMethod); + this.consoleLog( + '[WebViewProvider] Auth state saved after re-login', + ); + } + + // Send success notification to WebView + this.sendMessageToWebView({ + type: 'loginSuccess', + data: { message: 'Successfully logged in!' }, + }); + } catch (_error) { + console.error('[WebViewProvider] Force re-login failed:', _error); + console.error( + '[WebViewProvider] Error stack:', + _error instanceof Error ? _error.stack : 'N/A', + ); + + // Send error notification to WebView + this.sendMessageToWebView({ + type: 'loginError', + data: { + message: `Login failed: ${_error instanceof Error ? _error.message : String(_error)}`, + }, + }); + + throw _error; } - - // Wait a moment for cleanup to complete - await new Promise((resolve) => setTimeout(resolve, 300)); - - progress.report({ - message: 'Connecting to CLI and starting sign-in...', - }); - - // Reinitialize connection (will trigger fresh authentication) - await this.initializeAgentConnection(); - console.log( - '[WebViewProvider] Force re-login completed successfully', - ); - - // Ensure auth state is saved after successful re-login - if (this.authStateManager) { - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); - await this.authStateManager.saveAuthState(workingDir, authMethod); - console.log('[WebViewProvider] Auth state saved after re-login'); - } - - // Send success notification to WebView - this.sendMessageToWebView({ - type: 'loginSuccess', - data: { message: 'Successfully logged in!' }, - }); - } catch (_error) { - console.error('[WebViewProvider] Force re-login failed:', _error); - console.error( - '[WebViewProvider] Error stack:', - _error instanceof Error ? _error.stack : 'N/A', - ); - - // Send error notification to WebView - this.sendMessageToWebView({ - type: 'loginError', - data: { - message: `Login failed: ${_error instanceof Error ? _error.message : String(_error)}`, - }, - }); - - throw _error; - } - }, + }, + ), ); + + return AuthStateManager.runExclusiveAuth(() => p); } /** @@ -737,15 +772,15 @@ export class WebViewProvider { * Called when restoring WebView after VSCode restart */ async refreshConnection(): Promise { - console.log('[WebViewProvider] Refresh connection requested'); + this.consoleLog('[WebViewProvider] Refresh connection requested'); // Disconnect existing connection if any if (this.agentInitialized) { try { this.agentManager.disconnect(); - console.log('[WebViewProvider] Existing connection disconnected'); + this.consoleLog('[WebViewProvider] Existing connection disconnected'); } catch (_error) { - console.log('[WebViewProvider] Error disconnecting:', _error); + this.consoleLog('[WebViewProvider] Error disconnecting:', _error); } this.agentInitialized = false; } @@ -756,7 +791,7 @@ export class WebViewProvider { // Reinitialize connection (will use cached auth if available) try { await this.initializeAgentConnection(); - console.log( + this.consoleLog( '[WebViewProvider] Connection refresh completed successfully', ); @@ -786,35 +821,41 @@ export class WebViewProvider { */ private async loadCurrentSessionMessages(): Promise { try { - console.log( + this.consoleLog( '[WebViewProvider] Initializing with new session (skipping restoration)', ); const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); - // Skip session restoration entirely and create a new session directly - try { - await this.agentManager.createNewSession( - workingDir, - this.authStateManager, - ); - console.log('[WebViewProvider] ACP session created successfully'); + // avoid creating another session if connect() already created one. + if (!this.agentManager.currentSessionId) { + try { + await this.agentManager.createNewSession( + workingDir, + this.authStateManager, + ); + this.consoleLog('[WebViewProvider] ACP session created successfully'); - // Ensure auth state is saved after successful session creation - if (this.authStateManager) { - await this.authStateManager.saveAuthState(workingDir, authMethod); - console.log( - '[WebViewProvider] Auth state saved after session creation', + // Ensure auth state is saved after successful session creation + if (this.authStateManager) { + await this.authStateManager.saveAuthState(workingDir, authMethod); + this.consoleLog( + '[WebViewProvider] Auth state saved after session creation', + ); + } + } catch (sessionError) { + console.error( + '[WebViewProvider] Failed to create ACP session:', + sessionError, + ); + vscode.window.showWarningMessage( + `Failed to create ACP session: ${sessionError}. You may need to authenticate first.`, ); } - } catch (sessionError) { - console.error( - '[WebViewProvider] Failed to create ACP session:', - sessionError, - ); - vscode.window.showWarningMessage( - `Failed to create ACP session: ${sessionError}. You may need to authenticate first.`, + } else { + this.consoleLog( + '[WebViewProvider] Existing ACP session detected, skipping new session creation', ); } @@ -837,14 +878,14 @@ export class WebViewProvider { */ private async initializeEmptyConversation(): Promise { try { - console.log('[WebViewProvider] Initializing empty conversation'); + this.consoleLog('[WebViewProvider] Initializing empty conversation'); const newConv = await this.conversationStore.createConversation(); this.messageHandler.setCurrentConversationId(newConv.id); this.sendMessageToWebView({ type: 'conversationLoaded', data: newConv, }); - console.log( + this.consoleLog( '[WebViewProvider] Empty conversation initialized:', this.messageHandler.getCurrentConversationId(), ); @@ -968,7 +1009,7 @@ export class WebViewProvider { * Call this when auth cache is cleared to force re-authentication */ resetAgentState(): void { - console.log('[WebViewProvider] Resetting agent state'); + this.consoleLog('[WebViewProvider] Resetting agent state'); this.agentInitialized = false; // Disconnect existing connection this.agentManager.disconnect(); @@ -978,7 +1019,7 @@ export class WebViewProvider { * Clear authentication cache for this WebViewProvider instance */ async clearAuthCache(): Promise { - console.log('[WebViewProvider] Clearing auth cache for this instance'); + this.consoleLog('[WebViewProvider] Clearing auth cache for this instance'); if (this.authStateManager) { await this.authStateManager.clearAuthState(); this.resetAgentState(); @@ -990,8 +1031,8 @@ export class WebViewProvider { * This sets up the panel with all event listeners */ async restorePanel(panel: vscode.WebviewPanel): Promise { - console.log('[WebViewProvider] Restoring WebView panel'); - console.log( + this.consoleLog('[WebViewProvider] Restoring WebView panel'); + this.consoleLog( '[WebViewProvider] Current authStateManager in restore:', !!this.authStateManager, ); @@ -1122,10 +1163,10 @@ export class WebViewProvider { // Capture the tab reference on restore this.panelManager.captureTab(); - console.log('[WebViewProvider] Panel restored successfully'); + this.consoleLog('[WebViewProvider] Panel restored successfully'); // Attempt to restore authentication state and initialize connection - console.log( + this.consoleLog( '[WebViewProvider] Attempting to restore auth state and connection after restore...', ); await this.attemptAuthStateRestoration(); @@ -1139,12 +1180,12 @@ export class WebViewProvider { conversationId: string | null; agentInitialized: boolean; } { - console.log('[WebViewProvider] Getting state for serialization'); - console.log( + this.consoleLog('[WebViewProvider] Getting state for serialization'); + this.consoleLog( '[WebViewProvider] Current conversationId:', this.messageHandler.getCurrentConversationId(), ); - console.log( + this.consoleLog( '[WebViewProvider] Current agentInitialized:', this.agentInitialized, ); @@ -1152,7 +1193,7 @@ export class WebViewProvider { conversationId: this.messageHandler.getCurrentConversationId(), agentInitialized: this.agentInitialized, }; - console.log('[WebViewProvider] Returning state:', state); + this.consoleLog('[WebViewProvider] Returning state:', state); return state; } @@ -1170,10 +1211,10 @@ export class WebViewProvider { conversationId: string | null; agentInitialized: boolean; }): void { - console.log('[WebViewProvider] Restoring state:', state); + this.consoleLog('[WebViewProvider] Restoring state:', state); this.messageHandler.setCurrentConversationId(state.conversationId); this.agentInitialized = state.agentInitialized; - console.log( + this.consoleLog( '[WebViewProvider] State restored. agentInitialized:', this.agentInitialized, ); @@ -1206,8 +1247,6 @@ export class WebViewProvider { type: 'conversationCleared', data: {}, }); - - console.log('[WebViewProvider] New session created successfully'); } catch (_error) { console.error('[WebViewProvider] Failed to create new session:', _error); vscode.window.showErrorMessage(`Failed to create new session: ${_error}`); diff --git a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts index 741d9684..a46febcd 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts @@ -291,6 +291,41 @@ export class SessionMessageHandler extends BaseMessageHandler { return; } + // Ensure an ACP session exists before sending prompt + if (!this.agentManager.currentSessionId) { + try { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); + await this.agentManager.createNewSession(workingDir); + } catch (createErr) { + console.error( + '[SessionMessageHandler] Failed to create session before sending message:', + createErr, + ); + const errorMsg = + createErr instanceof Error ? createErr.message : String(createErr); + if ( + errorMsg.includes('Authentication required') || + errorMsg.includes('(code: -32000)') + ) { + const result = await vscode.window.showWarningMessage( + '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; + } + vscode.window.showErrorMessage(`Failed to create session: ${errorMsg}`); + return; + } + } + // Send to agent try { this.resetStreamContent(); diff --git a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts index cd312361..a00fa0e5 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -14,6 +14,7 @@ import type { import type { ToolCallUpdate } from '../../types/chatTypes.js'; import type { ApprovalModeValue } from '../../types/acpTypes.js'; import type { PlanEntry } from '../../types/chatTypes.js'; +import { createWebviewConsoleLogger } from '../utils/logger.js'; interface UseWebViewMessagesProps { // Session management @@ -129,6 +130,7 @@ export const useWebViewMessages = ({ }: UseWebViewMessagesProps) => { // VS Code API for posting messages back to the extension host const vscode = useVSCode(); + const consoleLog = useRef(createWebviewConsoleLogger('WebViewMessages')); // Track active long-running tool calls (execute/bash/command) so we can // keep the bottom "waiting" message visible until all of them complete. const activeExecToolCallsRef = useRef>(new Set()); @@ -227,40 +229,26 @@ export const useWebViewMessages = ({ break; } - // case 'cliNotInstalled': { - // // Show CLI not installed message - // const errorMsg = - // (message?.data?.error as string) || - // 'Qwen Code CLI is not installed. Please install it to enable full functionality.'; + case 'agentConnected': { + // Agent connected successfully; clear any pending spinner + handlers.messageHandling.clearWaitingForResponse(); + break; + } - // handlers.messageHandling.addMessage({ - // role: 'assistant', - // content: `Qwen CLI is not installed. Please install it to enable full functionality.\n\nError: ${errorMsg}\n\nInstallation instructions:\n1. Install via npm:\n npm install -g @qwen-code/qwen-code@latest\n\n2. After installation, reload VS Code or restart the extension.`, - // timestamp: Date.now(), - // }); - // break; - // } + case 'agentConnectionError': { + // Agent connection failed; surface the error and unblock the UI + handlers.messageHandling.clearWaitingForResponse(); + const errorMsg = + (message?.data?.message as string) || + 'Failed to connect to Qwen agent.'; - // case 'agentConnected': { - // // Agent connected successfully - // handlers.messageHandling.clearWaitingForResponse(); - // break; - // } - - // case 'agentConnectionError': { - // // Agent connection failed - // handlers.messageHandling.clearWaitingForResponse(); - // const errorMsg = - // (message?.data?.message as string) || - // 'Failed to connect to Qwen agent.'; - - // handlers.messageHandling.addMessage({ - // role: 'assistant', - // content: `Failed to connect to Qwen agent: ${errorMsg}\nYou can still use the chat UI, but messages won't be sent to AI.`, - // timestamp: Date.now(), - // }); - // break; - // } + handlers.messageHandling.addMessage({ + role: 'assistant', + content: `Failed to connect to Qwen agent: ${errorMsg}\nYou can still use the chat UI, but messages won't be sent to AI.`, + timestamp: Date.now(), + }); + break; + } case 'loginError': { // Clear loading state and show error notice @@ -765,7 +753,10 @@ export const useWebViewMessages = ({ path: string; }>; if (files) { - console.log('[WebView] Received workspaceFiles:', files.length); + consoleLog.current( + '[WebView] Received workspaceFiles:', + files.length, + ); handlers.fileContext.setWorkspaceFiles(files); } break; diff --git a/packages/vscode-ide-companion/src/webview/styles/styles.css b/packages/vscode-ide-companion/src/webview/styles/styles.css index 4c3db053..956912cb 100644 --- a/packages/vscode-ide-companion/src/webview/styles/styles.css +++ b/packages/vscode-ide-companion/src/webview/styles/styles.css @@ -5,7 +5,6 @@ */ /* Import component styles */ -@import '../components/messages/Assistant/AssistantMessage.css'; @import './timeline.css'; @import '../components/messages/MarkdownRenderer/MarkdownRenderer.css'; diff --git a/packages/vscode-ide-companion/src/webview/utils/logger.ts b/packages/vscode-ide-companion/src/webview/utils/logger.ts new file mode 100644 index 00000000..40d1793e --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/utils/logger.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Creates a dev-only console logger for the WebView bundle. + * In production builds it becomes a no-op to avoid noisy logs. + */ +export function createWebviewConsoleLogger(scope?: string) { + return (...args: unknown[]) => { + const env = (globalThis as { process?: { env?: Record } }) + .process?.env; + const isProduction = env?.NODE_ENV === 'production'; + if (isProduction) { + return; + } + if (scope) { + console.log(`[${scope}]`, ...args); + return; + } + console.log(...args); + }; +} diff --git a/packages/vscode-ide-companion/src/webview/utils/simpleDiff.ts b/packages/vscode-ide-companion/src/webview/utils/simpleDiff.ts deleted file mode 100644 index 0231f383..00000000 --- a/packages/vscode-ide-companion/src/webview/utils/simpleDiff.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - * - * Minimal line-diff utility for webview previews. - * - * This is a lightweight LCS-based algorithm to compute add/remove operations - * between two texts. It intentionally avoids heavy dependencies and is - * sufficient for rendering a compact preview inside the chat. - */ - -export type DiffOp = - | { type: 'add'; line: string; newIndex: number } - | { type: 'remove'; line: string; oldIndex: number }; - -/** - * Compute a minimal line-diff (added/removed only). - * - Equal lines are omitted from output by design (we only preview changes). - * - Order of operations follows the new text progression so the preview feels natural. - */ -export function computeLineDiff( - oldText: string | null | undefined, - newText: string | undefined, -): DiffOp[] { - const a = (oldText || '').split('\n'); - const b = (newText || '').split('\n'); - - const n = a.length; - const m = b.length; - - // Build LCS DP table - const dp: number[][] = Array.from({ length: n + 1 }, () => - new Array(m + 1).fill(0), - ); - for (let i = n - 1; i >= 0; i--) { - for (let j = m - 1; j >= 0; j--) { - if (a[i] === b[j]) { - dp[i][j] = dp[i + 1][j + 1] + 1; - } else { - dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]); - } - } - } - - // Walk to produce operations - const ops: DiffOp[] = []; - let i = 0; - let j = 0; - while (i < n && j < m) { - if (a[i] === b[j]) { - i++; - j++; - } else if (dp[i + 1][j] >= dp[i][j + 1]) { - // remove a[i] - ops.push({ type: 'remove', line: a[i], oldIndex: i }); - i++; - } else { - // add b[j] - ops.push({ type: 'add', line: b[j], newIndex: j }); - j++; - } - } - - // Remaining tails - while (i < n) { - ops.push({ type: 'remove', line: a[i], oldIndex: i }); - i++; - } - while (j < m) { - ops.push({ type: 'add', line: b[j], newIndex: j }); - j++; - } - - return ops; -} - -/** - * Truncate a long list of operations for preview purposes. - * Keeps first `head` and last `tail` operations, inserting a gap marker. - */ -export function truncateOps( - ops: T[], - head = 120, - tail = 80, -): { items: T[]; truncated: boolean; omitted: number } { - if (ops.length <= head + tail) { - return { items: ops, truncated: false, omitted: 0 }; - } - const items = [...ops.slice(0, head), ...ops.slice(-tail)]; - return { items, truncated: true, omitted: ops.length - head - tail }; -}