mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat(vscode-ide-companion): implement manual login via /login command
BREAKING CHANGE: Login is no longer automatic when opening webview Changes: - Remove auto-login on webview open and restore - Add /login slash command for manual authentication - Add VSCode progress notification during login process - Add warning notification when user tries to chat without login - Implement pending message auto-retry after successful login - Add NotLoggedInMessage component (for future use) - Improve InfoBanner close button styling consistency User flow: 1. Open webview - no automatic login 2. Type /login or select from completion menu to login 3. Show "Logging in to Qwen Code..." progress notification 4. After login, show success message and auto-retry pending messages 5. If user tries to chat without login, show warning with "Login Now" button 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,11 @@ export class WebViewProvider {
|
||||
(message) => this.sendMessageToWebView(message),
|
||||
);
|
||||
|
||||
// Set login handler for /login command
|
||||
this.messageHandler.setLoginHandler(async () => {
|
||||
await this.initializeAgentConnection();
|
||||
});
|
||||
|
||||
// Setup agent callbacks
|
||||
this.agentManager.onStreamChunk((chunk: string) => {
|
||||
this.messageHandler.appendStreamContent(chunk);
|
||||
@@ -168,9 +173,13 @@ export class WebViewProvider {
|
||||
);
|
||||
this.disposables.push(editorChangeDisposable);
|
||||
|
||||
// Initialize agent connection only once
|
||||
// Don't auto-login; user must use /login command
|
||||
// Just initialize empty conversation for the UI
|
||||
if (!this.agentInitialized) {
|
||||
await this.initializeAgentConnection();
|
||||
console.log(
|
||||
'[WebViewProvider] Agent not initialized, waiting for /login command',
|
||||
);
|
||||
await this.initializeEmptyConversation();
|
||||
} else {
|
||||
console.log(
|
||||
'[WebViewProvider] Agent already initialized, reusing existing connection',
|
||||
@@ -182,9 +191,9 @@ export class WebViewProvider {
|
||||
|
||||
/**
|
||||
* Initialize agent connection and session
|
||||
* Can be called from show() or restorePanel()
|
||||
* Can be called from show() or via /login command
|
||||
*/
|
||||
private async initializeAgentConnection(): Promise<void> {
|
||||
async initializeAgentConnection(): Promise<void> {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||
|
||||
@@ -383,14 +392,15 @@ export class WebViewProvider {
|
||||
|
||||
console.log('[WebViewProvider] Panel restored successfully');
|
||||
|
||||
// Initialize agent connection if not already done
|
||||
// Don't auto-login on restore; user must use /login command
|
||||
// Just initialize empty conversation for the UI
|
||||
if (!this.agentInitialized) {
|
||||
console.log(
|
||||
'[WebViewProvider] Initializing agent connection after restore...',
|
||||
'[WebViewProvider] Agent not initialized after restore, waiting for /login command',
|
||||
);
|
||||
this.initializeAgentConnection().catch((error) => {
|
||||
this.initializeEmptyConversation().catch((error) => {
|
||||
console.error(
|
||||
'[WebViewProvider] Failed to initialize agent after restore:',
|
||||
'[WebViewProvider] Failed to initialize empty conversation after restore:',
|
||||
error,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -378,7 +378,42 @@ export const App: React.FC = () => {
|
||||
const inputElement = inputFieldRef.current;
|
||||
const currentText = inputElement.textContent || '';
|
||||
|
||||
if (item.type === 'file') {
|
||||
if (item.type === 'command') {
|
||||
// Handle /login command directly
|
||||
if (item.label === '/login') {
|
||||
// Clear input field
|
||||
inputElement.textContent = '';
|
||||
setInputText('');
|
||||
// Close completion
|
||||
completion.closeCompletion();
|
||||
// Send login command to extension
|
||||
vscode.postMessage({
|
||||
type: 'login',
|
||||
data: {},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// For other commands, replace entire input with command
|
||||
inputElement.textContent = item.label + ' ';
|
||||
setInputText(item.label + ' ');
|
||||
|
||||
// Move cursor to end
|
||||
setTimeout(() => {
|
||||
const range = document.createRange();
|
||||
const sel = window.getSelection();
|
||||
if (inputElement.firstChild) {
|
||||
range.setStart(inputElement.firstChild, (item.label + ' ').length);
|
||||
range.collapse(true);
|
||||
} else {
|
||||
range.selectNodeContents(inputElement);
|
||||
range.collapse(false);
|
||||
}
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
inputElement.focus();
|
||||
}, 10);
|
||||
} else if (item.type === 'file') {
|
||||
// Store file reference mapping
|
||||
const filePath = (item.value as string) || item.label;
|
||||
fileReferenceMap.current.set(item.label, filePath);
|
||||
@@ -441,32 +476,12 @@ export const App: React.FC = () => {
|
||||
inputElement.focus();
|
||||
}, 10);
|
||||
}
|
||||
} else if (item.type === 'command') {
|
||||
// Replace entire input with command
|
||||
inputElement.textContent = item.label + ' ';
|
||||
setInputText(item.label + ' ');
|
||||
|
||||
// Move cursor to end
|
||||
setTimeout(() => {
|
||||
const range = document.createRange();
|
||||
const sel = window.getSelection();
|
||||
if (inputElement.firstChild) {
|
||||
range.setStart(inputElement.firstChild, (item.label + ' ').length);
|
||||
range.collapse(true);
|
||||
} else {
|
||||
range.selectNodeContents(inputElement);
|
||||
range.collapse(false);
|
||||
}
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
inputElement.focus();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// Close completion
|
||||
completion.closeCompletion();
|
||||
},
|
||||
[completion],
|
||||
[completion, vscode],
|
||||
);
|
||||
|
||||
// Handle attach context button click (Cmd/Ctrl + /)
|
||||
@@ -642,6 +657,7 @@ export const App: React.FC = () => {
|
||||
// Listen for messages from extension
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
const message = event.data;
|
||||
// console.log('[App] Received message from extension:', message.type, message);
|
||||
|
||||
switch (message.type) {
|
||||
case 'conversationLoaded': {
|
||||
@@ -714,6 +730,18 @@ export const App: React.FC = () => {
|
||||
setIsWaitingForResponse(false);
|
||||
break;
|
||||
|
||||
// case 'notLoggedIn':
|
||||
// // Show not logged in message with login button
|
||||
// console.log('[App] Received notLoggedIn message:', message.data);
|
||||
// setIsStreaming(false);
|
||||
// setIsWaitingForResponse(false);
|
||||
// setNotLoggedInMessage(
|
||||
// (message.data as { message: string })?.message ||
|
||||
// 'Please login to start chatting.',
|
||||
// );
|
||||
// console.log('[App] Set notLoggedInMessage to:', (message.data as { message: string })?.message);
|
||||
// break;
|
||||
|
||||
case 'permissionRequest':
|
||||
// Show permission dialog
|
||||
handlePermissionRequest(message.data);
|
||||
@@ -987,6 +1015,21 @@ export const App: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a /login command
|
||||
if (inputText.trim() === '/login') {
|
||||
// Clear input field
|
||||
setInputText('');
|
||||
if (inputFieldRef.current) {
|
||||
inputFieldRef.current.textContent = '';
|
||||
}
|
||||
// Send login command to extension
|
||||
vscode.postMessage({
|
||||
type: 'login',
|
||||
data: {},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Set waiting state with random loading message
|
||||
setIsWaitingForResponse(true);
|
||||
setLoadingMessage(getRandomLoadingMessage());
|
||||
@@ -1380,6 +1423,24 @@ export const App: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Not Logged In Message with Login Button - COMMENTED OUT */}
|
||||
{/* {notLoggedInMessage && (
|
||||
<>
|
||||
{console.log('[App] Rendering NotLoggedInMessage with message:', notLoggedInMessage)}
|
||||
<NotLoggedInMessage
|
||||
message={notLoggedInMessage}
|
||||
onLoginClick={() => {
|
||||
setNotLoggedInMessage(null);
|
||||
vscode.postMessage({
|
||||
type: 'login',
|
||||
data: {},
|
||||
});
|
||||
}}
|
||||
onDismiss={() => setNotLoggedInMessage(null)}
|
||||
/>
|
||||
</>
|
||||
)} */}
|
||||
|
||||
{isStreaming && currentStreamContent && (
|
||||
<div className="message assistant streaming">
|
||||
<div className="message-content">
|
||||
|
||||
@@ -287,6 +287,11 @@
|
||||
/* Tool Call Styles */
|
||||
--app-tool-background: var(--vscode-editor-background);
|
||||
--app-code-background: var(--vscode-textCodeBlock-background);
|
||||
|
||||
/* Warning/Error Styles */
|
||||
--app-warning-background: var(--vscode-editorWarning-background, rgba(255, 204, 0, 0.1));
|
||||
--app-warning-border: var(--vscode-editorWarning-foreground, #ffcc00);
|
||||
--app-warning-foreground: var(--vscode-editorWarning-foreground, #ffcc00);
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
|
||||
@@ -27,6 +27,10 @@ export class MessageHandler {
|
||||
}) => void;
|
||||
// 当前消息列表
|
||||
private messages: ChatMessage[] = [];
|
||||
// 登录处理器
|
||||
private loginHandler?: () => Promise<void>;
|
||||
// 待发送消息(登录后自动重发)
|
||||
private pendingMessage: string | null = null;
|
||||
|
||||
constructor(
|
||||
private agentManager: QwenAgentManager,
|
||||
@@ -35,6 +39,13 @@ export class MessageHandler {
|
||||
private sendToWebView: (message: unknown) => void,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 设置登录处理器
|
||||
*/
|
||||
setLoginHandler(handler: () => Promise<void>): void {
|
||||
this.loginHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前对话 ID
|
||||
*/
|
||||
@@ -211,6 +222,10 @@ export class MessageHandler {
|
||||
await this.handleOpenSettings();
|
||||
break;
|
||||
|
||||
case 'login':
|
||||
await this.handleLogin();
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('[MessageHandler] Unknown message type:', message.type);
|
||||
break;
|
||||
@@ -316,13 +331,33 @@ export class MessageHandler {
|
||||
console.warn(
|
||||
'[MessageHandler] Agent is not connected, skipping AI response',
|
||||
);
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: {
|
||||
message:
|
||||
'Agent is not connected. Enable Qwen in settings or configure API key.',
|
||||
},
|
||||
});
|
||||
|
||||
// Save pending message for auto-retry after login
|
||||
this.pendingMessage = text;
|
||||
console.log(
|
||||
'[MessageHandler] Saved pending message for retry after login',
|
||||
);
|
||||
|
||||
// Show VSCode warning notification
|
||||
const result = await vscode.window.showWarningMessage(
|
||||
'You need to login first to use Qwen Code.',
|
||||
'Login Now',
|
||||
);
|
||||
|
||||
if (result === 'Login Now') {
|
||||
// Trigger login
|
||||
await this.handleLogin();
|
||||
}
|
||||
|
||||
// COMMENTED OUT: Send special error type to WebView for inline display
|
||||
// console.log('[MessageHandler] Sending notLoggedIn message to webview');
|
||||
// this.sendToWebView({
|
||||
// type: 'notLoggedIn',
|
||||
// data: {
|
||||
// message: 'Please login to start chatting with Qwen Code.',
|
||||
// },
|
||||
// });
|
||||
// console.log('[MessageHandler] notLoggedIn message sent');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -363,13 +398,46 @@ export class MessageHandler {
|
||||
console.log('[MessageHandler] Stream end sent');
|
||||
} catch (error) {
|
||||
console.error('[MessageHandler] Error sending message:', error);
|
||||
|
||||
// Check if error is due to no active ACP session (not logged in)
|
||||
const errorMsg = String(error);
|
||||
if (errorMsg.includes('No active ACP session')) {
|
||||
// Save pending message for auto-retry after login
|
||||
this.pendingMessage = text;
|
||||
console.log(
|
||||
'[MessageHandler] Saved pending message for retry after login',
|
||||
);
|
||||
|
||||
// Show VSCode warning notification with login option
|
||||
const result = await vscode.window.showWarningMessage(
|
||||
'You need to login first to use Qwen Code.',
|
||||
'Login Now',
|
||||
);
|
||||
|
||||
if (result === 'Login Now') {
|
||||
// Trigger login
|
||||
await this.handleLogin();
|
||||
}
|
||||
|
||||
// COMMENTED OUT: Send special error type to WebView for inline display with login button
|
||||
// console.log('[MessageHandler] Sending notLoggedIn message (session expired) to webview');
|
||||
// this.sendToWebView({
|
||||
// type: 'notLoggedIn',
|
||||
// data: {
|
||||
// message: 'Session expired. Please login to continue chatting.',
|
||||
// },
|
||||
// });
|
||||
// console.log('[MessageHandler] notLoggedIn message sent');
|
||||
} else {
|
||||
// For other errors, show regular error message
|
||||
vscode.window.showErrorMessage(`Error sending message: ${error}`);
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: { message: String(error) },
|
||||
data: { message: errorMsg },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理加载对话请求
|
||||
@@ -946,4 +1014,58 @@ export class MessageHandler {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录请求
|
||||
* 通过 /login 命令触发登录流程
|
||||
*/
|
||||
private async handleLogin(): Promise<void> {
|
||||
try {
|
||||
console.log('[MessageHandler] Login requested via /login command');
|
||||
|
||||
if (this.loginHandler) {
|
||||
// Show progress notification in VSCode
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: 'Logging in to Qwen Code...',
|
||||
cancellable: false,
|
||||
},
|
||||
async () => {
|
||||
await this.loginHandler!();
|
||||
},
|
||||
);
|
||||
console.log('[MessageHandler] Login completed successfully');
|
||||
|
||||
// Show success notification
|
||||
vscode.window.showInformationMessage(
|
||||
'Successfully logged in to Qwen Code!',
|
||||
);
|
||||
|
||||
// Auto-resend pending message if exists
|
||||
if (this.pendingMessage) {
|
||||
console.log(
|
||||
'[MessageHandler] Auto-resending pending message after login',
|
||||
);
|
||||
const messageToSend = this.pendingMessage;
|
||||
this.pendingMessage = null; // Clear pending message
|
||||
|
||||
// Resend the message
|
||||
await this.handleSendMessage(messageToSend);
|
||||
}
|
||||
} else {
|
||||
console.error('[MessageHandler] No login handler registered');
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: { message: 'Login handler not available' },
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MessageHandler] Login failed:', error);
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: { message: `Login failed: ${error}` },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,11 +98,13 @@ export const InfoBanner: React.FC<InfoBannerProps> = ({
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
className="flex items-center justify-center cursor-pointer p-1.5 rounded"
|
||||
className="flex items-center justify-center cursor-pointer rounded"
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: '6px',
|
||||
color: 'var(--app-secondary-foreground)',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor =
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
interface NotLoggedInMessageProps {
|
||||
/**
|
||||
* The message to display
|
||||
*/
|
||||
message: string;
|
||||
|
||||
/**
|
||||
* Callback when the login button is clicked
|
||||
*/
|
||||
onLoginClick: () => void;
|
||||
|
||||
/**
|
||||
* Callback when the message is dismissed (optional)
|
||||
*/
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
export const NotLoggedInMessage: React.FC<NotLoggedInMessageProps> = ({
|
||||
message,
|
||||
onLoginClick,
|
||||
onDismiss,
|
||||
}) => (
|
||||
<div
|
||||
className="flex items-start gap-3 p-4 my-4 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: 'var(--app-warning-background)',
|
||||
borderLeft: '3px solid var(--app-warning-border)',
|
||||
}}
|
||||
>
|
||||
{/* Warning Icon */}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
className="flex-shrink-0 w-5 h-5 mt-0.5"
|
||||
style={{ color: 'var(--app-warning-foreground)' }}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p
|
||||
className="m-0 mb-3 text-sm leading-relaxed"
|
||||
style={{ color: 'var(--app-primary-foreground)' }}
|
||||
>
|
||||
{message}
|
||||
</p>
|
||||
|
||||
{/* Login Button */}
|
||||
<button
|
||||
className="px-4 py-2 text-sm font-medium rounded transition-colors duration-200"
|
||||
style={{
|
||||
backgroundColor: 'var(--app-qwen-orange)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.opacity = '0.9';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.opacity = '1';
|
||||
}}
|
||||
onClick={() => {
|
||||
if (onDismiss) {
|
||||
onDismiss();
|
||||
}
|
||||
onLoginClick();
|
||||
}}
|
||||
>
|
||||
Login Now
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Optional Close Button */}
|
||||
{onDismiss && (
|
||||
<button
|
||||
className="flex-shrink-0 flex items-center justify-center cursor-pointer rounded"
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: '6px',
|
||||
color: 'var(--app-secondary-foreground)',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor =
|
||||
'var(--app-ghost-button-hover-background)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
}}
|
||||
aria-label="Dismiss"
|
||||
onClick={onDismiss}
|
||||
>
|
||||
<svg
|
||||
className="w-[10px] h-[10px]"
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1 1L13 13M1 13L13 1"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
Reference in New Issue
Block a user