mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-22 09:47:47 +00:00
feat(vscode-ide-companion/auth): deduplicate concurrent authentication calls
Prevent multiple simultaneous authentication flows by: - Adding static authInFlight promise tracking in AcpConnection - Implementing runExclusiveAuth method in AuthStateManager - Adding sessionCreateInFlight tracking in QwenAgentManager - Ensuring only one auth flow runs at a time across different components This prevents race conditions and duplicate login prompts when multiple components request authentication simultaneously.
This commit is contained in:
@@ -45,9 +45,11 @@ import { FileIcon, UserIcon } from './components/icons/index.js';
|
||||
import { ApprovalMode, NEXT_APPROVAL_MODE } from '../types/acpTypes.js';
|
||||
import type { ApprovalModeValue } from '../types/acpTypes.js';
|
||||
import type { PlanEntry } from '../types/chatTypes.js';
|
||||
import { createWebviewConsoleLogger } from './utils/logger.js';
|
||||
|
||||
export const App: React.FC = () => {
|
||||
const vscode = useVSCode();
|
||||
const consoleLog = useMemo(() => createWebviewConsoleLogger('App'), []);
|
||||
|
||||
// Core hooks
|
||||
const sessionManagement = useSessionManagement(vscode);
|
||||
@@ -167,7 +169,7 @@ export const App: React.FC = () => {
|
||||
}, [fileContext.workspaceFiles, completion.isOpen, completion.triggerChar]);
|
||||
|
||||
// Message submission
|
||||
const handleSubmit = useMessageSubmit({
|
||||
const { handleSubmit: submitMessage } = useMessageSubmit({
|
||||
inputText,
|
||||
setInputText,
|
||||
messageHandling,
|
||||
@@ -487,6 +489,22 @@ export const App: React.FC = () => {
|
||||
setThinkingEnabled((prev) => !prev);
|
||||
};
|
||||
|
||||
// When user sends a message after scrolling up, re-pin and jump to the bottom
|
||||
const handleSubmitWithScroll = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
setPinnedToBottom(true);
|
||||
|
||||
const container = messagesContainerRef.current;
|
||||
if (container) {
|
||||
const top = container.scrollHeight - container.clientHeight;
|
||||
container.scrollTo({ top });
|
||||
}
|
||||
|
||||
submitMessage(e);
|
||||
},
|
||||
[submitMessage],
|
||||
);
|
||||
|
||||
// Create unified message array containing all types of messages and tool calls
|
||||
const allMessages = useMemo<
|
||||
Array<{
|
||||
@@ -524,7 +542,7 @@ export const App: React.FC = () => {
|
||||
);
|
||||
}, [messageHandling.messages, inProgressToolCalls, completedToolCalls]);
|
||||
|
||||
console.log('[App] Rendering messages:', allMessages);
|
||||
consoleLog('[App] Rendering messages:', allMessages);
|
||||
|
||||
// Render all messages and tool calls
|
||||
const renderMessages = useCallback<() => React.ReactNode>(
|
||||
@@ -686,7 +704,7 @@ export const App: React.FC = () => {
|
||||
onCompositionStart={() => setIsComposing(true)}
|
||||
onCompositionEnd={() => setIsComposing(false)}
|
||||
onKeyDown={() => {}}
|
||||
onSubmit={handleSubmit.handleSubmit}
|
||||
onSubmit={handleSubmitWithScroll}
|
||||
onCancel={handleCancel}
|
||||
onToggleEditMode={handleToggleEditMode}
|
||||
onToggleThinking={handleToggleThinking}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { WebViewContent } from '../webview/WebViewContent.js';
|
||||
import { CliInstaller } from '../cli/cliInstaller.js';
|
||||
import { getFileName } from './utils/webviewUtils.js';
|
||||
import { authMethod, type ApprovalModeValue } from '../types/acpTypes.js';
|
||||
import { createConsoleLogger } from '../utils/logger.js';
|
||||
|
||||
export class WebViewProvider {
|
||||
private panelManager: PanelManager;
|
||||
@@ -32,12 +33,15 @@ export class WebViewProvider {
|
||||
private pendingPermissionResolve: ((optionId: string) => void) | null = null;
|
||||
// Track current ACP mode id to influence permission/diff behavior
|
||||
private currentModeId: ApprovalModeValue | null = null;
|
||||
private consoleLog: (...args: unknown[]) => void;
|
||||
|
||||
constructor(
|
||||
context: vscode.ExtensionContext,
|
||||
private extensionUri: vscode.Uri,
|
||||
) {
|
||||
this.agentManager = new QwenAgentManager();
|
||||
const agentConsoleLogger = createConsoleLogger(context, 'QwenAgentManager');
|
||||
this.consoleLog = createConsoleLogger(context, 'WebViewProvider');
|
||||
this.agentManager = new QwenAgentManager(agentConsoleLogger);
|
||||
this.conversationStore = new ConversationStore(context);
|
||||
this.authStateManager = AuthStateManager.getInstance(context);
|
||||
this.panelManager = new PanelManager(extensionUri, () => {
|
||||
@@ -380,7 +384,7 @@ export class WebViewProvider {
|
||||
|
||||
// Set up state serialization
|
||||
newPanel.onDidChangeViewState(() => {
|
||||
console.log(
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Panel view state changed, triggering serialization check',
|
||||
);
|
||||
});
|
||||
@@ -510,7 +514,7 @@ export class WebViewProvider {
|
||||
}
|
||||
|
||||
// Attempt to restore authentication state and initialize connection
|
||||
console.log(
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Attempting to restore auth state and connection...',
|
||||
);
|
||||
await this.attemptAuthStateRestoration();
|
||||
@@ -532,23 +536,26 @@ export class WebViewProvider {
|
||||
workingDir,
|
||||
authMethod,
|
||||
);
|
||||
console.log('[WebViewProvider] Has valid cached auth:', hasValidAuth);
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Has valid cached auth:',
|
||||
hasValidAuth,
|
||||
);
|
||||
|
||||
if (hasValidAuth) {
|
||||
console.log(
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Valid auth found, attempting connection...',
|
||||
);
|
||||
// Try to connect with cached auth
|
||||
await this.initializeAgentConnection();
|
||||
} else {
|
||||
console.log(
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] No valid auth found, rendering empty conversation',
|
||||
);
|
||||
// Render the chat UI immediately without connecting
|
||||
await this.initializeEmptyConversation();
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] No auth state manager, rendering empty conversation',
|
||||
);
|
||||
await this.initializeEmptyConversation();
|
||||
@@ -565,84 +572,101 @@ export class WebViewProvider {
|
||||
* Can be called from show() or via /login command
|
||||
*/
|
||||
async initializeAgentConnection(): Promise<void> {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||
|
||||
console.log(
|
||||
'[WebViewProvider] Starting initialization, workingDir:',
|
||||
workingDir,
|
||||
);
|
||||
console.log(
|
||||
'[WebViewProvider] AuthStateManager available:',
|
||||
!!this.authStateManager,
|
||||
return AuthStateManager.runExclusiveAuth(() =>
|
||||
this.doInitializeAgentConnection(),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if CLI is installed before attempting to connect
|
||||
const cliDetection = await CliDetector.detectQwenCli();
|
||||
/**
|
||||
* Internal: perform actual connection/initialization (no auth locking).
|
||||
*/
|
||||
private async doInitializeAgentConnection(): Promise<void> {
|
||||
const run = async () => {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||
|
||||
if (!cliDetection.isInstalled) {
|
||||
console.log(
|
||||
'[WebViewProvider] Qwen CLI not detected, skipping agent connection',
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Starting initialization, workingDir:',
|
||||
workingDir,
|
||||
);
|
||||
console.log('[WebViewProvider] CLI detection error:', cliDetection.error);
|
||||
|
||||
// Show VSCode notification with installation option
|
||||
await CliInstaller.promptInstallation();
|
||||
|
||||
// Initialize empty conversation (can still browse history)
|
||||
await this.initializeEmptyConversation();
|
||||
} else {
|
||||
console.log(
|
||||
'[WebViewProvider] Qwen CLI detected, attempting connection...',
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] AuthStateManager available:',
|
||||
!!this.authStateManager,
|
||||
);
|
||||
console.log('[WebViewProvider] CLI path:', cliDetection.cliPath);
|
||||
console.log('[WebViewProvider] CLI version:', cliDetection.version);
|
||||
|
||||
try {
|
||||
console.log('[WebViewProvider] Connecting to agent...');
|
||||
console.log(
|
||||
'[WebViewProvider] Using authStateManager:',
|
||||
!!this.authStateManager,
|
||||
// Check if CLI is installed before attempting to connect
|
||||
const cliDetection = await CliDetector.detectQwenCli();
|
||||
|
||||
if (!cliDetection.isInstalled) {
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Qwen CLI not detected, skipping agent connection',
|
||||
);
|
||||
const authInfo = await this.authStateManager.getAuthInfo();
|
||||
console.log('[WebViewProvider] Auth cache status:', authInfo);
|
||||
|
||||
// Pass the detected CLI path to ensure we use the correct installation
|
||||
await this.agentManager.connect(
|
||||
workingDir,
|
||||
this.authStateManager,
|
||||
cliDetection.cliPath,
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] CLI detection error:',
|
||||
cliDetection.error,
|
||||
);
|
||||
console.log('[WebViewProvider] Agent connected successfully');
|
||||
this.agentInitialized = true;
|
||||
|
||||
// Load messages from the current Qwen session
|
||||
await this.loadCurrentSessionMessages();
|
||||
// Show VSCode notification with installation option
|
||||
await CliInstaller.promptInstallation();
|
||||
|
||||
// Notify webview that agent is connected
|
||||
this.sendMessageToWebView({
|
||||
type: 'agentConnected',
|
||||
data: {},
|
||||
});
|
||||
} catch (_error) {
|
||||
console.error('[WebViewProvider] Agent connection error:', _error);
|
||||
// Clear auth cache on error (might be auth issue)
|
||||
await this.authStateManager.clearAuthState();
|
||||
vscode.window.showWarningMessage(
|
||||
`Failed to connect to Qwen CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`,
|
||||
);
|
||||
// Fallback to empty conversation
|
||||
// Initialize empty conversation (can still browse history)
|
||||
await this.initializeEmptyConversation();
|
||||
} else {
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Qwen CLI detected, attempting connection...',
|
||||
);
|
||||
this.consoleLog('[WebViewProvider] CLI path:', cliDetection.cliPath);
|
||||
this.consoleLog('[WebViewProvider] CLI version:', cliDetection.version);
|
||||
|
||||
// Notify webview that agent connection failed
|
||||
this.sendMessageToWebView({
|
||||
type: 'agentConnectionError',
|
||||
data: {
|
||||
message: _error instanceof Error ? _error.message : String(_error),
|
||||
},
|
||||
});
|
||||
try {
|
||||
this.consoleLog('[WebViewProvider] Connecting to agent...');
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Using authStateManager:',
|
||||
!!this.authStateManager,
|
||||
);
|
||||
const authInfo = await this.authStateManager.getAuthInfo();
|
||||
this.consoleLog('[WebViewProvider] Auth cache status:', authInfo);
|
||||
|
||||
// Pass the detected CLI path to ensure we use the correct installation
|
||||
await this.agentManager.connect(
|
||||
workingDir,
|
||||
this.authStateManager,
|
||||
cliDetection.cliPath,
|
||||
);
|
||||
this.consoleLog('[WebViewProvider] Agent connected successfully');
|
||||
this.agentInitialized = true;
|
||||
|
||||
// Load messages from the current Qwen session
|
||||
await this.loadCurrentSessionMessages();
|
||||
|
||||
// Notify webview that agent is connected
|
||||
this.sendMessageToWebView({
|
||||
type: 'agentConnected',
|
||||
data: {},
|
||||
});
|
||||
} catch (_error) {
|
||||
console.error('[WebViewProvider] Agent connection error:', _error);
|
||||
// Clear auth cache on error (might be auth issue)
|
||||
await this.authStateManager.clearAuthState();
|
||||
vscode.window.showWarningMessage(
|
||||
`Failed to connect to Qwen CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`,
|
||||
);
|
||||
// Fallback to empty conversation
|
||||
await this.initializeEmptyConversation();
|
||||
|
||||
// Notify webview that agent connection failed
|
||||
this.sendMessageToWebView({
|
||||
type: 'agentConnectionError',
|
||||
data: {
|
||||
message:
|
||||
_error instanceof Error ? _error.message : String(_error),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return run();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -650,86 +674,97 @@ export class WebViewProvider {
|
||||
* Called when user explicitly uses /login command
|
||||
*/
|
||||
async forceReLogin(): Promise<void> {
|
||||
console.log('[WebViewProvider] Force re-login requested');
|
||||
console.log(
|
||||
this.consoleLog('[WebViewProvider] Force re-login requested');
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Current authStateManager:',
|
||||
!!this.authStateManager,
|
||||
);
|
||||
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: 'Logging in to Qwen Code... ',
|
||||
cancellable: false,
|
||||
},
|
||||
async (progress) => {
|
||||
try {
|
||||
progress.report({ message: 'Preparing sign-in...' });
|
||||
// If a login/connection flow is already running, reuse it to avoid double prompts
|
||||
const p = Promise.resolve(
|
||||
vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
cancellable: false,
|
||||
},
|
||||
async (progress) => {
|
||||
try {
|
||||
progress.report({ message: 'Preparing sign-in...' });
|
||||
|
||||
// Clear existing auth cache
|
||||
if (this.authStateManager) {
|
||||
await this.authStateManager.clearAuthState();
|
||||
console.log('[WebViewProvider] Auth cache cleared');
|
||||
} else {
|
||||
console.log('[WebViewProvider] No authStateManager to clear');
|
||||
}
|
||||
|
||||
// Disconnect existing connection if any
|
||||
if (this.agentInitialized) {
|
||||
try {
|
||||
this.agentManager.disconnect();
|
||||
console.log('[WebViewProvider] Existing connection disconnected');
|
||||
} catch (_error) {
|
||||
console.log('[WebViewProvider] Error disconnecting:', _error);
|
||||
// Clear existing auth cache
|
||||
if (this.authStateManager) {
|
||||
await this.authStateManager.clearAuthState();
|
||||
this.consoleLog('[WebViewProvider] Auth cache cleared');
|
||||
} else {
|
||||
this.consoleLog('[WebViewProvider] No authStateManager to clear');
|
||||
}
|
||||
this.agentInitialized = false;
|
||||
|
||||
// Disconnect existing connection if any
|
||||
if (this.agentInitialized) {
|
||||
try {
|
||||
this.agentManager.disconnect();
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Existing connection disconnected',
|
||||
);
|
||||
} catch (_error) {
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Error disconnecting:',
|
||||
_error,
|
||||
);
|
||||
}
|
||||
this.agentInitialized = false;
|
||||
}
|
||||
|
||||
// Wait a moment for cleanup to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
progress.report({
|
||||
message: 'Connecting to CLI and starting sign-in...',
|
||||
});
|
||||
|
||||
// Reinitialize connection (will trigger fresh authentication)
|
||||
await this.doInitializeAgentConnection();
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Force re-login completed successfully',
|
||||
);
|
||||
|
||||
// Ensure auth state is saved after successful re-login
|
||||
if (this.authStateManager) {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||
await this.authStateManager.saveAuthState(workingDir, authMethod);
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Auth state saved after re-login',
|
||||
);
|
||||
}
|
||||
|
||||
// Send success notification to WebView
|
||||
this.sendMessageToWebView({
|
||||
type: 'loginSuccess',
|
||||
data: { message: 'Successfully logged in!' },
|
||||
});
|
||||
} catch (_error) {
|
||||
console.error('[WebViewProvider] Force re-login failed:', _error);
|
||||
console.error(
|
||||
'[WebViewProvider] Error stack:',
|
||||
_error instanceof Error ? _error.stack : 'N/A',
|
||||
);
|
||||
|
||||
// Send error notification to WebView
|
||||
this.sendMessageToWebView({
|
||||
type: 'loginError',
|
||||
data: {
|
||||
message: `Login failed: ${_error instanceof Error ? _error.message : String(_error)}`,
|
||||
},
|
||||
});
|
||||
|
||||
throw _error;
|
||||
}
|
||||
|
||||
// Wait a moment for cleanup to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
progress.report({
|
||||
message: 'Connecting to CLI and starting sign-in...',
|
||||
});
|
||||
|
||||
// Reinitialize connection (will trigger fresh authentication)
|
||||
await this.initializeAgentConnection();
|
||||
console.log(
|
||||
'[WebViewProvider] Force re-login completed successfully',
|
||||
);
|
||||
|
||||
// Ensure auth state is saved after successful re-login
|
||||
if (this.authStateManager) {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||
await this.authStateManager.saveAuthState(workingDir, authMethod);
|
||||
console.log('[WebViewProvider] Auth state saved after re-login');
|
||||
}
|
||||
|
||||
// Send success notification to WebView
|
||||
this.sendMessageToWebView({
|
||||
type: 'loginSuccess',
|
||||
data: { message: 'Successfully logged in!' },
|
||||
});
|
||||
} catch (_error) {
|
||||
console.error('[WebViewProvider] Force re-login failed:', _error);
|
||||
console.error(
|
||||
'[WebViewProvider] Error stack:',
|
||||
_error instanceof Error ? _error.stack : 'N/A',
|
||||
);
|
||||
|
||||
// Send error notification to WebView
|
||||
this.sendMessageToWebView({
|
||||
type: 'loginError',
|
||||
data: {
|
||||
message: `Login failed: ${_error instanceof Error ? _error.message : String(_error)}`,
|
||||
},
|
||||
});
|
||||
|
||||
throw _error;
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return AuthStateManager.runExclusiveAuth(() => p);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -737,15 +772,15 @@ export class WebViewProvider {
|
||||
* Called when restoring WebView after VSCode restart
|
||||
*/
|
||||
async refreshConnection(): Promise<void> {
|
||||
console.log('[WebViewProvider] Refresh connection requested');
|
||||
this.consoleLog('[WebViewProvider] Refresh connection requested');
|
||||
|
||||
// Disconnect existing connection if any
|
||||
if (this.agentInitialized) {
|
||||
try {
|
||||
this.agentManager.disconnect();
|
||||
console.log('[WebViewProvider] Existing connection disconnected');
|
||||
this.consoleLog('[WebViewProvider] Existing connection disconnected');
|
||||
} catch (_error) {
|
||||
console.log('[WebViewProvider] Error disconnecting:', _error);
|
||||
this.consoleLog('[WebViewProvider] Error disconnecting:', _error);
|
||||
}
|
||||
this.agentInitialized = false;
|
||||
}
|
||||
@@ -756,7 +791,7 @@ export class WebViewProvider {
|
||||
// Reinitialize connection (will use cached auth if available)
|
||||
try {
|
||||
await this.initializeAgentConnection();
|
||||
console.log(
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Connection refresh completed successfully',
|
||||
);
|
||||
|
||||
@@ -786,35 +821,41 @@ export class WebViewProvider {
|
||||
*/
|
||||
private async loadCurrentSessionMessages(): Promise<void> {
|
||||
try {
|
||||
console.log(
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Initializing with new session (skipping restoration)',
|
||||
);
|
||||
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||
|
||||
// Skip session restoration entirely and create a new session directly
|
||||
try {
|
||||
await this.agentManager.createNewSession(
|
||||
workingDir,
|
||||
this.authStateManager,
|
||||
);
|
||||
console.log('[WebViewProvider] ACP session created successfully');
|
||||
// avoid creating another session if connect() already created one.
|
||||
if (!this.agentManager.currentSessionId) {
|
||||
try {
|
||||
await this.agentManager.createNewSession(
|
||||
workingDir,
|
||||
this.authStateManager,
|
||||
);
|
||||
this.consoleLog('[WebViewProvider] ACP session created successfully');
|
||||
|
||||
// Ensure auth state is saved after successful session creation
|
||||
if (this.authStateManager) {
|
||||
await this.authStateManager.saveAuthState(workingDir, authMethod);
|
||||
console.log(
|
||||
'[WebViewProvider] Auth state saved after session creation',
|
||||
// Ensure auth state is saved after successful session creation
|
||||
if (this.authStateManager) {
|
||||
await this.authStateManager.saveAuthState(workingDir, authMethod);
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Auth state saved after session creation',
|
||||
);
|
||||
}
|
||||
} catch (sessionError) {
|
||||
console.error(
|
||||
'[WebViewProvider] Failed to create ACP session:',
|
||||
sessionError,
|
||||
);
|
||||
vscode.window.showWarningMessage(
|
||||
`Failed to create ACP session: ${sessionError}. You may need to authenticate first.`,
|
||||
);
|
||||
}
|
||||
} catch (sessionError) {
|
||||
console.error(
|
||||
'[WebViewProvider] Failed to create ACP session:',
|
||||
sessionError,
|
||||
);
|
||||
vscode.window.showWarningMessage(
|
||||
`Failed to create ACP session: ${sessionError}. You may need to authenticate first.`,
|
||||
} else {
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Existing ACP session detected, skipping new session creation',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -837,14 +878,14 @@ export class WebViewProvider {
|
||||
*/
|
||||
private async initializeEmptyConversation(): Promise<void> {
|
||||
try {
|
||||
console.log('[WebViewProvider] Initializing empty conversation');
|
||||
this.consoleLog('[WebViewProvider] Initializing empty conversation');
|
||||
const newConv = await this.conversationStore.createConversation();
|
||||
this.messageHandler.setCurrentConversationId(newConv.id);
|
||||
this.sendMessageToWebView({
|
||||
type: 'conversationLoaded',
|
||||
data: newConv,
|
||||
});
|
||||
console.log(
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Empty conversation initialized:',
|
||||
this.messageHandler.getCurrentConversationId(),
|
||||
);
|
||||
@@ -968,7 +1009,7 @@ export class WebViewProvider {
|
||||
* Call this when auth cache is cleared to force re-authentication
|
||||
*/
|
||||
resetAgentState(): void {
|
||||
console.log('[WebViewProvider] Resetting agent state');
|
||||
this.consoleLog('[WebViewProvider] Resetting agent state');
|
||||
this.agentInitialized = false;
|
||||
// Disconnect existing connection
|
||||
this.agentManager.disconnect();
|
||||
@@ -978,7 +1019,7 @@ export class WebViewProvider {
|
||||
* Clear authentication cache for this WebViewProvider instance
|
||||
*/
|
||||
async clearAuthCache(): Promise<void> {
|
||||
console.log('[WebViewProvider] Clearing auth cache for this instance');
|
||||
this.consoleLog('[WebViewProvider] Clearing auth cache for this instance');
|
||||
if (this.authStateManager) {
|
||||
await this.authStateManager.clearAuthState();
|
||||
this.resetAgentState();
|
||||
@@ -990,8 +1031,8 @@ export class WebViewProvider {
|
||||
* This sets up the panel with all event listeners
|
||||
*/
|
||||
async restorePanel(panel: vscode.WebviewPanel): Promise<void> {
|
||||
console.log('[WebViewProvider] Restoring WebView panel');
|
||||
console.log(
|
||||
this.consoleLog('[WebViewProvider] Restoring WebView panel');
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Current authStateManager in restore:',
|
||||
!!this.authStateManager,
|
||||
);
|
||||
@@ -1122,10 +1163,10 @@ export class WebViewProvider {
|
||||
// Capture the tab reference on restore
|
||||
this.panelManager.captureTab();
|
||||
|
||||
console.log('[WebViewProvider] Panel restored successfully');
|
||||
this.consoleLog('[WebViewProvider] Panel restored successfully');
|
||||
|
||||
// Attempt to restore authentication state and initialize connection
|
||||
console.log(
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Attempting to restore auth state and connection after restore...',
|
||||
);
|
||||
await this.attemptAuthStateRestoration();
|
||||
@@ -1139,12 +1180,12 @@ export class WebViewProvider {
|
||||
conversationId: string | null;
|
||||
agentInitialized: boolean;
|
||||
} {
|
||||
console.log('[WebViewProvider] Getting state for serialization');
|
||||
console.log(
|
||||
this.consoleLog('[WebViewProvider] Getting state for serialization');
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Current conversationId:',
|
||||
this.messageHandler.getCurrentConversationId(),
|
||||
);
|
||||
console.log(
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] Current agentInitialized:',
|
||||
this.agentInitialized,
|
||||
);
|
||||
@@ -1152,7 +1193,7 @@ export class WebViewProvider {
|
||||
conversationId: this.messageHandler.getCurrentConversationId(),
|
||||
agentInitialized: this.agentInitialized,
|
||||
};
|
||||
console.log('[WebViewProvider] Returning state:', state);
|
||||
this.consoleLog('[WebViewProvider] Returning state:', state);
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1170,10 +1211,10 @@ export class WebViewProvider {
|
||||
conversationId: string | null;
|
||||
agentInitialized: boolean;
|
||||
}): void {
|
||||
console.log('[WebViewProvider] Restoring state:', state);
|
||||
this.consoleLog('[WebViewProvider] Restoring state:', state);
|
||||
this.messageHandler.setCurrentConversationId(state.conversationId);
|
||||
this.agentInitialized = state.agentInitialized;
|
||||
console.log(
|
||||
this.consoleLog(
|
||||
'[WebViewProvider] State restored. agentInitialized:',
|
||||
this.agentInitialized,
|
||||
);
|
||||
@@ -1206,8 +1247,6 @@ export class WebViewProvider {
|
||||
type: 'conversationCleared',
|
||||
data: {},
|
||||
});
|
||||
|
||||
console.log('[WebViewProvider] New session created successfully');
|
||||
} catch (_error) {
|
||||
console.error('[WebViewProvider] Failed to create new session:', _error);
|
||||
vscode.window.showErrorMessage(`Failed to create new session: ${_error}`);
|
||||
|
||||
@@ -291,6 +291,41 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure an ACP session exists before sending prompt
|
||||
if (!this.agentManager.currentSessionId) {
|
||||
try {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||
await this.agentManager.createNewSession(workingDir);
|
||||
} catch (createErr) {
|
||||
console.error(
|
||||
'[SessionMessageHandler] Failed to create session before sending message:',
|
||||
createErr,
|
||||
);
|
||||
const errorMsg =
|
||||
createErr instanceof Error ? createErr.message : String(createErr);
|
||||
if (
|
||||
errorMsg.includes('Authentication required') ||
|
||||
errorMsg.includes('(code: -32000)')
|
||||
) {
|
||||
const result = await vscode.window.showWarningMessage(
|
||||
'Your login session has expired or is invalid. Please login again to continue using Qwen Code.',
|
||||
'Login Now',
|
||||
);
|
||||
if (result === 'Login Now') {
|
||||
if (this.loginHandler) {
|
||||
await this.loginHandler();
|
||||
} else {
|
||||
await vscode.commands.executeCommand('qwen-code.login');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
vscode.window.showErrorMessage(`Failed to create session: ${errorMsg}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Send to agent
|
||||
try {
|
||||
this.resetStreamContent();
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
import type { ToolCallUpdate } from '../../types/chatTypes.js';
|
||||
import type { ApprovalModeValue } from '../../types/acpTypes.js';
|
||||
import type { PlanEntry } from '../../types/chatTypes.js';
|
||||
import { createWebviewConsoleLogger } from '../utils/logger.js';
|
||||
|
||||
interface UseWebViewMessagesProps {
|
||||
// Session management
|
||||
@@ -129,6 +130,7 @@ export const useWebViewMessages = ({
|
||||
}: UseWebViewMessagesProps) => {
|
||||
// VS Code API for posting messages back to the extension host
|
||||
const vscode = useVSCode();
|
||||
const consoleLog = useRef(createWebviewConsoleLogger('WebViewMessages'));
|
||||
// Track active long-running tool calls (execute/bash/command) so we can
|
||||
// keep the bottom "waiting" message visible until all of them complete.
|
||||
const activeExecToolCallsRef = useRef<Set<string>>(new Set());
|
||||
@@ -227,40 +229,26 @@ export const useWebViewMessages = ({
|
||||
break;
|
||||
}
|
||||
|
||||
// case 'cliNotInstalled': {
|
||||
// // Show CLI not installed message
|
||||
// const errorMsg =
|
||||
// (message?.data?.error as string) ||
|
||||
// 'Qwen Code CLI is not installed. Please install it to enable full functionality.';
|
||||
case 'agentConnected': {
|
||||
// Agent connected successfully; clear any pending spinner
|
||||
handlers.messageHandling.clearWaitingForResponse();
|
||||
break;
|
||||
}
|
||||
|
||||
// handlers.messageHandling.addMessage({
|
||||
// role: 'assistant',
|
||||
// content: `Qwen CLI is not installed. Please install it to enable full functionality.\n\nError: ${errorMsg}\n\nInstallation instructions:\n1. Install via npm:\n npm install -g @qwen-code/qwen-code@latest\n\n2. After installation, reload VS Code or restart the extension.`,
|
||||
// timestamp: Date.now(),
|
||||
// });
|
||||
// break;
|
||||
// }
|
||||
case 'agentConnectionError': {
|
||||
// Agent connection failed; surface the error and unblock the UI
|
||||
handlers.messageHandling.clearWaitingForResponse();
|
||||
const errorMsg =
|
||||
(message?.data?.message as string) ||
|
||||
'Failed to connect to Qwen agent.';
|
||||
|
||||
// case 'agentConnected': {
|
||||
// // Agent connected successfully
|
||||
// handlers.messageHandling.clearWaitingForResponse();
|
||||
// break;
|
||||
// }
|
||||
|
||||
// case 'agentConnectionError': {
|
||||
// // Agent connection failed
|
||||
// handlers.messageHandling.clearWaitingForResponse();
|
||||
// const errorMsg =
|
||||
// (message?.data?.message as string) ||
|
||||
// 'Failed to connect to Qwen agent.';
|
||||
|
||||
// handlers.messageHandling.addMessage({
|
||||
// role: 'assistant',
|
||||
// content: `Failed to connect to Qwen agent: ${errorMsg}\nYou can still use the chat UI, but messages won't be sent to AI.`,
|
||||
// timestamp: Date.now(),
|
||||
// });
|
||||
// break;
|
||||
// }
|
||||
handlers.messageHandling.addMessage({
|
||||
role: 'assistant',
|
||||
content: `Failed to connect to Qwen agent: ${errorMsg}\nYou can still use the chat UI, but messages won't be sent to AI.`,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'loginError': {
|
||||
// Clear loading state and show error notice
|
||||
@@ -765,7 +753,10 @@ export const useWebViewMessages = ({
|
||||
path: string;
|
||||
}>;
|
||||
if (files) {
|
||||
console.log('[WebView] Received workspaceFiles:', files.length);
|
||||
consoleLog.current(
|
||||
'[WebView] Received workspaceFiles:',
|
||||
files.length,
|
||||
);
|
||||
handlers.fileContext.setWorkspaceFiles(files);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
*/
|
||||
|
||||
/* Import component styles */
|
||||
@import '../components/messages/Assistant/AssistantMessage.css';
|
||||
@import './timeline.css';
|
||||
@import '../components/messages/MarkdownRenderer/MarkdownRenderer.css';
|
||||
|
||||
|
||||
25
packages/vscode-ide-companion/src/webview/utils/logger.ts
Normal file
25
packages/vscode-ide-companion/src/webview/utils/logger.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a dev-only console logger for the WebView bundle.
|
||||
* In production builds it becomes a no-op to avoid noisy logs.
|
||||
*/
|
||||
export function createWebviewConsoleLogger(scope?: string) {
|
||||
return (...args: unknown[]) => {
|
||||
const env = (globalThis as { process?: { env?: Record<string, string> } })
|
||||
.process?.env;
|
||||
const isProduction = env?.NODE_ENV === 'production';
|
||||
if (isProduction) {
|
||||
return;
|
||||
}
|
||||
if (scope) {
|
||||
console.log(`[${scope}]`, ...args);
|
||||
return;
|
||||
}
|
||||
console.log(...args);
|
||||
};
|
||||
}
|
||||
@@ -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