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:
yiliang114
2025-11-23 19:20:01 +08:00
parent e5729b0420
commit c4bcd178a4
6 changed files with 369 additions and 43 deletions

View File

@@ -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">