mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
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:
@@ -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}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user