wip(vscode-ide-companion): OnboardingPage

This commit is contained in:
yiliang114
2025-12-13 16:28:58 +08:00
parent 5841370b1a
commit bca288e742
12 changed files with 235 additions and 138 deletions

View File

@@ -25,17 +25,36 @@ export class CliDetector {
private static readonly CACHE_DURATION_MS = 30000; // 30 seconds private static readonly CACHE_DURATION_MS = 30000; // 30 seconds
/** /**
* Lightweight check if the Qwen Code CLI is installed * Lightweight CLI Detection Method
* This version only checks for CLI existence without getting version info for faster performance *
* @param forceRefresh - Force a new check, ignoring cache * This method is designed for performance optimization, checking only if the CLI exists
* @returns Detection result with installation status and path * without retrieving version information.
* Suitable for quick detection scenarios, such as pre-checks before initializing connections.
*
* Compared to the full detectQwenCli method, this method:
* - Omits version information retrieval step
* - Uses shorter timeout (3 seconds)
* - Faster response time
*
* @param forceRefresh - Whether to force refresh cached results, default is false
* @returns Promise<CliDetectionResult> - Detection result containing installation status and path
*
* @example
* ```typescript
* const result = await CliDetector.detectQwenCliLightweight();
* if (result.isInstalled) {
* console.log('CLI installed at:', result.cliPath);
* } else {
* console.log('CLI not found:', result.error);
* }
* ```
*/ */
static async detectQwenCliLightweight( static async detectQwenCliLightweight(
forceRefresh = false, forceRefresh = false,
): Promise<CliDetectionResult> { ): Promise<CliDetectionResult> {
const now = Date.now(); const now = Date.now();
// Return cached result if available and not expired // Check if cached result is available and not expired (30-second validity)
if ( if (
!forceRefresh && !forceRefresh &&
this.cachedResult && this.cachedResult &&
@@ -56,7 +75,7 @@ export class CliDetector {
// Check if qwen command exists // Check if qwen command exists
try { try {
// Use simpler detection without NVM for speed // Use simplified detection without NVM for speed
const detectionCommand = isWindows const detectionCommand = isWindows
? `${whichCommand} qwen` ? `${whichCommand} qwen`
: `${whichCommand} qwen`; : `${whichCommand} qwen`;
@@ -66,32 +85,34 @@ export class CliDetector {
detectionCommand, detectionCommand,
); );
// Execute command to detect CLI path, set shorter timeout (3 seconds)
const { stdout } = await execAsync(detectionCommand, { const { stdout } = await execAsync(detectionCommand, {
timeout: 3000, // Reduced timeout for faster detection timeout: 3000, // Reduced timeout for faster detection
shell: isWindows ? undefined : '/bin/bash', shell: isWindows ? undefined : '/bin/bash',
}); });
// The output may contain multiple lines // Output may contain multiple lines, get first line as actual path
// We want the first line which should be the actual path
const lines = stdout const lines = stdout
.trim() .trim()
.split('\n') .split('\n')
.filter((line) => line.trim()); .filter((line) => line.trim());
const cliPath = lines[0]; // Just take the first path const cliPath = lines[0]; // Take only the first path
console.log('[CliDetector] Found CLI at:', cliPath); console.log('[CliDetector] Found CLI at:', cliPath);
// Build successful detection result, note no version information
this.cachedResult = { this.cachedResult = {
isInstalled: true, isInstalled: true,
cliPath, cliPath,
// Version is not retrieved in lightweight detection // Version information not retrieved in lightweight detection
}; };
this.lastCheckTime = now; this.lastCheckTime = now;
return this.cachedResult; return this.cachedResult;
} catch (detectionError) { } catch (detectionError) {
console.log('[CliDetector] CLI not found, error:', detectionError); console.log('[CliDetector] CLI not found, error:', detectionError);
// CLI not found
let error = `Qwen Code CLI not found in PATH. Please install it using: npm install -g @qwen-code/qwen-code@latest`; // CLI not found, build error message
let error = `Qwen Code CLI not found in PATH. Please install using: npm install -g @qwen-code/qwen-code@latest`;
// Provide specific guidance for permission errors // Provide specific guidance for permission errors
if (detectionError instanceof Error) { if (detectionError instanceof Error) {
@@ -100,11 +121,11 @@ export class CliDetector {
errorMessage.includes('EACCES') || errorMessage.includes('EACCES') ||
errorMessage.includes('Permission denied') errorMessage.includes('Permission denied')
) { ) {
error += `\n\nThis may be due to permission issues. Possible solutions: error += `\n\nThis may be due to permission issues. Solutions:
\n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest \n1. Reinstall CLI without sudo: npm install -g @qwen-code/qwen-code@latest
\n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code
\n3. Use nvm for Node.js version management to avoid permission issues \n3. Use nvm for Node.js version management to avoid permission issues
\n4. Check your PATH environment variable includes npm's global bin directory`; \n4. Check PATH environment variable includes npm's global bin directory`;
} }
} }
@@ -127,11 +148,11 @@ export class CliDetector {
errorMessage.includes('EACCES') || errorMessage.includes('EACCES') ||
errorMessage.includes('Permission denied') errorMessage.includes('Permission denied')
) { ) {
userFriendlyError += `\n\nThis may be due to permission issues. Possible solutions: userFriendlyError += `\n\nThis may be due to permission issues. Solutions:
\n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest \n1. Reinstall CLI without sudo: npm install -g @qwen-code/qwen-code@latest
\n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code
\n3. Use nvm for Node.js version management to avoid permission issues \n3. Use nvm for Node.js version management to avoid permission issues
\n4. Check your PATH environment variable includes npm's global bin directory`; \n4. Check PATH environment variable includes npm's global bin directory`;
} }
this.cachedResult = { this.cachedResult = {

View File

@@ -0,0 +1,78 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import * as vscode from 'vscode';
import { CliContextManager } from './cliContextManager.js';
import { CliVersionManager } from './cliVersionManager.js';
import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from './cliVersionManager.js';
import type { CliVersionInfo } from './cliVersionManager.js';
/**
* Check CLI version and show warning if below minimum requirement
*
* @returns Version information
*/
export async function checkCliVersionAndWarn(): Promise<void> {
try {
const cliContextManager = CliContextManager.getInstance();
const versionInfo =
await CliVersionManager.getInstance().detectCliVersion(true);
cliContextManager.setCurrentVersionInfo(versionInfo);
if (!versionInfo.isSupported) {
vscode.window.showWarningMessage(
`Qwen Code CLI version ${versionInfo.version} is below the minimum required version. Some features may not work properly. Please upgrade to version ${MIN_CLI_VERSION_FOR_SESSION_METHODS} or later.`,
);
}
} catch (error) {
console.error('[CliVersionChecker] Failed to check CLI version:', error);
}
}
/**
* Process server version information from initialize response
*
* @param init - Initialize response object
*/
export function processServerVersion(init: unknown): void {
try {
const obj = (init || {}) as Record<string, unknown>;
// Extract version information from initialize response
const serverVersion =
obj['version'] || obj['serverVersion'] || obj['cliVersion'];
if (serverVersion) {
console.log(
'[CliVersionChecker] Server version from initialize response:',
serverVersion,
);
// Update CLI context with version info from server
const cliContextManager = CliContextManager.getInstance();
// Create version info directly without async call
const versionInfo: CliVersionInfo = {
version: String(serverVersion),
isSupported: true, // Assume supported for now
features: {
supportsSessionList: true,
supportsSessionLoad: true,
},
detectionResult: {
isInstalled: true,
version: String(serverVersion),
},
};
cliContextManager.setCurrentVersionInfo(versionInfo);
}
} catch (error) {
console.error(
'[CliVersionChecker] Failed to process server version:',
error,
);
}
}

View File

@@ -42,9 +42,7 @@ export class AcpConnection {
onPermissionRequest: (data: AcpPermissionRequest) => Promise<{ onPermissionRequest: (data: AcpPermissionRequest) => Promise<{
optionId: string; optionId: string;
}> = () => Promise.resolve({ optionId: 'allow' }); }> = () => Promise.resolve({ optionId: 'allow' });
onEndTurn: (reason?: string) => void = (reason?: string | undefined) => { onEndTurn: () => void = () => {};
console.log('[ACP] onEndTurn__________ reason:', reason || 'unknown');
};
// Called after successful initialize() with the initialize result // Called after successful initialize() with the initialize result
onInitialized: (init: unknown) => void = () => {}; onInitialized: (init: unknown) => void = () => {};

View File

@@ -111,16 +111,6 @@ export class AcpMessageHandler {
message.result, message.result,
); );
console.log(
'[ACP] Response for message.result:',
message.result,
message.result &&
typeof message.result === 'object' &&
'stopReason' in message.result,
!!callbacks.onEndTurn,
);
if (message.result && typeof message.result === 'object') { if (message.result && typeof message.result === 'object') {
const stopReasonValue = const stopReasonValue =
(message.result as { stopReason?: unknown }).stopReason ?? (message.result as { stopReason?: unknown }).stopReason ??

View File

@@ -24,10 +24,8 @@ import {
import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js'; import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js';
import { CliContextManager } from '../cli/cliContextManager.js'; import { CliContextManager } from '../cli/cliContextManager.js';
import { authMethod } from '../types/acpTypes.js'; import { authMethod } from '../types/acpTypes.js';
import { import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from '../cli/cliVersionManager.js';
MIN_CLI_VERSION_FOR_SESSION_METHODS, import { processServerVersion } from '../cli/cliVersionChecker.js';
type CliVersionInfo,
} from '../cli/cliVersionManager.js';
import { isAuthenticationRequiredError } from '../utils/authErrors.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js';
export type { ChatMessage, PlanEntry, ToolCallUpdateData }; export type { ChatMessage, PlanEntry, ToolCallUpdateData };
@@ -149,37 +147,10 @@ export class QwenAgentManager {
// Initialize callback to surface available modes and current mode to UI // Initialize callback to surface available modes and current mode to UI
this.connection.onInitialized = (init: unknown) => { this.connection.onInitialized = (init: unknown) => {
try { try {
// Process server version information
processServerVersion(init);
const obj = (init || {}) as Record<string, unknown>; const obj = (init || {}) as Record<string, unknown>;
// Extract version information from initialize response
const serverVersion =
obj['version'] || obj['serverVersion'] || obj['cliVersion'];
if (serverVersion) {
console.log(
'[QwenAgentManager] Server version from initialize response:',
serverVersion,
);
// Update CLI context with version info from server
const cliContextManager = CliContextManager.getInstance();
// Create version info directly without async call
const versionInfo: CliVersionInfo = {
version: String(serverVersion),
isSupported: true, // Assume supported for now
features: {
supportsSessionList: true,
supportsSessionLoad: true,
},
detectionResult: {
isInstalled: true,
version: String(serverVersion),
},
};
cliContextManager.setCurrentVersionInfo(versionInfo);
}
const modes = obj['modes'] as const modes = obj['modes'] as
| { | {
currentModeId?: 'plan' | 'default' | 'auto-edit' | 'yolo'; currentModeId?: 'plan' | 'default' | 'auto-edit' | 'yolo';
@@ -1369,7 +1340,6 @@ export class QwenAgentManager {
* @param callback - Called when ACP stopReason is reported * @param callback - Called when ACP stopReason is reported
*/ */
onEndTurn(callback: (reason?: string) => void): void { onEndTurn(callback: (reason?: string) => void): void {
console.log('[QwenAgentManager] onEndTurn__________ callback:', callback);
this.callbacks.onEndTurn = callback; this.callbacks.onEndTurn = callback;
this.sessionUpdateHandler.updateCallbacks(this.callbacks); this.sessionUpdateHandler.updateCallbacks(this.callbacks);
} }

View File

@@ -10,12 +10,12 @@
* Handles Qwen Agent connection establishment, authentication, and session creation * Handles Qwen Agent connection establishment, authentication, and session creation
*/ */
// 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 { CliDetector } from '../cli/cliDetector.js'; import { CliDetector } from '../cli/cliDetector.js';
import { authMethod } from '../types/acpTypes.js'; import { authMethod } from '../types/acpTypes.js';
import { isAuthenticationRequiredError } from '../utils/authErrors.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js';
import { checkCliVersionAndWarn } from '../cli/cliVersionChecker.js';
export interface QwenConnectionResult { export interface QwenConnectionResult {
sessionCreated: boolean; sessionCreated: boolean;
@@ -59,19 +59,12 @@ export class QwenConnectionHandler {
} }
console.log('[QwenAgentManager] CLI detected at:', detectionResult.cliPath); console.log('[QwenAgentManager] CLI detected at:', detectionResult.cliPath);
// TODO: @yiliang114. closed temporarily
// Show warning if CLI version is below minimum requirement // Show warning if CLI version is below minimum requirement
// if (!versionInfo.isSupported) { await checkCliVersionAndWarn();
// // Wait to determine release version number
// vscode.window.showWarningMessage(
// `Qwen Code CLI version ${versionInfo.version} is below the minimum required version. Some features may not work properly. Please upgrade to version ${MIN_CLI_VERSION_FOR_SESSION_METHODS} or later.`,
// );
// }
// Build extra CLI arguments (only essential parameters) // Build extra CLI arguments (only essential parameters)
const extraArgs: string[] = []; const extraArgs: string[] = [];
// TODO:
await connection.connect(cliPath!, workingDir, extraArgs); await connection.connect(cliPath!, workingDir, extraArgs);
// Try to restore existing session or create new session // Try to restore existing session or create new session

View File

@@ -4,23 +4,48 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
/**
* Authentication Error Utility
*
* Used to uniformly identify and handle various authentication-related error messages.
* Determines if re-authentication is needed by matching predefined error patterns.
*
* @param error - The error object or string to check
* @returns true if it's an authentication-related error, false otherwise
*/
const AUTH_ERROR_PATTERNS = [ const AUTH_ERROR_PATTERNS = [
'Authentication required', 'Authentication required', // Standard authentication request message
'(code: -32000)', '(code: -32000)', // RPC error code -32000 indicates authentication failure
'Unauthorized', 'Unauthorized', // HTTP unauthorized error
'Invalid token', 'Invalid token', // Invalid token
'Session expired', 'Session expired', // Session expired
]; ];
/**
* Determines if the given error is authentication-related
*
* This function detects various forms of authentication errors, including:
* - Direct error objects
* - String-form error messages
* - Other types of errors converted to strings for pattern matching
*
* @param error - The error object to check, can be an Error instance, string, or other type
* @returns boolean - true if the error is authentication-related, false otherwise
*/
export const isAuthenticationRequiredError = (error: unknown): boolean => { export const isAuthenticationRequiredError = (error: unknown): boolean => {
// Null check to avoid unnecessary processing
if (!error) { if (!error) {
return false; return false;
} }
// Extract error message text
const message = const message =
error instanceof Error error instanceof Error
? error.message ? error.message
: typeof error === 'string' : typeof error === 'string'
? error ? error
: String(error); : String(error);
// Match authentication-related errors using predefined patterns
return AUTH_ERROR_PATTERNS.some((pattern) => message.includes(pattern)); return AUTH_ERROR_PATTERNS.some((pattern) => message.includes(pattern));
}; };

View File

@@ -29,7 +29,7 @@ import { PermissionDrawer } from './components/PermissionDrawer/PermissionDrawer
import { ToolCall } from './components/messages/toolcalls/ToolCall.js'; import { ToolCall } from './components/messages/toolcalls/ToolCall.js';
import { hasToolCallOutput } from './components/messages/toolcalls/shared/utils.js'; import { hasToolCallOutput } from './components/messages/toolcalls/shared/utils.js';
import { EmptyState } from './components/layout/EmptyState.js'; import { EmptyState } from './components/layout/EmptyState.js';
import { OnboardingPage } from './components/layout/OnboardingPage.js'; import { Onboarding } from './components/layout/Onboarding.js';
import { type CompletionItem } from '../types/completionItemTypes.js'; import { type CompletionItem } from '../types/completionItemTypes.js';
import { useCompletionTrigger } from './hooks/useCompletionTrigger.js'; import { useCompletionTrigger } from './hooks/useCompletionTrigger.js';
import { ChatHeader } from './components/layout/ChatHeader.js'; import { ChatHeader } from './components/layout/ChatHeader.js';
@@ -670,7 +670,7 @@ export const App: React.FC = () => {
> >
{!hasContent ? ( {!hasContent ? (
isAuthenticated === false ? ( isAuthenticated === false ? (
<OnboardingPage <Onboarding
onLogin={() => { onLogin={() => {
vscode.postMessage({ type: 'login', data: {} }); vscode.postMessage({ type: 'login', data: {} });
messageHandling.setWaitingForResponse( messageHandling.setWaitingForResponse(

View File

@@ -17,6 +17,19 @@ import { getFileName } from './utils/webviewUtils.js';
import { type ApprovalModeValue } from '../types/acpTypes.js'; import { type ApprovalModeValue } from '../types/acpTypes.js';
import { isAuthenticationRequiredError } from '../utils/authErrors.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js';
/**
* WebView Provider Class
*
* Manages the WebView panel lifecycle, agent connection, and message handling.
* Acts as the central coordinator between VS Code extension and WebView UI.
*
* Key responsibilities:
* - WebView panel creation and management
* - Qwen agent connection and session management
* - Message routing between extension and WebView
* - Authentication state handling
* - Permission request processing
*/
export class WebViewProvider { export class WebViewProvider {
private panelManager: PanelManager; private panelManager: PanelManager;
private messageHandler: MessageHandler; private messageHandler: MessageHandler;
@@ -122,7 +135,6 @@ export class WebViewProvider {
// Setup end-turn handler from ACP stopReason notifications // Setup end-turn handler from ACP stopReason notifications
this.agentManager.onEndTurn((reason) => { this.agentManager.onEndTurn((reason) => {
console.log(' ============== ', reason);
// Ensure WebView exits streaming state even if no explicit streamEnd was emitted elsewhere // Ensure WebView exits streaming state even if no explicit streamEnd was emitted elsewhere
this.sendMessageToWebView({ this.sendMessageToWebView({
type: 'streamEnd', type: 'streamEnd',
@@ -521,6 +533,12 @@ export class WebViewProvider {
/** /**
* Attempt to restore authentication state and initialize connection * Attempt to restore authentication state and initialize connection
* This is called when the webview is first shown * This is called when the webview is first shown
*
* This method tries to establish a connection without forcing authentication,
* allowing detection of existing authentication state. If connection fails,
* initializes an empty conversation to allow browsing history.
*
* @returns Promise<void> - Resolves when auth state restoration attempt is complete
*/ */
private async attemptAuthStateRestoration(): Promise<void> { private async attemptAuthStateRestoration(): Promise<void> {
try { try {
@@ -550,6 +568,16 @@ export class WebViewProvider {
/** /**
* Internal: perform actual connection/initialization (no auth locking). * Internal: perform actual connection/initialization (no auth locking).
*
* This method handles the complete agent connection and initialization workflow:
* 1. Detects if Qwen CLI is installed
* 2. If CLI is not installed, prompts user for installation
* 3. If CLI is installed, attempts to connect to the agent
* 4. Handles authentication requirements and session creation
* 5. Notifies WebView of connection status
*
* @param options - Connection options including auto-authentication setting
* @returns Promise<void> - Resolves when initialization is complete
*/ */
private async doInitializeAgentConnection(options?: { private async doInitializeAgentConnection(options?: {
autoAuthenticate?: boolean; autoAuthenticate?: boolean;

View File

@@ -172,7 +172,7 @@ export const InputForm: React.FC<InputFormProps> = ({
<div <div
ref={inputFieldRef} ref={inputFieldRef}
contentEditable={'plaintext-only'} contentEditable="plaintext-only"
className="composer-input" className="composer-input"
role="textbox" role="textbox"
aria-label="Message input" aria-label="Message input"
@@ -186,9 +186,6 @@ export const InputForm: React.FC<InputFormProps> = ({
: 'false' : 'false'
} }
onInput={(e) => { onInput={(e) => {
if (composerDisabled) {
return;
}
const target = e.target as HTMLDivElement; const target = e.target as HTMLDivElement;
// Filter out zero-width space that we use to maintain height // Filter out zero-width space that we use to maintain height
const text = target.textContent?.replace(/\u200B/g, '') || ''; const text = target.textContent?.replace(/\u200B/g, '') || '';

View File

@@ -1,23 +1,30 @@
import type React from 'react'; /**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { generateIconUrl } from '../../utils/resourceUrl.js'; import { generateIconUrl } from '../../utils/resourceUrl.js';
interface OnboardingPageProps { interface OnboardingPageProps {
onLogin: () => void; onLogin: () => void;
} }
export const OnboardingPage: React.FC<OnboardingPageProps> = ({ onLogin }) => { export const Onboarding: React.FC<OnboardingPageProps> = ({ onLogin }) => {
const iconUri = generateIconUrl('icon.png'); const iconUri = generateIconUrl('icon.png');
return ( return (
<div className="flex flex-col items-center justify-center h-full p-5 md:p-10"> <div className="flex flex-col items-center justify-center h-full p-5 md:p-10">
<div className="flex flex-col items-center gap-8 w-full max-w-md"> <div className="flex flex-col items-center gap-8 w-full max-w-md">
<div className="flex flex-col items-center gap-6"> <div className="flex flex-col items-center gap-6">
{/* Application icon container with brand logo and decorative close icon */}
<div className="relative"> <div className="relative">
<img <img
src={iconUri} src={iconUri}
alt="Qwen Code Logo" alt="Qwen Code Logo"
className="w-[80px] h-[80px] object-contain" className="w-[80px] h-[80px] object-contain"
/> />
{/* Decorative close icon for enhanced visual effect */}
<div className="absolute -top-2 -right-2 w-6 h-6 bg-[#4f46e5] rounded-full flex items-center justify-center"> <div className="absolute -top-2 -right-2 w-6 h-6 bg-[#4f46e5] rounded-full flex items-center justify-center">
<svg width="12" height="12" viewBox="0 0 12 12" fill="white"> <svg width="12" height="12" viewBox="0 0 12 12" fill="white">
<path <path
@@ -30,6 +37,7 @@ export const OnboardingPage: React.FC<OnboardingPageProps> = ({ onLogin }) => {
</div> </div>
</div> </div>
{/* Text content area */}
<div className="text-center"> <div className="text-center">
<h1 className="text-2xl font-bold text-app-primary-foreground mb-2"> <h1 className="text-2xl font-bold text-app-primary-foreground mb-2">
Welcome to Qwen Code Welcome to Qwen Code
@@ -40,40 +48,12 @@ export const OnboardingPage: React.FC<OnboardingPageProps> = ({ onLogin }) => {
</p> </p>
</div> </div>
{/* <div className="flex flex-col gap-5 w-full">
<div className="bg-app-secondary-background rounded-xl p-5 border border-app-primary-border-color shadow-sm">
<h2 className="font-semibold text-app-primary-foreground mb-3">Get Started</h2>
<ul className="space-y-2">
<li className="flex items-start gap-2">
<div className="mt-1 w-1.5 h-1.5 rounded-full bg-[#4f46e5] flex-shrink-0"></div>
<span className="text-sm text-app-secondary-foreground">Understand complex codebases faster</span>
</li>
<li className="flex items-start gap-2">
<div className="mt-1 w-1.5 h-1.5 rounded-full bg-[#4f46e5] flex-shrink-0"></div>
<span className="text-sm text-app-secondary-foreground">Navigate with AI-powered suggestions</span>
</li>
<li className="flex items-start gap-2">
<div className="mt-1 w-1.5 h-1.5 rounded-full bg-[#4f46e5] flex-shrink-0"></div>
<span className="text-sm text-app-secondary-foreground">Transform code with confidence</span>
</li>
</ul>
</div>
</div> */}
<button <button
onClick={onLogin} onClick={onLogin}
className="w-full px-4 py-3 bg-[#4f46e5] text-white font-medium rounded-lg shadow-sm" className="w-full px-4 py-3 bg-[#4f46e5] text-white font-medium rounded-lg shadow-sm"
> >
Log in to Qwen Code Log in to Qwen Code
</button> </button>
{/* <div className="text-center">
<p className="text-xs text-app-secondary-foreground">
By logging in, you agree to the Terms of Service and Privacy
Policy.
</p>
</div> */}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -329,7 +329,6 @@ export const useWebViewMessages = ({
} }
} }
} }
console.log('[useWebViewMessages1111]__________ other message:', msg);
break; break;
} }
@@ -351,30 +350,42 @@ export const useWebViewMessages = ({
} }
case 'streamEnd': { case 'streamEnd': {
// Always end local streaming state and collapse any thoughts // Always end local streaming state and clear thinking state
handlers.messageHandling.endStreaming(); handlers.messageHandling.endStreaming();
handlers.messageHandling.clearThinking(); handlers.messageHandling.clearThinking();
// If the stream ended due to explicit user cancel, proactively // If stream ended due to explicit user cancellation, proactively clear
// clear the waiting indicator and reset any tracked exec calls. // waiting indicator and reset tracked execution calls.
// This avoids the UI being stuck with the Stop button visible // This avoids UI getting stuck with Stop button visible after
// after rejecting a permission request. // rejecting a permission request.
try { try {
const reason = ( const reason = (
(message.data as { reason?: string } | undefined)?.reason || '' (message.data as { reason?: string } | undefined)?.reason || ''
).toLowerCase(); ).toLowerCase();
/**
* Handle different types of stream end reasons:
* - 'user_cancelled': User explicitly cancelled operation
* - 'cancelled': General cancellation
* For these cases, immediately clear all active states
*/
if (reason === 'user_cancelled' || reason === 'cancelled') { if (reason === 'user_cancelled' || reason === 'cancelled') {
// Clear active execution tool call tracking, reset state
activeExecToolCallsRef.current.clear(); activeExecToolCallsRef.current.clear();
// Clear waiting response state to ensure UI returns to normal
handlers.messageHandling.clearWaitingForResponse(); handlers.messageHandling.clearWaitingForResponse();
break; break;
} }
} catch (_error) { } catch (_error) {
// best-effort // Best-effort handling, errors don't affect main flow
} }
// Otherwise, clear the generic waiting indicator only if there are /**
// no active long-running tool calls. If there are still active * For other types of stream end (non-user cancellation):
// execute/bash/command calls, keep the hint visible. * Only clear generic waiting indicator when there are no active
* long-running tool calls. If there are still active execute/bash/command
* calls, keep the hint visible.
*/
if (activeExecToolCallsRef.current.size === 0) { if (activeExecToolCallsRef.current.size === 0) {
handlers.messageHandling.clearWaitingForResponse(); handlers.messageHandling.clearWaitingForResponse();
} }
@@ -575,15 +586,21 @@ export const useWebViewMessages = ({
// While long-running tools (e.g., execute/bash/command) are in progress, // While long-running tools (e.g., execute/bash/command) are in progress,
// surface a lightweight loading indicator and expose the Stop button. // surface a lightweight loading indicator and expose the Stop button.
try { try {
const kind = (toolCallData.kind || '').toString().toLowerCase();
const isExec =
kind === 'execute' || kind === 'bash' || kind === 'command';
if (isExec) {
const id = (toolCallData.toolCallId || '').toString(); const id = (toolCallData.toolCallId || '').toString();
const kind = (toolCallData.kind || '').toString().toLowerCase();
const isExecKind =
kind === 'execute' || kind === 'bash' || kind === 'command';
// CLI sometimes omits kind in tool_call_update payloads; fall back to
// whether we've already tracked this ID as an exec tool.
const wasTrackedExec = activeExecToolCallsRef.current.has(id);
const isExec = isExecKind || wasTrackedExec;
if (!isExec || !id) {
break;
}
// Maintain the active set by status
if (status === 'pending' || status === 'in_progress') { if (status === 'pending' || status === 'in_progress') {
if (isExecKind) {
activeExecToolCallsRef.current.add(id); activeExecToolCallsRef.current.add(id);
// Build a helpful hint from rawInput // Build a helpful hint from rawInput
@@ -597,6 +614,7 @@ export const useWebViewMessages = ({
} }
const hint = cmd ? `Running: ${cmd}` : 'Running command...'; const hint = cmd ? `Running: ${cmd}` : 'Running command...';
handlers.messageHandling.setWaitingForResponse(hint); handlers.messageHandling.setWaitingForResponse(hint);
}
} else if (status === 'completed' || status === 'failed') { } else if (status === 'completed' || status === 'failed') {
activeExecToolCallsRef.current.delete(id); activeExecToolCallsRef.current.delete(id);
} }
@@ -605,7 +623,6 @@ export const useWebViewMessages = ({
if (activeExecToolCallsRef.current.size === 0) { if (activeExecToolCallsRef.current.size === 0) {
handlers.messageHandling.clearWaitingForResponse(); handlers.messageHandling.clearWaitingForResponse();
} }
}
} catch (_error) { } catch (_error) {
// Best-effort UI hint; ignore errors // Best-effort UI hint; ignore errors
} }