Compare commits

..

16 Commits

Author SHA1 Message Date
github-actions[bot]
60e4bdf216 chore(release): v0.5.0-nightly.20251213.8b29dd13 2025-12-13 00:23:53 +00:00
tanzhenxin
8b29dd130e Merge pull request #1233 from QwenLM/chore/v0.5.0
pump version to 0.5.0
2025-12-12 16:32:13 +08:00
tanzhenxin
d0be8b43d7 pump version to 0.5.0 2025-12-12 16:29:50 +08:00
tanzhenxin
3095442eb3 Merge pull request #1223 from QwenLM/fix/vscode-ide-companion-login-twice
fix(vscode-ide-companion/auth): deduplicate concurrent authentication calls
2025-12-12 16:19:25 +08:00
tanzhenxin
2ceecab503 Merge pull request #1226 from QwenLM/feat/support-channel-field
feat: Add channel field support for client identification
2025-12-12 16:16:36 +08:00
pomelo
e5ed0334ab Merge pull request #1230 from BlockHand/docker-ide 2025-12-12 16:16:23 +08:00
刘伟光
2b62b1e8bc feat: 将注释修改成英文 2025-12-12 14:40:30 +08:00
yiliang114
89be6edb5e chore(vscode-ide-companion): add comment 2025-12-12 13:59:05 +08:00
yiliang114
d812c9dcf2 chore(vscode-ide-companion): add fixme comment for auth delay 2025-12-12 13:51:14 +08:00
yiliang114
d754767e73 chore(vscode-ide-companion): rm authState manager in vscode-ide-companion to simplify the login architecture 2025-12-12 13:40:18 +08:00
刘伟光
bb8447edd7 fix: 修复在docker环境中无法连接ide的问题 2025-12-12 11:36:15 +08:00
yiliang114
02234f5434 chore(vscode-ide-companion): change comments and delays 2025-12-12 01:17:38 +08:00
yiliang114
25261ab88d fix(vscode-ide-companion): slight delay to ensure auth state settlement 2025-12-12 01:14:28 +08:00
DragonnZhang
60a58ad8e5 feat: add support for the channel field to CLI parameters and configurations 2025-12-12 01:06:00 +08:00
yiliang114
c20df192a8 chore(vscode-ide-companion): revert some log util, will continue next time 2025-12-11 23:57:21 +08:00
yiliang114
b34894c8ea feat(vscode-ide-companion/auth): deduplicate concurrent authentication calls
Prevent multiple simultaneous authentication flows by:
- Adding static authInFlight promise tracking in AcpConnection
- Implementing runExclusiveAuth method in AuthStateManager
- Adding sessionCreateInFlight tracking in QwenAgentManager
- Ensuring only one auth flow runs at a time across different components

This prevents race conditions and duplicate login prompts when multiple components request authentication simultaneously.
2025-12-11 22:56:58 +08:00
27 changed files with 338 additions and 881 deletions

