Merge pull request #1223 from QwenLM/fix/vscode-ide-companion-login-twice

fix(vscode-ide-companion/auth): deduplicate concurrent authentication calls
This commit is contained in:
tanzhenxin
2025-12-12 16:19:25 +08:00
committed by GitHub
10 changed files with 278 additions and 861 deletions

View File

@@ -1,215 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type * as vscode from 'vscode';
interface AuthState {
isAuthenticated: boolean;
authMethod: string;
timestamp: number;
workingDir?: string;
}
/**
* Manages authentication state caching to avoid repeated logins
*/
export class AuthStateManager {
private static instance: AuthStateManager | null = null;
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 constructor() {}
/**
* Get singleton instance of AuthStateManager
*/
static getInstance(context?: vscode.ExtensionContext): AuthStateManager {
if (!AuthStateManager.instance) {
AuthStateManager.instance = new AuthStateManager();
}
// If a context is provided, update the static context
if (context) {
AuthStateManager.context = context;
}
return AuthStateManager.instance;
}
/**
* Check if there's a valid cached authentication
*/
async hasValidAuth(workingDir: string, authMethod: string): Promise<boolean> {
const state = await this.getAuthState();
if (!state) {
console.log('[AuthStateManager] No cached auth state found');
return false;
}
console.log('[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:', {
workingDir,
authMethod,
});
// Check if auth is still valid (within cache duration)
const now = Date.now();
const isExpired =
now - state.timestamp > AuthStateManager.AUTH_CACHE_DURATION;
if (isExpired) {
console.log('[AuthStateManager] Cached auth expired');
console.log(
'[AuthStateManager] Cache age:',
Math.floor((now - state.timestamp) / 1000 / 60),
'minutes',
);
await this.clearAuthState();
return false;
}
// Check if it's for the same working directory and auth method
const isSameContext =
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);
return false;
}
console.log('[AuthStateManager] Valid cached auth found');
return state.isAuthenticated;
}
/**
* Force check auth state without clearing cache
* This is useful for debugging to see what's actually cached
*/
async debugAuthState(): Promise<void> {
const state = await this.getAuthState();
console.log('[AuthStateManager] DEBUG - Current auth state:', state);
if (state) {
const now = Date.now();
const age = Math.floor((now - state.timestamp) / 1000 / 60);
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] DEBUG - Auth state valid:',
state.isAuthenticated,
);
}
}
/**
* Save successful authentication state
*/
async saveAuthState(workingDir: string, authMethod: string): Promise<void> {
// Ensure we have a valid context
if (!AuthStateManager.context) {
throw new Error(
'[AuthStateManager] No context available for saving auth state',
);
}
const state: AuthState = {
isAuthenticated: true,
authMethod,
workingDir,
timestamp: Date.now(),
};
console.log('[AuthStateManager] Saving auth state:', {
workingDir,
authMethod,
timestamp: new Date(state.timestamp).toISOString(),
});
await AuthStateManager.context.globalState.update(
AuthStateManager.AUTH_STATE_KEY,
state,
);
console.log('[AuthStateManager] Auth state saved');
// Verify the state was saved correctly
const savedState = await this.getAuthState();
console.log('[AuthStateManager] Verified saved state:', savedState);
}
/**
* Clear authentication state
*/
async clearAuthState(): Promise<void> {
// Ensure we have a valid context
if (!AuthStateManager.context) {
throw new Error(
'[AuthStateManager] No context available for clearing auth state',
);
}
console.log('[AuthStateManager] Clearing auth state');
const currentState = await this.getAuthState();
console.log(
'[AuthStateManager] Current state before clearing:',
currentState,
);
await AuthStateManager.context.globalState.update(
AuthStateManager.AUTH_STATE_KEY,
undefined,
);
console.log('[AuthStateManager] Auth state cleared');
// Verify the state was cleared
const newState = await this.getAuthState();
console.log('[AuthStateManager] State after clearing:', newState);
}
/**
* Get current auth state
*/
private async getAuthState(): Promise<AuthState | undefined> {
// Ensure we have a valid context
if (!AuthStateManager.context) {
console.log(
'[AuthStateManager] No context available for getting auth state',
);
return undefined;
}
const a = AuthStateManager.context.globalState.get<AuthState>(
AuthStateManager.AUTH_STATE_KEY,
);
console.log('[AuthStateManager] Auth state:', a);
return a;
}
/**
* Get auth state info for debugging
*/
async getAuthInfo(): Promise<string> {
const state = await this.getAuthState();
if (!state) {
return 'No cached auth';
}
const age = Math.floor((Date.now() - state.timestamp) / 1000 / 60);
return `Auth cached ${age}m ago, method: ${state.authMethod}`;
}
}

