mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
wip(vscode-ide-companion): OnboardingPage
This commit is contained in:
@@ -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 = {
|
||||||
|
|||||||
78
packages/vscode-ide-companion/src/cli/cliVersionChecker.ts
Normal file
78
packages/vscode-ide-companion/src/cli/cliVersionChecker.ts
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 = () => {};
|
||||||
|
|
||||||
|
|||||||
@@ -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 ??
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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, '') || '';
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user