14
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.4.1-nightly.20251212.58d3a9c2",
"version": "0.5.0-nightly.20251213.8b29dd13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qwen-code/qwen-code",
"version": "0.4.1-nightly.20251212.58d3a9c2",
"version": "0.5.0-nightly.20251213.8b29dd13",
"workspaces": [
"packages/*"
],
@@ -17493,7 +17493,7 @@
},
"packages/cli": {
"name": "@qwen-code/qwen-code",
"version": "0.4.1-nightly.20251212.58d3a9c2",
"version": "0.5.0-nightly.20251213.8b29dd13",
"dependencies": {
"@google/genai": "1.16.0",
"@iarna/toml": "^2.2.5",
@@ -17608,7 +17608,7 @@
},
"packages/core": {
"name": "@qwen-code/qwen-code-core",
"version": "0.4.1-nightly.20251212.58d3a9c2",
"version": "0.5.0-nightly.20251213.8b29dd13",
"hasInstallScript": true,
"dependencies": {
"@google/genai": "1.16.0",
@@ -17748,7 +17748,7 @@
},
"packages/sdk-typescript": {
"name": "@qwen-code/sdk",
"version": "0.4.1-nightly.20251212.58d3a9c2",
"version": "0.5.0-nightly.20251213.8b29dd13",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4"
@@ -20177,7 +20177,7 @@
},
"packages/test-utils": {
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.4.1-nightly.20251212.58d3a9c2",
"version": "0.5.0-nightly.20251213.8b29dd13",
"dev": true,
"license": "Apache-2.0",
"devDependencies": {
@@ -20189,7 +20189,7 @@
},
"packages/vscode-ide-companion": {
"name": "qwen-code-vscode-ide-companion",
"version": "0.4.1-nightly.20251212.58d3a9c2",
"version": "0.5.0-nightly.20251213.8b29dd13",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.4.1-nightly.20251212.58d3a9c2",
"version": "0.5.0-nightly.20251213.8b29dd13",
"engines": {
"node": ">=20.0.0"
},
@@ -13,7 +13,7 @@
"url": "git+https://github.com/QwenLM/qwen-code.git"
},
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.4.1-nightly.20251212.58d3a9c2"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.5.0-nightly.20251213.8b29dd13"
},
"scripts": {
"start": "cross-env node scripts/start.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.4.1-nightly.20251212.58d3a9c2",
"version": "0.5.0-nightly.20251213.8b29dd13",
"description": "Qwen Code",
"repository": {
"type": "git",
@@ -33,7 +33,7 @@
"dist"
],
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.4.1-nightly.20251212.58d3a9c2"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.5.0-nightly.20251213.8b29dd13"
},
"dependencies": {
"@google/genai": "1.16.0",

View File

@@ -138,6 +138,7 @@ export interface CliArgs {
coreTools: string[] | undefined;
excludeTools: string[] | undefined;
authType: string | undefined;
channel: string | undefined;
}
function normalizeOutputFormat(
@@ -297,6 +298,11 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
type: 'boolean',
description: 'Starts the agent in ACP mode',
})
.option('channel', {
type: 'string',
choices: ['VSCode', 'ACP', 'SDK', 'CI'],
description: 'Channel identifier (VSCode, ACP, SDK, CI)',
})
.option('allowed-mcp-server-names', {
type: 'array',
string: true,
@@ -559,6 +565,12 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
// The import format is now only controlled by settings.memoryImportFormat
// We no longer accept it as a CLI argument
// Apply ACP fallback: if experimental-acp is present but no explicit --channel, treat as ACP
if (result['experimentalAcp'] && !result['channel']) {
(result as Record<string, unknown>)['channel'] = 'ACP';
}
return result as unknown as CliArgs;
}
@@ -983,6 +995,7 @@ export async function loadCliConfig(
output: {
format: outputSettingsFormat,
},
channel: argv.channel,
});
}

View File

@@ -485,6 +485,7 @@ describe('gemini.tsx main function kitty protocol', () => {
excludeTools: undefined,
authType: undefined,
maxSessionTurns: undefined,
channel: undefined,
});
await main();

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-core",
"version": "0.4.1-nightly.20251212.58d3a9c2",
"version": "0.5.0-nightly.20251213.8b29dd13",
"description": "Qwen Code Core",
"repository": {
"type": "git",

View File

@@ -349,6 +349,7 @@ export interface ConfigParameters {
skipStartupContext?: boolean;
sdkMode?: boolean;
sessionSubagents?: SubagentConfig[];
channel?: string;
}
function normalizeConfigOutputFormat(
@@ -485,6 +486,7 @@ export class Config {
private readonly enableToolOutputTruncation: boolean;
private readonly eventEmitter?: EventEmitter;
private readonly useSmartEdit: boolean;
private readonly channel: string | undefined;
constructor(params: ConfigParameters) {
this.sessionId = params.sessionId ?? randomUUID();
@@ -598,6 +600,7 @@ export class Config {
this.enableToolOutputTruncation = params.enableToolOutputTruncation ?? true;
this.useSmartEdit = params.useSmartEdit ?? false;
this.extensionManagement = params.extensionManagement ?? true;
this.channel = params.channel;
this.storage = new Storage(this.targetDir);
this.vlmSwitchMode = params.vlmSwitchMode;
this.inputFormat = params.inputFormat ?? InputFormat.TEXT;
@@ -1144,6 +1147,10 @@ export class Config {
return this.cliVersion;
}
getChannel(): string | undefined {
return this.channel;
}
/**
* Get the current FileSystemService
*/

View File

@@ -130,10 +130,13 @@ export class DashScopeOpenAICompatibleProvider
}
buildMetadata(userPromptId: string): DashScopeRequestMetadata {
const channel = this.cliConfig.getChannel?.();
return {
metadata: {
sessionId: this.cliConfig.getSessionId?.(),
promptId: userPromptId,
...(channel ? { channel } : {}),
},
};
}

View File

@@ -28,5 +28,6 @@ export type DashScopeRequestMetadata = {
metadata: {
sessionId?: string;
promptId: string;
channel?: string;
};
};

View File

@@ -249,6 +249,9 @@ export class QwenLogger {
authType === AuthType.USE_OPENAI
? this.config?.getContentGeneratorConfig().baseUrl || ''
: '',
...(this.config?.getChannel?.()
? { channel: this.config.getChannel() }
: {}),
},
_v: `qwen-code@${version}`,
} as RumPayload;

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/sdk",
"version": "0.4.1-nightly.20251212.58d3a9c2",
"version": "0.5.0-nightly.20251213.8b29dd13",
"description": "TypeScript SDK for programmatic access to qwen-code CLI",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",

View File

@@ -139,6 +139,7 @@ export class ProcessTransport implements Transport {
'stream-json',
'--output-format',
'stream-json',
'--channel=SDK',
];
if (this.options.model) {

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.4.1-nightly.20251212.58d3a9c2",
"version": "0.5.0-nightly.20251213.8b29dd13",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",

View File

@@ -2,7 +2,7 @@
"name": "qwen-code-vscode-ide-companion",
"displayName": "Qwen Code Companion",
"description": "Enable Qwen Code with direct access to your VS Code workspace.",
"version": "0.4.1-nightly.20251212.58d3a9c2",
"version": "0.5.0-nightly.20251213.8b29dd13",
"publisher": "qwenlm",
"icon": "assets/icon.png",
"repository": {

View File

@@ -7,7 +7,7 @@
import semver from 'semver';
import { CliDetector, type CliDetectionResult } from './cliDetector.js';
export const MIN_CLI_VERSION_FOR_SESSION_METHODS = '0.4.0';
export const MIN_CLI_VERSION_FOR_SESSION_METHODS = '0.5.0';
export interface CliFeatureFlags {
supportsSessionList: boolean;

View File

@@ -164,6 +164,7 @@ export class IDEServer {
const allowedHosts = [
`localhost:${this.port}`,
`127.0.0.1:${this.port}`,
`host.docker.internal:${this.port}`, // Add Docker support
];
if (!allowedHosts.includes(host)) {
return res.status(403).json({ error: 'Invalid Host header' });

View File

@@ -94,7 +94,12 @@ export class AcpConnection {
if (cliPath.startsWith('npx ')) {
const parts = cliPath.split(' ');
spawnCommand = isWindows ? 'npx.cmd' : 'npx';
spawnArgs = [...parts.slice(1), '--experimental-acp', ...extraArgs];
spawnArgs = [
...parts.slice(1),
'--experimental-acp',
'--channel=VSCode',
...extraArgs,
];
} else {
// For qwen CLI, ensure we use the correct Node.js version
// Handle various Node.js version managers (nvm, n, manual installations)
@@ -103,11 +108,16 @@ export class AcpConnection {
const nodePathResult = determineNodePathForCli(cliPath);
if (nodePathResult.path) {
spawnCommand = nodePathResult.path;
spawnArgs = [cliPath, '--experimental-acp', ...extraArgs];
spawnArgs = [
cliPath,
'--experimental-acp',
'--channel=VSCode',
...extraArgs,
];
} else {
// Fallback to direct execution
spawnCommand = cliPath;
spawnArgs = ['--experimental-acp', ...extraArgs];
spawnArgs = ['--experimental-acp', '--channel=VSCode', ...extraArgs];
// Log any error for debugging
if (nodePathResult.error) {
@@ -118,7 +128,7 @@ export class AcpConnection {
}
} else {
spawnCommand = cliPath;
spawnArgs = ['--experimental-acp', ...extraArgs];
spawnArgs = ['--experimental-acp', '--channel=VSCode', ...extraArgs];
}
}

View File

@@ -1,215 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type * as vscode from 'vscode';
interface AuthState {
isAuthenticated: boolean;
authMethod: string;
timestamp: number;
workingDir?: string;
}
/**
* Manages authentication state caching to avoid repeated logins
*/
export class AuthStateManager {
private static instance: AuthStateManager | null = null;
private static context: vscode.ExtensionContext | null = null;
private static readonly AUTH_STATE_KEY = 'qwen.authState';
private static readonly AUTH_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
private constructor() {}
/**
* Get singleton instance of AuthStateManager
*/
static getInstance(context?: vscode.ExtensionContext): AuthStateManager {
if (!AuthStateManager.instance) {
AuthStateManager.instance = new AuthStateManager();
}
// If a context is provided, update the static context
if (context) {
AuthStateManager.context = context;
}
return AuthStateManager.instance;
}
/**
* Check if there's a valid cached authentication
*/
async hasValidAuth(workingDir: string, authMethod: string): Promise<boolean> {
const state = await this.getAuthState();
if (!state) {
console.log('[AuthStateManager] No cached auth state found');
return false;
}
console.log('[AuthStateManager] Found cached auth state:', {
workingDir: state.workingDir,
authMethod: state.authMethod,
timestamp: new Date(state.timestamp).toISOString(),
isAuthenticated: state.isAuthenticated,
});
console.log('[AuthStateManager] Checking against:', {
workingDir,
authMethod,
});
// Check if auth is still valid (within cache duration)
const now = Date.now();
const isExpired =
now - state.timestamp > AuthStateManager.AUTH_CACHE_DURATION;
if (isExpired) {
console.log('[AuthStateManager] Cached auth expired');
console.log(
'[AuthStateManager] Cache age:',
Math.floor((now - state.timestamp) / 1000 / 60),
'minutes',
);
await this.clearAuthState();
return false;
}
// Check if it's for the same working directory and auth method
const isSameContext =
state.workingDir === workingDir && state.authMethod === authMethod;
if (!isSameContext) {
console.log('[AuthStateManager] Working dir or auth method changed');
console.log('[AuthStateManager] Cached workingDir:', state.workingDir);
console.log('[AuthStateManager] Current workingDir:', workingDir);
console.log('[AuthStateManager] Cached authMethod:', state.authMethod);
console.log('[AuthStateManager] Current authMethod:', authMethod);
return false;
}
console.log('[AuthStateManager] Valid cached auth found');
return state.isAuthenticated;
}
/**
* Force check auth state without clearing cache
* This is useful for debugging to see what's actually cached
*/
async debugAuthState(): Promise<void> {
const state = await this.getAuthState();
console.log('[AuthStateManager] DEBUG - Current auth state:', state);
if (state) {
const now = Date.now();
const age = Math.floor((now - state.timestamp) / 1000 / 60);
const isExpired =
now - state.timestamp > AuthStateManager.AUTH_CACHE_DURATION;
console.log('[AuthStateManager] DEBUG - Auth state age:', age, 'minutes');
console.log('[AuthStateManager] DEBUG - Auth state expired:', isExpired);
console.log(
'[AuthStateManager] DEBUG - Auth state valid:',
state.isAuthenticated,
);
}
}
/**
* Save successful authentication state
*/
async saveAuthState(workingDir: string, authMethod: string): Promise<void> {
// Ensure we have a valid context
if (!AuthStateManager.context) {
throw new Error(
'[AuthStateManager] No context available for saving auth state',
);
}
const state: AuthState = {
isAuthenticated: true,
authMethod,
workingDir,
timestamp: Date.now(),
};
console.log('[AuthStateManager] Saving auth state:', {
workingDir,
authMethod,
timestamp: new Date(state.timestamp).toISOString(),
});
await AuthStateManager.context.globalState.update(
AuthStateManager.AUTH_STATE_KEY,
state,
);
console.log('[AuthStateManager] Auth state saved');
// Verify the state was saved correctly
const savedState = await this.getAuthState();
console.log('[AuthStateManager] Verified saved state:', savedState);
}
/**
* Clear authentication state
*/
async clearAuthState(): Promise<void> {
// Ensure we have a valid context
if (!AuthStateManager.context) {
throw new Error(
'[AuthStateManager] No context available for clearing auth state',
);
}
console.log('[AuthStateManager] Clearing auth state');
const currentState = await this.getAuthState();
console.log(
'[AuthStateManager] Current state before clearing:',
currentState,
);
await AuthStateManager.context.globalState.update(
AuthStateManager.AUTH_STATE_KEY,
undefined,
);
console.log('[AuthStateManager] Auth state cleared');
// Verify the state was cleared
const newState = await this.getAuthState();
console.log('[AuthStateManager] State after clearing:', newState);
}
/**
* Get current auth state
*/
private async getAuthState(): Promise<AuthState | undefined> {
// Ensure we have a valid context
if (!AuthStateManager.context) {
console.log(
'[AuthStateManager] No context available for getting auth state',
);
return undefined;
}
const a = AuthStateManager.context.globalState.get<AuthState>(
AuthStateManager.AUTH_STATE_KEY,
);
console.log('[AuthStateManager] Auth state:', a);
return a;
}
/**
* Get auth state info for debugging
*/
async getAuthInfo(): Promise<string> {
const state = await this.getAuthState();
if (!state) {
return 'No cached auth';
}
const age = Math.floor((Date.now() - state.timestamp) / 1000 / 60);
return `Auth cached ${age}m ago, method: ${state.authMethod}`;
}
}

View File

@@ -11,7 +11,6 @@ import type {
} from '../types/acpTypes.js';
import { QwenSessionReader, type QwenSession } from './qwenSessionReader.js';
import { QwenSessionManager } from './qwenSessionManager.js';
import type { AuthStateManager } from './authStateManager.js';
import type {
ChatMessage,
PlanEntry,
@@ -42,9 +41,9 @@ export class QwenAgentManager {
// session/update notifications. We set this flag to route message chunks
// (user/assistant) as discrete chat messages instead of live streaming.
private rehydratingSessionId: string | null = null;
// Cache the last used AuthStateManager so internal calls (e.g. fallback paths)
// can reuse it and avoid forcing a fresh authentication unnecessarily.
private defaultAuthStateManager?: AuthStateManager;
// CLI is now the single source of truth for authentication state
// Deduplicate concurrent session/new attempts
private sessionCreateInFlight: Promise<string | null> | null = null;
// Callback storage
private callbacks: QwenAgentCallbacks = {};
@@ -163,22 +162,14 @@ export class QwenAgentManager {
* Connect to Qwen service
*
* @param workingDir - Working directory
* @param authStateManager - Authentication state manager (optional)
* @param cliPath - CLI path (optional, if provided will override the path in configuration)
*/
async connect(
workingDir: string,
authStateManager?: AuthStateManager,
_cliPath?: string,
): Promise<void> {
async connect(workingDir: string, _cliPath?: string): Promise<void> {
this.currentWorkingDir = workingDir;
// Remember the provided authStateManager for future calls
this.defaultAuthStateManager = authStateManager;
await this.connectionHandler.connect(
this.connection,
this.sessionReader,
workingDir,
authStateManager,
_cliPath,
);
}
@@ -1179,97 +1170,62 @@ export class QwenAgentManager {
* @param workingDir - Working directory
* @returns Newly created session ID
*/
async createNewSession(
workingDir: string,
authStateManager?: AuthStateManager,
): Promise<string | null> {
async createNewSession(workingDir: string): Promise<string | null> {
// Reuse existing session if present
if (this.connection.currentSessionId) {
return this.connection.currentSessionId;
}
// Deduplicate concurrent session/new attempts
if (this.sessionCreateInFlight) {
return this.sessionCreateInFlight;
}
console.log('[QwenAgentManager] Creating new session...');
// Check if we have valid cached authentication
let hasValidAuth = false;
// Prefer the provided authStateManager, otherwise fall back to the one
// remembered during connect(). This prevents accidental re-auth in
// fallback paths (e.g. session switching) when the handler didn't pass it.
const effectiveAuth = authStateManager || this.defaultAuthStateManager;
if (effectiveAuth) {
hasValidAuth = await effectiveAuth.hasValidAuth(workingDir, authMethod);
console.log(
'[QwenAgentManager] Has valid cached auth for new session:',
hasValidAuth,
);
}
// Only authenticate if we don't have valid cached auth
if (!hasValidAuth) {
console.log(
'[QwenAgentManager] Authenticating before creating session...',
);
this.sessionCreateInFlight = (async () => {
try {
await this.connection.authenticate(authMethod);
console.log('[QwenAgentManager] Authentication successful');
// Save auth state
if (effectiveAuth) {
console.log(
'[QwenAgentManager] Saving auth state after successful authentication',
);
await effectiveAuth.saveAuthState(workingDir, authMethod);
}
} catch (authError) {
console.error('[QwenAgentManager] Authentication failed:', authError);
// Clear potentially invalid cache
if (effectiveAuth) {
console.log(
'[QwenAgentManager] Clearing auth cache due to authentication failure',
);
await effectiveAuth.clearAuthState();
}
throw authError;
}
} else {
console.log(
'[QwenAgentManager] Skipping authentication - using valid cached auth',
);
}
// Try to create a new ACP session. If Qwen asks for auth despite our
// cached flag (e.g. fresh process or expired tokens), re-authenticate and retry.
try {
await this.connection.newSession(workingDir);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
const requiresAuth =
msg.includes('Authentication required') ||
msg.includes('(code: -32000)');
if (requiresAuth) {
console.warn(
'[QwenAgentManager] session/new requires authentication. Retrying with authenticate...',
);
// Try to create a new ACP session. If Qwen asks for auth, let it handle authentication.
try {
await this.connection.authenticate(authMethod);
// Persist auth cache so subsequent calls can skip the web flow.
if (effectiveAuth) {
await effectiveAuth.saveAuthState(workingDir, authMethod);
}
await this.connection.newSession(workingDir);
} catch (reauthErr) {
// Clear potentially stale cache on failure and rethrow
if (effectiveAuth) {
await effectiveAuth.clearAuthState();
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
const requiresAuth =
msg.includes('Authentication required') ||
msg.includes('(code: -32000)');
if (requiresAuth) {
console.warn(
'[QwenAgentManager] session/new requires authentication. Retrying with authenticate...',
);
try {
// Let CLI handle authentication - it's the single source of truth
await this.connection.authenticate(authMethod);
// Add a slight delay to ensure auth state is settled
await new Promise((resolve) => setTimeout(resolve, 300));
await this.connection.newSession(workingDir);
} catch (reauthErr) {
console.error(
'[QwenAgentManager] Re-authentication failed:',
reauthErr,
);
throw reauthErr;
}
} else {
throw err;
}
throw reauthErr;
}
} else {
throw err;
const newSessionId = this.connection.currentSessionId;
console.log(
'[QwenAgentManager] New session created with ID:',
newSessionId,
);
return newSessionId;
} finally {
this.sessionCreateInFlight = null;
}
}
const newSessionId = this.connection.currentSessionId;
console.log(
'[QwenAgentManager] New session created with ID:',
newSessionId,
);
return newSessionId;
})();
return this.sessionCreateInFlight;
}
/**

View File

@@ -13,7 +13,6 @@
import * as vscode from 'vscode';
import type { AcpConnection } from './acpConnection.js';
import type { QwenSessionReader } from '../services/qwenSessionReader.js';
import type { AuthStateManager } from '../services/authStateManager.js';
import {
CliVersionManager,
MIN_CLI_VERSION_FOR_SESSION_METHODS,
@@ -32,14 +31,12 @@ export class QwenConnectionHandler {
* @param connection - ACP connection instance
* @param sessionReader - Session reader instance
* @param workingDir - Working directory
* @param authStateManager - Authentication state manager (optional)
* @param cliPath - CLI path (optional, if provided will override the path in configuration)
*/
async connect(
connection: AcpConnection,
sessionReader: QwenSessionReader,
workingDir: string,
authStateManager?: AuthStateManager,
cliPath?: string,
): Promise<void> {
const connectId = Date.now();
@@ -72,21 +69,6 @@ export class QwenConnectionHandler {
await connection.connect(effectiveCliPath, workingDir, extraArgs);
// Check if we have valid cached authentication
if (authStateManager) {
console.log('[QwenAgentManager] Checking for cached authentication...');
console.log('[QwenAgentManager] Working dir:', workingDir);
console.log('[QwenAgentManager] Auth method:', authMethod);
const hasValidAuth = await authStateManager.hasValidAuth(
workingDir,
authMethod,
);
console.log('[QwenAgentManager] Has valid auth:', hasValidAuth);
} else {
console.log('[QwenAgentManager] No authStateManager provided');
}
// Try to restore existing session or create new session
// Note: Auto-restore on connect is disabled to avoid surprising loads
// when user opens a "New Chat" tab. Restoration is now an explicit action
@@ -99,81 +81,15 @@ export class QwenConnectionHandler {
'[QwenAgentManager] no sessionRestored, Creating new session...',
);
// Check if we have valid cached authentication
let hasValidAuth = false;
if (authStateManager) {
hasValidAuth = await authStateManager.hasValidAuth(
workingDir,
authMethod,
);
}
// Only authenticate if we don't have valid cached auth
if (!hasValidAuth) {
console.log(
'[QwenAgentManager] Authenticating before creating session...',
);
try {
await connection.authenticate(authMethod);
console.log('[QwenAgentManager] Authentication successful');
// Save auth state
if (authStateManager) {
console.log(
'[QwenAgentManager] Saving auth state after successful authentication',
);
console.log('[QwenAgentManager] Working dir for save:', workingDir);
console.log('[QwenAgentManager] Auth method for save:', authMethod);
await authStateManager.saveAuthState(workingDir, authMethod);
console.log('[QwenAgentManager] Auth state save completed');
}
} catch (authError) {
console.error('[QwenAgentManager] Authentication failed:', authError);
// Clear potentially invalid cache
if (authStateManager) {
console.log(
'[QwenAgentManager] Clearing auth cache due to authentication failure',
);
await authStateManager.clearAuthState();
}
throw authError;
}
} else {
console.log(
'[QwenAgentManager] Skipping authentication - using valid cached auth',
);
}
try {
console.log(
'[QwenAgentManager] Creating new session after authentication...',
);
await this.newSessionWithRetry(
connection,
workingDir,
3,
authMethod,
authStateManager,
'[QwenAgentManager] Creating new session (letting CLI handle authentication)...',
);
await this.newSessionWithRetry(connection, workingDir, 3, authMethod);
console.log('[QwenAgentManager] New session created successfully');
// Ensure auth state is saved (prevent repeated authentication)
if (authStateManager) {
console.log(
'[QwenAgentManager] Saving auth state after successful session creation',
);
await authStateManager.saveAuthState(workingDir, authMethod);
}
} catch (sessionError) {
console.log(`\n⚠ [SESSION FAILED] newSessionWithRetry threw error\n`);
console.log(`[QwenAgentManager] Error details:`, sessionError);
// Clear cache
if (authStateManager) {
console.log('[QwenAgentManager] Clearing auth cache due to failure');
await authStateManager.clearAuthState();
}
throw sessionError;
}
}
@@ -195,7 +111,6 @@ export class QwenConnectionHandler {
workingDir: string,
maxRetries: number,
authMethod: string,
authStateManager?: AuthStateManager,
): Promise<void> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
@@ -224,9 +139,10 @@ export class QwenConnectionHandler {
);
try {
await connection.authenticate(authMethod);
if (authStateManager) {
await authStateManager.saveAuthState(workingDir, authMethod);
}
// FIXME: @yiliang114 If there is no delay for a while, immediately executing
// newSession may cause the cli authorization jump to be triggered again
// Add a slight delay to ensure auth state is settled
await new Promise((resolve) => setTimeout(resolve, 300));
// Retry immediately after successful auth
await connection.newSession(workingDir);
console.log(
@@ -238,9 +154,6 @@ export class QwenConnectionHandler {
'[QwenAgentManager] Re-authentication failed:',
authErr,
);
if (authStateManager) {
await authStateManager.clearAuthState();
}
// Fall through to retry logic below
}
}

View File

@@ -167,7 +167,7 @@ export const App: React.FC = () => {
}, [fileContext.workspaceFiles, completion.isOpen, completion.triggerChar]);
// Message submission
const handleSubmit = useMessageSubmit({
const { handleSubmit: submitMessage } = useMessageSubmit({
inputText,
setInputText,
messageHandling,
@@ -487,6 +487,22 @@ export const App: React.FC = () => {
setThinkingEnabled((prev) => !prev);
};
// When user sends a message after scrolling up, re-pin and jump to the bottom
const handleSubmitWithScroll = useCallback(
(e: React.FormEvent) => {
setPinnedToBottom(true);
const container = messagesContainerRef.current;
if (container) {
const top = container.scrollHeight - container.clientHeight;
container.scrollTo({ top });
}
submitMessage(e);
},
[submitMessage],
);
// Create unified message array containing all types of messages and tool calls
const allMessages = useMemo<
Array<{
@@ -686,7 +702,7 @@ export const App: React.FC = () => {
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}
onKeyDown={() => {}}
onSubmit={handleSubmit.handleSubmit}
onSubmit={handleSubmitWithScroll}
onCancel={handleCancel}
onToggleEditMode={handleToggleEditMode}
onToggleThinking={handleToggleThinking}

View File

@@ -9,20 +9,18 @@ import { QwenAgentManager } from '../services/qwenAgentManager.js';
import { ConversationStore } from '../services/conversationStore.js';
import type { AcpPermissionRequest } from '../types/acpTypes.js';
import { CliDetector } from '../cli/cliDetector.js';
import { AuthStateManager } from '../services/authStateManager.js';
import { PanelManager } from '../webview/PanelManager.js';
import { MessageHandler } from '../webview/MessageHandler.js';
import { WebViewContent } from '../webview/WebViewContent.js';
import { CliInstaller } from '../cli/cliInstaller.js';
import { getFileName } from './utils/webviewUtils.js';
import { authMethod, type ApprovalModeValue } from '../types/acpTypes.js';
import { type ApprovalModeValue } from '../types/acpTypes.js';
export class WebViewProvider {
private panelManager: PanelManager;
private messageHandler: MessageHandler;
private agentManager: QwenAgentManager;
private conversationStore: ConversationStore;
private authStateManager: AuthStateManager;
private disposables: vscode.Disposable[] = [];
private agentInitialized = false; // Track if agent has been initialized
// Track a pending permission request and its resolver so extension commands
@@ -39,7 +37,6 @@ export class WebViewProvider {
) {
this.agentManager = new QwenAgentManager();
this.conversationStore = new ConversationStore(context);
this.authStateManager = AuthStateManager.getInstance(context);
this.panelManager = new PanelManager(extensionUri, () => {
// Panel dispose callback
this.disposables.forEach((d) => d.dispose());
@@ -522,40 +519,16 @@ export class WebViewProvider {
*/
private async attemptAuthStateRestoration(): Promise<void> {
try {
if (this.authStateManager) {
// Debug current auth state
await this.authStateManager.debugAuthState();
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
const hasValidAuth = await this.authStateManager.hasValidAuth(
workingDir,
authMethod,
);
console.log('[WebViewProvider] Has valid cached auth:', hasValidAuth);
if (hasValidAuth) {
console.log(
'[WebViewProvider] Valid auth found, attempting connection...',
);
// Try to connect with cached auth
await this.initializeAgentConnection();
} else {
console.log(
'[WebViewProvider] No valid auth found, rendering empty conversation',
);
// Render the chat UI immediately without connecting
await this.initializeEmptyConversation();
}
} else {
console.log(
'[WebViewProvider] No auth state manager, rendering empty conversation',
);
await this.initializeEmptyConversation();
}
} catch (_error) {
console.error('[WebViewProvider] Auth state restoration failed:', _error);
// Fallback to rendering empty conversation
console.log(
'[WebViewProvider] Attempting connection (CLI handle authentication)...',
);
//always attempt connection and let CLI handle authentication
await this.initializeAgentConnection();
} catch (error) {
console.error(
'[WebViewProvider] Error in attemptAuthStateRestoration:',
error,
);
await this.initializeEmptyConversation();
}
}
@@ -565,84 +538,84 @@ export class WebViewProvider {
* Can be called from show() or via /login command
*/
async initializeAgentConnection(): Promise<void> {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
return this.doInitializeAgentConnection();
}
console.log(
'[WebViewProvider] Starting initialization, workingDir:',
workingDir,
);
console.log(
'[WebViewProvider] AuthStateManager available:',
!!this.authStateManager,
);
/**
* Internal: perform actual connection/initialization (no auth locking).
*/
private async doInitializeAgentConnection(): Promise<void> {
const run = async () => {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
// Check if CLI is installed before attempting to connect
const cliDetection = await CliDetector.detectQwenCli();
if (!cliDetection.isInstalled) {
console.log(
'[WebViewProvider] Qwen CLI not detected, skipping agent connection',
'[WebViewProvider] Starting initialization, workingDir:',
workingDir,
);
console.log('[WebViewProvider] CLI detection error:', cliDetection.error);
console.log('[WebViewProvider] Using CLI-managed authentication');
// Show VSCode notification with installation option
await CliInstaller.promptInstallation();
// Check if CLI is installed before attempting to connect
const cliDetection = await CliDetector.detectQwenCli();
// Initialize empty conversation (can still browse history)
await this.initializeEmptyConversation();
} else {
console.log(
'[WebViewProvider] Qwen CLI detected, attempting connection...',
);
console.log('[WebViewProvider] CLI path:', cliDetection.cliPath);
console.log('[WebViewProvider] CLI version:', cliDetection.version);
try {
console.log('[WebViewProvider] Connecting to agent...');
if (!cliDetection.isInstalled) {
console.log(
'[WebViewProvider] Using authStateManager:',
!!this.authStateManager,
'[WebViewProvider] Qwen CLI not detected, skipping agent connection',
);
const authInfo = await this.authStateManager.getAuthInfo();
console.log('[WebViewProvider] Auth cache status:', authInfo);
// Pass the detected CLI path to ensure we use the correct installation
await this.agentManager.connect(
workingDir,
this.authStateManager,
cliDetection.cliPath,
console.log(
'[WebViewProvider] CLI detection error:',
cliDetection.error,
);
console.log('[WebViewProvider] Agent connected successfully');
this.agentInitialized = true;
// Load messages from the current Qwen session
await this.loadCurrentSessionMessages();
// Show VSCode notification with installation option
await CliInstaller.promptInstallation();
// Notify webview that agent is connected
this.sendMessageToWebView({
type: 'agentConnected',
data: {},
});
} catch (_error) {
console.error('[WebViewProvider] Agent connection error:', _error);
// Clear auth cache on error (might be auth issue)
await this.authStateManager.clearAuthState();
vscode.window.showWarningMessage(
`Failed to connect to Qwen CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`,
);
// Fallback to empty conversation
// Initialize empty conversation (can still browse history)
await this.initializeEmptyConversation();
} else {
console.log(
'[WebViewProvider] Qwen CLI detected, attempting connection...',
);
console.log('[WebViewProvider] CLI path:', cliDetection.cliPath);
console.log('[WebViewProvider] CLI version:', cliDetection.version);
// Notify webview that agent connection failed
this.sendMessageToWebView({
type: 'agentConnectionError',
data: {
message: _error instanceof Error ? _error.message : String(_error),
},
});
try {
console.log('[WebViewProvider] Connecting to agent...');
// Pass the detected CLI path to ensure we use the correct installation
await this.agentManager.connect(workingDir, cliDetection.cliPath);
console.log('[WebViewProvider] Agent connected successfully');
this.agentInitialized = true;
// Load messages from the current Qwen session
await this.loadCurrentSessionMessages();
// Notify webview that agent is connected
this.sendMessageToWebView({
type: 'agentConnected',
data: {},
});
} catch (_error) {
console.error('[WebViewProvider] Agent connection error:', _error);
vscode.window.showWarningMessage(
`Failed to connect to Qwen CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`,
);
// Fallback to empty conversation
await this.initializeEmptyConversation();
// Notify webview that agent connection failed
this.sendMessageToWebView({
type: 'agentConnectionError',
data: {
message:
_error instanceof Error ? _error.message : String(_error),
},
});
}
}
}
};
return run();
}
/**
@@ -651,12 +624,8 @@ export class WebViewProvider {
*/
async forceReLogin(): Promise<void> {
console.log('[WebViewProvider] Force re-login requested');
console.log(
'[WebViewProvider] Current authStateManager:',
!!this.authStateManager,
);
await vscode.window.withProgress(
return vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: 'Logging in to Qwen Code... ',
@@ -666,14 +635,6 @@ export class WebViewProvider {
try {
progress.report({ message: 'Preparing sign-in...' });
// Clear existing auth cache
if (this.authStateManager) {
await this.authStateManager.clearAuthState();
console.log('[WebViewProvider] Auth cache cleared');
} else {
console.log('[WebViewProvider] No authStateManager to clear');
}
// Disconnect existing connection if any
if (this.agentInitialized) {
try {
@@ -693,19 +654,11 @@ export class WebViewProvider {
});
// Reinitialize connection (will trigger fresh authentication)
await this.initializeAgentConnection();
await this.doInitializeAgentConnection();
console.log(
'[WebViewProvider] Force re-login completed successfully',
);
// Ensure auth state is saved after successful re-login
if (this.authStateManager) {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
await this.authStateManager.saveAuthState(workingDir, authMethod);
console.log('[WebViewProvider] Auth state saved after re-login');
}
// Send success notification to WebView
this.sendMessageToWebView({
type: 'loginSuccess',
@@ -793,28 +746,23 @@ export class WebViewProvider {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
// Skip session restoration entirely and create a new session directly
try {
await this.agentManager.createNewSession(
workingDir,
this.authStateManager,
);
console.log('[WebViewProvider] ACP session created successfully');
// Ensure auth state is saved after successful session creation
if (this.authStateManager) {
await this.authStateManager.saveAuthState(workingDir, authMethod);
console.log(
'[WebViewProvider] Auth state saved after session creation',
// avoid creating another session if connect() already created one.
if (!this.agentManager.currentSessionId) {
try {
await this.agentManager.createNewSession(workingDir);
console.log('[WebViewProvider] ACP session created successfully');
} catch (sessionError) {
console.error(
'[WebViewProvider] Failed to create ACP session:',
sessionError,
);
vscode.window.showWarningMessage(
`Failed to create ACP session: ${sessionError}. You may need to authenticate first.`,
);
}
} catch (sessionError) {
console.error(
'[WebViewProvider] Failed to create ACP session:',
sessionError,
);
vscode.window.showWarningMessage(
`Failed to create ACP session: ${sessionError}. You may need to authenticate first.`,
} else {
console.log(
'[WebViewProvider] Existing ACP session detected, skipping new session creation',
);
}
@@ -974,17 +922,6 @@ export class WebViewProvider {
this.agentManager.disconnect();
}
/**
* Clear authentication cache for this WebViewProvider instance
*/
async clearAuthCache(): Promise<void> {
console.log('[WebViewProvider] Clearing auth cache for this instance');
if (this.authStateManager) {
await this.authStateManager.clearAuthState();
this.resetAgentState();
}
}
/**
* Restore an existing WebView panel (called during VSCode restart)
* This sets up the panel with all event listeners
@@ -992,8 +929,7 @@ export class WebViewProvider {
async restorePanel(panel: vscode.WebviewPanel): Promise<void> {
console.log('[WebViewProvider] Restoring WebView panel');
console.log(
'[WebViewProvider] Current authStateManager in restore:',
!!this.authStateManager,
'[WebViewProvider] Using CLI-managed authentication in restore',
);
this.panelManager.setPanel(panel);
@@ -1196,18 +1132,13 @@ export class WebViewProvider {
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
// Create new Qwen session via agent manager
await this.agentManager.createNewSession(
workingDir,
this.authStateManager,
);
await this.agentManager.createNewSession(workingDir);
// Clear current conversation UI
this.sendMessageToWebView({
type: 'conversationCleared',
data: {},
});
console.log('[WebViewProvider] New session created successfully');
} catch (_error) {
console.error('[WebViewProvider] Failed to create new session:', _error);
vscode.window.showErrorMessage(`Failed to create new session: ${_error}`);

View File

@@ -61,25 +61,6 @@ export const safeTitle = (title: unknown): string => {
return '';
};
/**
* Get icon emoji for a given tool kind
*/
export const getKindIcon = (kind: string): string => {
const kindMap: Record<string, string> = {
edit: '✏️',
write: '✏️',
read: '📖',
execute: '⚡',
fetch: '🌐',
delete: '🗑️',
move: '📦',
search: '🔍',
think: '💭',
diff: '📝',
};
return kindMap[kind.toLowerCase()] || '🔧';
};
/**
* Check if a tool call should be displayed
* Hides internal tool calls

View File

@@ -149,6 +149,50 @@ export class SessionMessageHandler extends BaseMessageHandler {
return this.isSavingCheckpoint;
}
/**
* Prompt user to login and invoke the registered login handler/command.
* Returns true if a login was initiated.
*/
private async promptLogin(message: string): Promise<boolean> {
const result = await vscode.window.showWarningMessage(message, 'Login Now');
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
return true;
}
return false;
}
/**
* Prompt user to login or view offline. Returns 'login', 'offline', or 'dismiss'.
* When login is chosen, it triggers the login handler/command.
*/
private async promptLoginOrOffline(
message: string,
): Promise<'login' | 'offline' | 'dismiss'> {
const selection = await vscode.window.showWarningMessage(
message,
'Login Now',
'View Offline',
);
if (selection === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
return 'login';
}
if (selection === 'View Offline') {
return 'offline';
}
return 'dismiss';
}
/**
* Handle send message request
*/
@@ -271,26 +315,37 @@ export class SessionMessageHandler extends BaseMessageHandler {
console.warn('[SessionMessageHandler] Agent not connected');
// Show non-modal notification with Login button
const result = await vscode.window.showWarningMessage(
'You need to login first to use Qwen Code.',
'Login Now',
);
if (result === 'Login Now') {
// Use login handler directly
if (this.loginHandler) {
await this.loginHandler();
} else {
// Fallback to command
vscode.window.showInformationMessage(
'Please wait while we connect to Qwen Code...',
);
await vscode.commands.executeCommand('qwen-code.login');
}
}
await this.promptLogin('You need to login first to use Qwen Code.');
return;
}
// Ensure an ACP session exists before sending prompt
if (!this.agentManager.currentSessionId) {
try {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
await this.agentManager.createNewSession(workingDir);
} catch (createErr) {
console.error(
'[SessionMessageHandler] Failed to create session before sending message:',
createErr,
);
const errorMsg =
createErr instanceof Error ? createErr.message : String(createErr);
if (
errorMsg.includes('Authentication required') ||
errorMsg.includes('(code: -32000)')
) {
await this.promptLogin(
'Your login session has expired or is invalid. Please login again to continue using Qwen Code.',
);
return;
}
vscode.window.showErrorMessage(`Failed to create session: ${errorMsg}`);
return;
}
}
// Send to agent
try {
this.resetStreamContent();
@@ -391,19 +446,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('Invalid token')
) {
// Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage(
await this.promptLogin(
'Your login session has expired or is invalid. Please login again to continue using Qwen Code.',
'Login Now',
);
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling
this.sendToWebView({
type: 'sessionExpired',
@@ -428,17 +474,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
// Ensure connection (login) before creating a new session
if (!this.agentManager.isConnected) {
const result = await vscode.window.showWarningMessage(
const proceeded = await this.promptLogin(
'You need to login before creating a new session.',
'Login Now',
);
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
} else {
if (!proceeded) {
return;
}
}
@@ -489,19 +528,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('No active ACP session')
) {
// Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage(
await this.promptLogin(
'Your login session has expired or is invalid. Please login again to create a new session.',
'Login Now',
);
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling
this.sendToWebView({
type: 'sessionExpired',
@@ -525,19 +555,11 @@ export class SessionMessageHandler extends BaseMessageHandler {
// If not connected yet, offer to login or view offline
if (!this.agentManager.isConnected) {
const selection = await vscode.window.showWarningMessage(
const choice = await this.promptLoginOrOffline(
'You are not logged in. Login now to fully restore this session, or view it offline.',
'Login Now',
'View Offline',
);
if (selection === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
} else if (selection === 'View Offline') {
if (choice === 'offline') {
// Show messages from local cache only
const messages =
await this.agentManager.getSessionMessages(sessionId);
@@ -550,7 +572,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
'Showing cached session content. Login to interact with the AI.',
);
return;
} else {
} else if (choice !== 'login') {
// User dismissed; do nothing
return;
}
@@ -637,19 +659,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('No active ACP session')
) {
// Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage(
await this.promptLogin(
'Your login session has expired or is invalid. Please login again to switch sessions.',
'Login Now',
);
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling
this.sendToWebView({
type: 'sessionExpired',
@@ -706,19 +719,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
createErrorMsg.includes('No active ACP session')
) {
// Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage(
await this.promptLogin(
'Your login session has expired or is invalid. Please login again to switch sessions.',
'Login Now',
);
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling
this.sendToWebView({
type: 'sessionExpired',
@@ -755,19 +759,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('No active ACP session')
) {
// Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage(
await this.promptLogin(
'Your login session has expired or is invalid. Please login again to switch sessions.',
'Login Now',
);
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling
this.sendToWebView({
type: 'sessionExpired',
@@ -819,19 +814,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('No active ACP session')
) {
// Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage(
await this.promptLogin(
'Your login session has expired or is invalid. Please login again to view sessions.',
'Login Now',
);
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling
this.sendToWebView({
type: 'sessionExpired',
@@ -883,19 +869,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('No active ACP session')
) {
// Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage(
await this.promptLogin(
'Your login session has expired or is invalid. Please login again to save sessions.',
'Login Now',
);
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling
this.sendToWebView({
type: 'sessionExpired',
@@ -931,19 +908,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('No active ACP session')
) {
// Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage(
await this.promptLogin(
'Your login session has expired or is invalid. Please login again to save sessions.',
'Login Now',
);
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling
this.sendToWebView({
type: 'sessionExpired',
@@ -996,19 +964,11 @@ export class SessionMessageHandler extends BaseMessageHandler {
try {
// If not connected, offer to login or view offline
if (!this.agentManager.isConnected) {
const selection = await vscode.window.showWarningMessage(
const choice = await this.promptLoginOrOffline(
'You are not logged in. Login now to fully restore this session, or view it offline.',
'Login Now',
'View Offline',
);
if (selection === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
} else if (selection === 'View Offline') {
if (choice === 'offline') {
const messages =
await this.agentManager.getSessionMessages(sessionId);
this.currentConversationId = sessionId;
@@ -1020,7 +980,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
'Showing cached session content. Login to interact with the AI.',
);
return;
} else {
} else if (choice !== 'login') {
return;
}
}
@@ -1054,19 +1014,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('No active ACP session')
) {
// Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage(
await this.promptLogin(
'Your login session has expired or is invalid. Please login again to resume sessions.',
'Login Now',
);
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling
this.sendToWebView({
type: 'sessionExpired',
@@ -1105,19 +1056,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('No active ACP session')
) {
// Show a more user-friendly error message for expired sessions
const result = await vscode.window.showWarningMessage(
await this.promptLogin(
'Your login session has expired or is invalid. Please login again to resume sessions.',
'Login Now',
);
if (result === 'Login Now') {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwen-code.login');
}
}
// Send a specific error to the webview for better UI handling
this.sendToWebView({
type: 'sessionExpired',

View File

@@ -227,40 +227,26 @@ export const useWebViewMessages = ({
break;
}
// case 'cliNotInstalled': {
// // Show CLI not installed message
// const errorMsg =
// (message?.data?.error as string) ||
// 'Qwen Code CLI is not installed. Please install it to enable full functionality.';
case 'agentConnected': {
// Agent connected successfully; clear any pending spinner
handlers.messageHandling.clearWaitingForResponse();
break;
}
// handlers.messageHandling.addMessage({
// role: 'assistant',
// content: `Qwen CLI is not installed. Please install it to enable full functionality.\n\nError: ${errorMsg}\n\nInstallation instructions:\n1. Install via npm:\n npm install -g @qwen-code/qwen-code@latest\n\n2. After installation, reload VS Code or restart the extension.`,
// timestamp: Date.now(),
// });
// break;
// }
case 'agentConnectionError': {
// Agent connection failed; surface the error and unblock the UI
handlers.messageHandling.clearWaitingForResponse();
const errorMsg =
(message?.data?.message as string) ||
'Failed to connect to Qwen agent.';
// case 'agentConnected': {
// // Agent connected successfully
// handlers.messageHandling.clearWaitingForResponse();
// break;
// }
// case 'agentConnectionError': {
// // Agent connection failed
// handlers.messageHandling.clearWaitingForResponse();
// const errorMsg =
// (message?.data?.message as string) ||
// 'Failed to connect to Qwen agent.';
// handlers.messageHandling.addMessage({
// role: 'assistant',
// content: `Failed to connect to Qwen agent: ${errorMsg}\nYou can still use the chat UI, but messages won't be sent to AI.`,
// timestamp: Date.now(),
// });
// break;
// }
handlers.messageHandling.addMessage({
role: 'assistant',
content: `Failed to connect to Qwen agent: ${errorMsg}\nYou can still use the chat UI, but messages won't be sent to AI.`,
timestamp: Date.now(),
});
break;
}
case 'loginError': {
// Clear loading state and show error notice

View File

@@ -5,7 +5,6 @@
*/
/* Import component styles */
@import '../components/messages/Assistant/AssistantMessage.css';
@import './timeline.css';
@import '../components/messages/MarkdownRenderer/MarkdownRenderer.css';

View File

@@ -1,92 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* Minimal line-diff utility for webview previews.
*
* This is a lightweight LCS-based algorithm to compute add/remove operations
* between two texts. It intentionally avoids heavy dependencies and is
* sufficient for rendering a compact preview inside the chat.
*/
export type DiffOp =
| { type: 'add'; line: string; newIndex: number }
| { type: 'remove'; line: string; oldIndex: number };
/**
* Compute a minimal line-diff (added/removed only).
* - Equal lines are omitted from output by design (we only preview changes).
* - Order of operations follows the new text progression so the preview feels natural.
*/
export function computeLineDiff(
oldText: string | null | undefined,
newText: string | undefined,
): DiffOp[] {
const a = (oldText || '').split('\n');
const b = (newText || '').split('\n');
const n = a.length;
const m = b.length;
// Build LCS DP table
const dp: number[][] = Array.from({ length: n + 1 }, () =>
new Array(m + 1).fill(0),
);
for (let i = n - 1; i >= 0; i--) {
for (let j = m - 1; j >= 0; j--) {
if (a[i] === b[j]) {
dp[i][j] = dp[i + 1][j + 1] + 1;
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
}
}
}
// Walk to produce operations
const ops: DiffOp[] = [];
let i = 0;
let j = 0;
while (i < n && j < m) {
if (a[i] === b[j]) {
i++;
j++;
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
// remove a[i]
ops.push({ type: 'remove', line: a[i], oldIndex: i });
i++;
} else {
// add b[j]
ops.push({ type: 'add', line: b[j], newIndex: j });
j++;
}
}
// Remaining tails
while (i < n) {
ops.push({ type: 'remove', line: a[i], oldIndex: i });
i++;
}
while (j < m) {
ops.push({ type: 'add', line: b[j], newIndex: j });
j++;
}
return ops;
}
/**
* Truncate a long list of operations for preview purposes.
* Keeps first `head` and last `tail` operations, inserting a gap marker.
*/
export function truncateOps<T>(
ops: T[],
head = 120,
tail = 80,
): { items: T[]; truncated: boolean; omitted: number } {
if (ops.length <= head + tail) {
return { items: ops, truncated: false, omitted: 0 };
}
const items = [...ops.slice(0, head), ...ops.slice(-tail)];
return { items, truncated: true, omitted: ops.length - head - tail };
}