View File

@@ -11,7 +11,6 @@ import type {
} from '../types/acpTypes.js'; } from '../types/acpTypes.js';
import { QwenSessionReader, type QwenSession } from './qwenSessionReader.js'; import { QwenSessionReader, type QwenSession } from './qwenSessionReader.js';
import { QwenSessionManager } from './qwenSessionManager.js'; import { QwenSessionManager } from './qwenSessionManager.js';
import type { AuthStateManager } from './authStateManager.js';
import type { import type {
ChatMessage, ChatMessage,
PlanEntry, PlanEntry,
@@ -42,9 +41,9 @@ export class QwenAgentManager {
// session/update notifications. We set this flag to route message chunks // session/update notifications. We set this flag to route message chunks
// (user/assistant) as discrete chat messages instead of live streaming. // (user/assistant) as discrete chat messages instead of live streaming.
private rehydratingSessionId: string | null = null; private rehydratingSessionId: string | null = null;
// Cache the last used AuthStateManager so internal calls (e.g. fallback paths) // CLI is now the single source of truth for authentication state
// can reuse it and avoid forcing a fresh authentication unnecessarily. // Deduplicate concurrent session/new attempts
private defaultAuthStateManager?: AuthStateManager; private sessionCreateInFlight: Promise<string | null> | null = null;
// Callback storage // Callback storage
private callbacks: QwenAgentCallbacks = {}; private callbacks: QwenAgentCallbacks = {};
@@ -163,22 +162,14 @@ export class QwenAgentManager {
* Connect to Qwen service * Connect to Qwen service
* *
* @param workingDir - Working directory * @param workingDir - Working directory
* @param authStateManager - Authentication state manager (optional)
* @param cliPath - CLI path (optional, if provided will override the path in configuration) * @param cliPath - CLI path (optional, if provided will override the path in configuration)
*/ */
async connect( async connect(workingDir: string, _cliPath?: string): Promise<void> {
workingDir: string,
authStateManager?: AuthStateManager,
_cliPath?: string,
): Promise<void> {
this.currentWorkingDir = workingDir; this.currentWorkingDir = workingDir;
// Remember the provided authStateManager for future calls
this.defaultAuthStateManager = authStateManager;
await this.connectionHandler.connect( await this.connectionHandler.connect(
this.connection, this.connection,
this.sessionReader, this.sessionReader,
workingDir, workingDir,
authStateManager,
_cliPath, _cliPath,
); );
} }
@@ -1179,97 +1170,62 @@ export class QwenAgentManager {
* @param workingDir - Working directory * @param workingDir - Working directory
* @returns Newly created session ID * @returns Newly created session ID
*/ */
async createNewSession( async createNewSession(workingDir: string): Promise<string | null> {
workingDir: string, // Reuse existing session if present
authStateManager?: AuthStateManager, if (this.connection.currentSessionId) {
): Promise<string | null> { return this.connection.currentSessionId;
}
// Deduplicate concurrent session/new attempts
if (this.sessionCreateInFlight) {
return this.sessionCreateInFlight;
}
console.log('[QwenAgentManager] Creating new session...'); console.log('[QwenAgentManager] Creating new session...');
// Check if we have valid cached authentication this.sessionCreateInFlight = (async () => {
let hasValidAuth = false;
// 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...',
);
try { try {
await this.connection.authenticate(authMethod); // Try to create a new ACP session. If Qwen asks for auth, let it handle authentication.
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 { 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); await this.connection.newSession(workingDir);
} catch (reauthErr) { } catch (err) {
// Clear potentially stale cache on failure and rethrow const msg = err instanceof Error ? err.message : String(err);
if (effectiveAuth) { const requiresAuth =
await effectiveAuth.clearAuthState(); msg.includes('Authentication required') ||
msg.includes('(code: -32000)');
if (requiresAuth) {
console.warn(
'[QwenAgentManager] session/new requires authentication. Retrying with authenticate...',
);
try {
// Let CLI handle authentication - it's the single source of truth
await this.connection.authenticate(authMethod);
// Add a slight delay to ensure auth state is settled
await new Promise((resolve) => setTimeout(resolve, 300));
await this.connection.newSession(workingDir);
} catch (reauthErr) {
console.error(
'[QwenAgentManager] Re-authentication failed:',
reauthErr,
);
throw reauthErr;
}
} else {
throw err;
} }
throw reauthErr;
} }
} else { const newSessionId = this.connection.currentSessionId;
throw err; console.log(
'[QwenAgentManager] New session created with ID:',
newSessionId,
);
return newSessionId;
} finally {
this.sessionCreateInFlight = null;
} }
} })();
const newSessionId = this.connection.currentSessionId;
console.log( return this.sessionCreateInFlight;
'[QwenAgentManager] New session created with ID:',
newSessionId,
);
return newSessionId;
} }
/** /**

View File

@@ -13,7 +13,6 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import type { AcpConnection } from './acpConnection.js'; import type { AcpConnection } from './acpConnection.js';
import type { QwenSessionReader } from '../services/qwenSessionReader.js'; import type { QwenSessionReader } from '../services/qwenSessionReader.js';
import type { AuthStateManager } from '../services/authStateManager.js';
import { import {
CliVersionManager, CliVersionManager,
MIN_CLI_VERSION_FOR_SESSION_METHODS, MIN_CLI_VERSION_FOR_SESSION_METHODS,
@@ -32,14 +31,12 @@ export class QwenConnectionHandler {
* @param connection - ACP connection instance * @param connection - ACP connection instance
* @param sessionReader - Session reader instance * @param sessionReader - Session reader instance
* @param workingDir - Working directory * @param workingDir - Working directory
* @param authStateManager - Authentication state manager (optional)
* @param cliPath - CLI path (optional, if provided will override the path in configuration) * @param cliPath - CLI path (optional, if provided will override the path in configuration)
*/ */
async connect( async connect(
connection: AcpConnection, connection: AcpConnection,
sessionReader: QwenSessionReader, sessionReader: QwenSessionReader,
workingDir: string, workingDir: string,
authStateManager?: AuthStateManager,
cliPath?: string, cliPath?: string,
): Promise<void> { ): Promise<void> {
const connectId = Date.now(); const connectId = Date.now();
@@ -72,21 +69,6 @@ export class QwenConnectionHandler {
await connection.connect(effectiveCliPath, workingDir, extraArgs); await connection.connect(effectiveCliPath, workingDir, extraArgs);
// Check if we have valid cached authentication
if (authStateManager) {
console.log('[QwenAgentManager] Checking for cached authentication...');
console.log('[QwenAgentManager] Working dir:', workingDir);
console.log('[QwenAgentManager] Auth method:', authMethod);
const hasValidAuth = await authStateManager.hasValidAuth(
workingDir,
authMethod,
);
console.log('[QwenAgentManager] Has valid auth:', hasValidAuth);
} else {
console.log('[QwenAgentManager] No authStateManager provided');
}
// Try to restore existing session or create new session // Try to restore existing session or create new session
// Note: Auto-restore on connect is disabled to avoid surprising loads // Note: Auto-restore on connect is disabled to avoid surprising loads
// when user opens a "New Chat" tab. Restoration is now an explicit action // when user opens a "New Chat" tab. Restoration is now an explicit action
@@ -99,81 +81,15 @@ export class QwenConnectionHandler {
'[QwenAgentManager] no sessionRestored, Creating new session...', '[QwenAgentManager] no sessionRestored, Creating new session...',
); );
// Check if we have valid cached authentication
let hasValidAuth = false;
if (authStateManager) {
hasValidAuth = await authStateManager.hasValidAuth(
workingDir,
authMethod,
);
}
// Only authenticate if we don't have valid cached auth
if (!hasValidAuth) {
console.log(
'[QwenAgentManager] Authenticating before creating session...',
);
try {
await connection.authenticate(authMethod);
console.log('[QwenAgentManager] Authentication successful');
// Save auth state
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);
console.log('[QwenAgentManager] Auth state save completed');
}
} catch (authError) {
console.error('[QwenAgentManager] Authentication failed:', authError);
// Clear potentially invalid cache
if (authStateManager) {
console.log(
'[QwenAgentManager] Clearing auth cache due to authentication failure',
);
await authStateManager.clearAuthState();
}
throw authError;
}
} else {
console.log(
'[QwenAgentManager] Skipping authentication - using valid cached auth',
);
}
try { try {
console.log( console.log(
'[QwenAgentManager] Creating new session after authentication...', '[QwenAgentManager] Creating new session (letting CLI handle authentication)...',
);
await this.newSessionWithRetry(
connection,
workingDir,
3,
authMethod,
authStateManager,
); );
await this.newSessionWithRetry(connection, workingDir, 3, authMethod);
console.log('[QwenAgentManager] New session created successfully'); console.log('[QwenAgentManager] New session created successfully');
// Ensure auth state is saved (prevent repeated authentication)
if (authStateManager) {
console.log(
'[QwenAgentManager] Saving auth state after successful session creation',
);
await authStateManager.saveAuthState(workingDir, authMethod);
}
} catch (sessionError) { } catch (sessionError) {
console.log(`\n⚠ [SESSION FAILED] newSessionWithRetry threw error\n`); console.log(`\n⚠ [SESSION FAILED] newSessionWithRetry threw error\n`);
console.log(`[QwenAgentManager] Error details:`, sessionError); console.log(`[QwenAgentManager] Error details:`, sessionError);
// Clear cache
if (authStateManager) {
console.log('[QwenAgentManager] Clearing auth cache due to failure');
await authStateManager.clearAuthState();
}
throw sessionError; throw sessionError;
} }
} }
@@ -195,7 +111,6 @@ export class QwenConnectionHandler {
workingDir: string, workingDir: string,
maxRetries: number, maxRetries: number,
authMethod: string, authMethod: string,
authStateManager?: AuthStateManager,
): Promise<void> { ): Promise<void> {
for (let attempt = 1; attempt <= maxRetries; attempt++) { for (let attempt = 1; attempt <= maxRetries; attempt++) {
try { try {
@@ -224,9 +139,10 @@ export class QwenConnectionHandler {
); );
try { try {
await connection.authenticate(authMethod); await connection.authenticate(authMethod);
if (authStateManager) { // FIXME: @yiliang114 If there is no delay for a while, immediately executing
await authStateManager.saveAuthState(workingDir, authMethod); // newSession may cause the cli authorization jump to be triggered again
} // Add a slight delay to ensure auth state is settled
await new Promise((resolve) => setTimeout(resolve, 300));
// Retry immediately after successful auth // Retry immediately after successful auth
await connection.newSession(workingDir); await connection.newSession(workingDir);
console.log( console.log(
@@ -238,9 +154,6 @@ export class QwenConnectionHandler {
'[QwenAgentManager] Re-authentication failed:', '[QwenAgentManager] Re-authentication failed:',
authErr, authErr,
); );
if (authStateManager) {
await authStateManager.clearAuthState();
}
// Fall through to retry logic below // Fall through to retry logic below
} }
} }

View File

@@ -167,7 +167,7 @@ export const App: React.FC = () => {
}, [fileContext.workspaceFiles, completion.isOpen, completion.triggerChar]); }, [fileContext.workspaceFiles, completion.isOpen, completion.triggerChar]);
// Message submission // Message submission
const handleSubmit = useMessageSubmit({ const { handleSubmit: submitMessage } = useMessageSubmit({
inputText, inputText,
setInputText, setInputText,
messageHandling, messageHandling,
@@ -487,6 +487,22 @@ export const App: React.FC = () => {
setThinkingEnabled((prev) => !prev); 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 // Create unified message array containing all types of messages and tool calls
const allMessages = useMemo< const allMessages = useMemo<
Array<{ Array<{
@@ -686,7 +702,7 @@ export const App: React.FC = () => {
onCompositionStart={() => setIsComposing(true)} onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)} onCompositionEnd={() => setIsComposing(false)}
onKeyDown={() => {}} onKeyDown={() => {}}
onSubmit={handleSubmit.handleSubmit} onSubmit={handleSubmitWithScroll}
onCancel={handleCancel} onCancel={handleCancel}
onToggleEditMode={handleToggleEditMode} onToggleEditMode={handleToggleEditMode}
onToggleThinking={handleToggleThinking} onToggleThinking={handleToggleThinking}

View File

@@ -9,20 +9,18 @@ import { QwenAgentManager } from '../services/qwenAgentManager.js';
import { ConversationStore } from '../services/conversationStore.js'; import { ConversationStore } from '../services/conversationStore.js';
import type { AcpPermissionRequest } from '../types/acpTypes.js'; import type { AcpPermissionRequest } from '../types/acpTypes.js';
import { CliDetector } from '../cli/cliDetector.js'; import { CliDetector } from '../cli/cliDetector.js';
import { AuthStateManager } from '../services/authStateManager.js';
import { PanelManager } from '../webview/PanelManager.js'; import { PanelManager } from '../webview/PanelManager.js';
import { MessageHandler } from '../webview/MessageHandler.js'; import { MessageHandler } from '../webview/MessageHandler.js';
import { WebViewContent } from '../webview/WebViewContent.js'; import { WebViewContent } from '../webview/WebViewContent.js';
import { CliInstaller } from '../cli/cliInstaller.js'; import { CliInstaller } from '../cli/cliInstaller.js';
import { getFileName } from './utils/webviewUtils.js'; import { getFileName } from './utils/webviewUtils.js';
import { authMethod, type ApprovalModeValue } from '../types/acpTypes.js'; import { type ApprovalModeValue } from '../types/acpTypes.js';
export class WebViewProvider { export class WebViewProvider {
private panelManager: PanelManager; private panelManager: PanelManager;
private messageHandler: MessageHandler; private messageHandler: MessageHandler;
private agentManager: QwenAgentManager; private agentManager: QwenAgentManager;
private conversationStore: ConversationStore; private conversationStore: ConversationStore;
private authStateManager: AuthStateManager;
private disposables: vscode.Disposable[] = []; private disposables: vscode.Disposable[] = [];
private agentInitialized = false; // Track if agent has been initialized private agentInitialized = false; // Track if agent has been initialized
// Track a pending permission request and its resolver so extension commands // Track a pending permission request and its resolver so extension commands
@@ -39,7 +37,6 @@ export class WebViewProvider {
) { ) {
this.agentManager = new QwenAgentManager(); this.agentManager = new QwenAgentManager();
this.conversationStore = new ConversationStore(context); this.conversationStore = new ConversationStore(context);
this.authStateManager = AuthStateManager.getInstance(context);
this.panelManager = new PanelManager(extensionUri, () => { this.panelManager = new PanelManager(extensionUri, () => {
// Panel dispose callback // Panel dispose callback
this.disposables.forEach((d) => d.dispose()); this.disposables.forEach((d) => d.dispose());
@@ -522,40 +519,16 @@ export class WebViewProvider {
*/ */
private async attemptAuthStateRestoration(): Promise<void> { private async attemptAuthStateRestoration(): Promise<void> {
try { try {
if (this.authStateManager) { console.log(
// Debug current auth state '[WebViewProvider] Attempting connection (CLI handle authentication)...',
await this.authStateManager.debugAuthState(); );
//always attempt connection and let CLI handle authentication
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; await this.initializeAgentConnection();
const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); } catch (error) {
const hasValidAuth = await this.authStateManager.hasValidAuth( console.error(
workingDir, '[WebViewProvider] Error in attemptAuthStateRestoration:',
authMethod, error,
); );
console.log('[WebViewProvider] Has valid cached auth:', hasValidAuth);
if (hasValidAuth) {
console.log(
'[WebViewProvider] Valid auth found, attempting connection...',
);
// Try to connect with cached auth
await this.initializeAgentConnection();
} else {
console.log(
'[WebViewProvider] No valid auth found, rendering empty conversation',
);
// Render the chat UI immediately without connecting
await this.initializeEmptyConversation();
}
} else {
console.log(
'[WebViewProvider] No auth state manager, rendering empty conversation',
);
await this.initializeEmptyConversation();
}
} catch (_error) {
console.error('[WebViewProvider] Auth state restoration failed:', _error);
// Fallback to rendering empty conversation
await this.initializeEmptyConversation(); await this.initializeEmptyConversation();
} }
} }
@@ -565,84 +538,84 @@ export class WebViewProvider {
* Can be called from show() or via /login command * Can be called from show() or via /login command
*/ */
async initializeAgentConnection(): Promise<void> { async initializeAgentConnection(): Promise<void> {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; return this.doInitializeAgentConnection();
const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); }
console.log( /**
'[WebViewProvider] Starting initialization, workingDir:', * Internal: perform actual connection/initialization (no auth locking).
workingDir, */
); private async doInitializeAgentConnection(): Promise<void> {
console.log( const run = async () => {
'[WebViewProvider] AuthStateManager available:', const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
!!this.authStateManager, const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
);
// Check if CLI is installed before attempting to connect
const cliDetection = await CliDetector.detectQwenCli();
if (!cliDetection.isInstalled) {
console.log( console.log(
'[WebViewProvider] Qwen CLI not detected, skipping agent connection', '[WebViewProvider] Starting initialization, workingDir:',
workingDir,
); );
console.log('[WebViewProvider] CLI detection error:', cliDetection.error); console.log('[WebViewProvider] Using CLI-managed authentication');
// Show VSCode notification with installation option // Check if CLI is installed before attempting to connect
await CliInstaller.promptInstallation(); const cliDetection = await CliDetector.detectQwenCli();
// Initialize empty conversation (can still browse history) if (!cliDetection.isInstalled) {
await this.initializeEmptyConversation();
} else {
console.log(
'[WebViewProvider] Qwen CLI detected, attempting connection...',
);
console.log('[WebViewProvider] CLI path:', cliDetection.cliPath);
console.log('[WebViewProvider] CLI version:', cliDetection.version);
try {
console.log('[WebViewProvider] Connecting to agent...');
console.log( console.log(
'[WebViewProvider] Using authStateManager:', '[WebViewProvider] Qwen CLI not detected, skipping agent connection',
!!this.authStateManager,
); );
const authInfo = await this.authStateManager.getAuthInfo(); console.log(
console.log('[WebViewProvider] Auth cache status:', authInfo); '[WebViewProvider] CLI detection error:',
cliDetection.error,
// Pass the detected CLI path to ensure we use the correct installation
await this.agentManager.connect(
workingDir,
this.authStateManager,
cliDetection.cliPath,
); );
console.log('[WebViewProvider] Agent connected successfully');
this.agentInitialized = true;
// Load messages from the current Qwen session // Show VSCode notification with installation option
await this.loadCurrentSessionMessages(); await CliInstaller.promptInstallation();
// Notify webview that agent is connected // Initialize empty conversation (can still browse history)
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(); await this.initializeEmptyConversation();
} else {
console.log(
'[WebViewProvider] Qwen CLI detected, attempting connection...',
);
console.log('[WebViewProvider] CLI path:', cliDetection.cliPath);
console.log('[WebViewProvider] CLI version:', cliDetection.version);
// Notify webview that agent connection failed try {
this.sendMessageToWebView({ console.log('[WebViewProvider] Connecting to agent...');
type: 'agentConnectionError',
data: { // Pass the detected CLI path to ensure we use the correct installation
message: _error instanceof Error ? _error.message : String(_error), await this.agentManager.connect(workingDir, cliDetection.cliPath);
}, console.log('[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);
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();
} }
/** /**
@@ -651,12 +624,8 @@ export class WebViewProvider {
*/ */
async forceReLogin(): Promise<void> { async forceReLogin(): Promise<void> {
console.log('[WebViewProvider] Force re-login requested'); console.log('[WebViewProvider] Force re-login requested');
console.log(
'[WebViewProvider] Current authStateManager:',
!!this.authStateManager,
);
await vscode.window.withProgress( return vscode.window.withProgress(
{ {
location: vscode.ProgressLocation.Notification, location: vscode.ProgressLocation.Notification,
title: 'Logging in to Qwen Code... ', title: 'Logging in to Qwen Code... ',
@@ -666,14 +635,6 @@ export class WebViewProvider {
try { try {
progress.report({ message: 'Preparing sign-in...' }); 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 // Disconnect existing connection if any
if (this.agentInitialized) { if (this.agentInitialized) {
try { try {
@@ -693,19 +654,11 @@ export class WebViewProvider {
}); });
// Reinitialize connection (will trigger fresh authentication) // Reinitialize connection (will trigger fresh authentication)
await this.initializeAgentConnection(); await this.doInitializeAgentConnection();
console.log( console.log(
'[WebViewProvider] Force re-login completed successfully', '[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 // Send success notification to WebView
this.sendMessageToWebView({ this.sendMessageToWebView({
type: 'loginSuccess', type: 'loginSuccess',
@@ -793,28 +746,23 @@ export class WebViewProvider {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
// Skip session restoration entirely and create a new session directly // avoid creating another session if connect() already created one.
try { if (!this.agentManager.currentSessionId) {
await this.agentManager.createNewSession( try {
workingDir, await this.agentManager.createNewSession(workingDir);
this.authStateManager, console.log('[WebViewProvider] ACP session created successfully');
); } catch (sessionError) {
console.log('[WebViewProvider] ACP session created successfully'); console.error(
'[WebViewProvider] Failed to create ACP session:',
// Ensure auth state is saved after successful session creation sessionError,
if (this.authStateManager) { );
await this.authStateManager.saveAuthState(workingDir, authMethod); vscode.window.showWarningMessage(
console.log( `Failed to create ACP session: ${sessionError}. You may need to authenticate first.`,
'[WebViewProvider] Auth state saved after session creation',
); );
} }
} catch (sessionError) { } else {
console.error( console.log(
'[WebViewProvider] Failed to create ACP session:', '[WebViewProvider] Existing ACP session detected, skipping new session creation',
sessionError,
);
vscode.window.showWarningMessage(
`Failed to create ACP session: ${sessionError}. You may need to authenticate first.`,
); );
} }
@@ -974,17 +922,6 @@ export class WebViewProvider {
this.agentManager.disconnect(); this.agentManager.disconnect();
} }
/**
* Clear authentication cache for this WebViewProvider instance
*/
async clearAuthCache(): Promise<void> {
console.log('[WebViewProvider] Clearing auth cache for this instance');
if (this.authStateManager) {
await this.authStateManager.clearAuthState();
this.resetAgentState();
}
}
/** /**
* Restore an existing WebView panel (called during VSCode restart) * Restore an existing WebView panel (called during VSCode restart)
* This sets up the panel with all event listeners * This sets up the panel with all event listeners
@@ -992,8 +929,7 @@ export class WebViewProvider {
async restorePanel(panel: vscode.WebviewPanel): Promise<void> { async restorePanel(panel: vscode.WebviewPanel): Promise<void> {
console.log('[WebViewProvider] Restoring WebView panel'); console.log('[WebViewProvider] Restoring WebView panel');
console.log( console.log(
'[WebViewProvider] Current authStateManager in restore:', '[WebViewProvider] Using CLI-managed authentication in restore',
!!this.authStateManager,
); );
this.panelManager.setPanel(panel); this.panelManager.setPanel(panel);
@@ -1196,18 +1132,13 @@ export class WebViewProvider {
const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
// Create new Qwen session via agent manager // Create new Qwen session via agent manager
await this.agentManager.createNewSession( await this.agentManager.createNewSession(workingDir);
workingDir,
this.authStateManager,
);
// Clear current conversation UI // Clear current conversation UI
this.sendMessageToWebView({ this.sendMessageToWebView({
type: 'conversationCleared', type: 'conversationCleared',
data: {}, data: {},
}); });
console.log('[WebViewProvider] New session created successfully');
} catch (_error) { } catch (_error) {
console.error('[WebViewProvider] Failed to create new session:', _error); console.error('[WebViewProvider] Failed to create new session:', _error);
vscode.window.showErrorMessage(`Failed to create new session: ${_error}`); vscode.window.showErrorMessage(`Failed to create new session: ${_error}`);

View File

@@ -61,25 +61,6 @@ export const safeTitle = (title: unknown): string => {
return ''; return '';
}; };
/**
* Get icon emoji for a given tool kind
*/
export const getKindIcon = (kind: string): string => {
const kindMap: Record<string, string> = {
edit: '✏️',
write: '✏️',
read: '📖',
execute: '⚡',
fetch: '🌐',
delete: '🗑️',
move: '📦',
search: '🔍',
think: '💭',
diff: '📝',
};
return kindMap[kind.toLowerCase()] || '🔧';
};
/** /**
* Check if a tool call should be displayed * Check if a tool call should be displayed
* Hides internal tool calls * Hides internal tool calls

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,26 +315,37 @@ 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;
} }
// 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)')
) {
await this.promptLogin(
'Your login session has expired or is invalid. Please login again to continue using Qwen Code.',
);
return;
}
vscode.window.showErrorMessage(`Failed to create session: ${errorMsg}`);
return;
}
}
// Send to agent // Send to agent
try { try {
this.resetStreamContent(); this.resetStreamContent();
@@ -391,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',
@@ -428,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;
} }
} }
@@ -489,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',
@@ -525,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);
@@ -550,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;
} }
@@ -637,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',
@@ -706,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',
@@ -755,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',
@@ -819,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',
@@ -883,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',
@@ -931,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',
@@ -996,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;
@@ -1020,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;
} }
} }
@@ -1054,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',
@@ -1105,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',

View File

@@ -227,40 +227,26 @@ export const useWebViewMessages = ({
break; break;
} }
// case 'cliNotInstalled': { case 'agentConnected': {
// // Show CLI not installed message // Agent connected successfully; clear any pending spinner
// const errorMsg = handlers.messageHandling.clearWaitingForResponse();
// (message?.data?.error as string) || break;
// 'Qwen Code CLI is not installed. Please install it to enable full functionality.'; }
// handlers.messageHandling.addMessage({ case 'agentConnectionError': {
// role: 'assistant', // Agent connection failed; surface the error and unblock the UI
// 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.`, handlers.messageHandling.clearWaitingForResponse();
// timestamp: Date.now(), const errorMsg =
// }); (message?.data?.message as string) ||
// break; 'Failed to connect to Qwen agent.';
// }
// case 'agentConnected': { handlers.messageHandling.addMessage({
// // Agent connected successfully role: 'assistant',
// handlers.messageHandling.clearWaitingForResponse(); content: `Failed to connect to Qwen agent: ${errorMsg}\nYou can still use the chat UI, but messages won't be sent to AI.`,
// break; timestamp: Date.now(),
// } });
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;
// }
case 'loginError': { case 'loginError': {
// Clear loading state and show error notice // Clear loading state and show error notice

View File

@@ -5,7 +5,6 @@
*/ */
/* Import component styles */ /* Import component styles */
@import '../components/messages/Assistant/AssistantMessage.css';
@import './timeline.css'; @import './timeline.css';
@import '../components/messages/MarkdownRenderer/MarkdownRenderer.css'; @import '../components/messages/MarkdownRenderer/MarkdownRenderer.css';

View File

@@ -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<T>(
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 };
}