mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
chore(vscode-ide-companion): wip
This commit is contained in:
@@ -50,15 +50,9 @@ export class CliDetector {
|
|||||||
this.cachedResult &&
|
this.cachedResult &&
|
||||||
now - this.lastCheckTime < this.CACHE_DURATION_MS
|
now - this.lastCheckTime < this.CACHE_DURATION_MS
|
||||||
) {
|
) {
|
||||||
console.log('[CliDetector] Returning cached result');
|
|
||||||
return this.cachedResult;
|
return this.cachedResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[CliDetector] Starting lightweight CLI detection, current PATH:',
|
|
||||||
process.env.PATH,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === 'win32';
|
||||||
const whichCommand = isWindows ? 'where' : 'which';
|
const whichCommand = isWindows ? 'where' : 'which';
|
||||||
@@ -70,11 +64,6 @@ export class CliDetector {
|
|||||||
? `${whichCommand} qwen`
|
? `${whichCommand} qwen`
|
||||||
: `${whichCommand} qwen`;
|
: `${whichCommand} qwen`;
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[CliDetector] Detecting CLI with lightweight command:',
|
|
||||||
detectionCommand,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute command to detect CLI path, set shorter timeout (3 seconds)
|
// 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
|
||||||
@@ -88,8 +77,6 @@ export class CliDetector {
|
|||||||
.filter((line) => line.trim());
|
.filter((line) => line.trim());
|
||||||
const cliPath = lines[0]; // Take only the first path
|
const cliPath = lines[0]; // Take only the first path
|
||||||
|
|
||||||
console.log('[CliDetector] Found CLI at:', cliPath);
|
|
||||||
|
|
||||||
// Build successful detection result, note no version information
|
// Build successful detection result, note no version information
|
||||||
this.cachedResult = {
|
this.cachedResult = {
|
||||||
isInstalled: true,
|
isInstalled: true,
|
||||||
|
|||||||
@@ -5,121 +5,154 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { CliContextManager } from './cliContextManager.js';
|
import { CliDetector, type CliDetectionResult } from './cliDetector.js';
|
||||||
import { CliVersionManager } from './cliVersionManager.js';
|
import { CliVersionManager } from './cliVersionManager.js';
|
||||||
import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from './cliVersionManager.js';
|
import semver from 'semver';
|
||||||
import type { CliVersionInfo } from './cliVersionManager.js';
|
|
||||||
|
|
||||||
// Track which versions have already been warned about to avoid repetitive warnings
|
|
||||||
// Using a Map with timestamps to allow warnings to be shown again after a certain period
|
|
||||||
const warnedVersions = new Map<string, number>();
|
|
||||||
const WARNING_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24 hours cooldown
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check CLI version and show warning if below minimum requirement
|
* CLI Version Checker
|
||||||
* Provides an "Upgrade Now" option for unsupported versions
|
|
||||||
*
|
*
|
||||||
* @returns Version information
|
* Handles CLI version checking with throttling to prevent frequent notifications.
|
||||||
|
* This class manages version checking and provides version information without
|
||||||
|
* constantly bothering the user with popups.
|
||||||
*/
|
*/
|
||||||
export async function checkCliVersionAndWarn(): Promise<CliVersionInfo> {
|
export class CliVersionChecker {
|
||||||
try {
|
private static instance: CliVersionChecker;
|
||||||
const cliContextManager = CliContextManager.getInstance();
|
private lastNotificationTime: number = 0;
|
||||||
const versionInfo =
|
private static readonly NOTIFICATION_COOLDOWN_MS = 300000; // 5 minutes cooldown
|
||||||
await CliVersionManager.getInstance().detectCliVersion(true);
|
private context: vscode.ExtensionContext;
|
||||||
cliContextManager.setCurrentVersionInfo(versionInfo);
|
|
||||||
|
|
||||||
if (!versionInfo.isSupported) {
|
private constructor(context: vscode.ExtensionContext) {
|
||||||
// Only show warning if we haven't already warned about this specific version recently
|
this.context = context;
|
||||||
const versionKey = versionInfo.version || 'unknown';
|
}
|
||||||
const lastWarningTime = warnedVersions.get(versionKey);
|
|
||||||
const currentTime = Date.now();
|
|
||||||
|
|
||||||
// Show warning if we haven't warned about this version or if enough time has passed
|
/**
|
||||||
if (
|
* Get singleton instance
|
||||||
!lastWarningTime ||
|
*/
|
||||||
currentTime - lastWarningTime > WARNING_COOLDOWN_MS
|
static getInstance(context?: vscode.ExtensionContext): CliVersionChecker {
|
||||||
) {
|
if (!CliVersionChecker.instance && context) {
|
||||||
// Wait to determine release version number
|
CliVersionChecker.instance = new CliVersionChecker(context);
|
||||||
const selection = await 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.`,
|
return CliVersionChecker.instance;
|
||||||
'Upgrade Now',
|
}
|
||||||
);
|
|
||||||
|
|
||||||
// Handle the user's selection
|
/**
|
||||||
if (selection === 'Upgrade Now') {
|
* Check CLI version with cooldown to prevent spamming notifications
|
||||||
// Open terminal and run npm install command
|
*
|
||||||
const terminal = vscode.window.createTerminal(
|
* @param showNotifications - Whether to show notifications for issues
|
||||||
'Qwen Code CLI Upgrade',
|
* @returns Promise with version check result
|
||||||
|
*/
|
||||||
|
async checkCliVersion(showNotifications: boolean = true): Promise<{
|
||||||
|
isInstalled: boolean;
|
||||||
|
version?: string;
|
||||||
|
isSupported: boolean;
|
||||||
|
needsUpdate: boolean;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
// Detect CLI installation
|
||||||
|
const detectionResult: CliDetectionResult =
|
||||||
|
await CliDetector.detectQwenCli();
|
||||||
|
|
||||||
|
if (!detectionResult.isInstalled) {
|
||||||
|
if (showNotifications && this.canShowNotification()) {
|
||||||
|
vscode.window.showWarningMessage(
|
||||||
|
`Qwen Code CLI not found. Please install it using: npm install -g @qwen-code/qwen-code@latest`,
|
||||||
);
|
);
|
||||||
terminal.show();
|
this.lastNotificationTime = Date.now();
|
||||||
terminal.sendText('npm install -g @qwen-code/qwen-code@latest');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the last warning time
|
return {
|
||||||
warnedVersions.set(versionKey, currentTime);
|
isInstalled: false,
|
||||||
|
error: detectionResult.error,
|
||||||
|
isSupported: false,
|
||||||
|
needsUpdate: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return versionInfo;
|
// Get version information
|
||||||
} catch (error) {
|
const versionManager = CliVersionManager.getInstance();
|
||||||
console.error('[CliVersionChecker] Failed to check CLI version:', error);
|
const versionInfo = await versionManager.detectCliVersion();
|
||||||
// Return a default version info in case of error
|
|
||||||
return {
|
const currentVersion = detectionResult.version;
|
||||||
version: undefined,
|
const isSupported = versionInfo.isSupported;
|
||||||
isSupported: false,
|
|
||||||
features: {
|
// Check if update is needed (version is too old)
|
||||||
supportsSessionList: false,
|
const minRequiredVersion = '0.5.0'; // This should match MIN_CLI_VERSION_FOR_SESSION_METHODS from CliVersionManager
|
||||||
supportsSessionLoad: false,
|
const needsUpdate = currentVersion
|
||||||
},
|
? !semver.satisfies(currentVersion, `>=${minRequiredVersion}`)
|
||||||
detectionResult: {
|
: false;
|
||||||
|
|
||||||
|
// Show notification only if needed and within cooldown period
|
||||||
|
if (showNotifications && !isSupported && this.canShowNotification()) {
|
||||||
|
vscode.window.showWarningMessage(
|
||||||
|
`Qwen Code CLI version is outdated. Current: ${currentVersion || 'unknown'}, Minimum required: ${minRequiredVersion}. Please update using: npm install -g @qwen-code/qwen-code@latest`,
|
||||||
|
);
|
||||||
|
this.lastNotificationTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isInstalled: true,
|
||||||
|
version: currentVersion,
|
||||||
|
isSupported,
|
||||||
|
needsUpdate,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[CliVersionChecker] Version check failed:', error);
|
||||||
|
|
||||||
|
if (showNotifications && this.canShowNotification()) {
|
||||||
|
vscode.window.showErrorMessage(
|
||||||
|
`Failed to check Qwen Code CLI version: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
this.lastNotificationTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
isInstalled: false,
|
isInstalled: false,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
},
|
isSupported: false,
|
||||||
};
|
needsUpdate: false,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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,
|
* Check if notification can be shown based on cooldown period
|
||||||
|
*/
|
||||||
|
private canShowNotification(): boolean {
|
||||||
|
return (
|
||||||
|
Date.now() - this.lastNotificationTime >
|
||||||
|
CliVersionChecker.NOTIFICATION_COOLDOWN_MS
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear notification cooldown (allows immediate next notification)
|
||||||
|
*/
|
||||||
|
clearCooldown(): void {
|
||||||
|
this.lastNotificationTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get version status for display in status bar or other UI elements
|
||||||
|
*/
|
||||||
|
async getVersionStatus(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const versionManager = CliVersionManager.getInstance();
|
||||||
|
const versionInfo = await versionManager.detectCliVersion();
|
||||||
|
|
||||||
|
if (!versionInfo.detectionResult.isInstalled) {
|
||||||
|
return 'CLI: Not installed';
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = versionInfo.version || 'Unknown';
|
||||||
|
if (!versionInfo.isSupported) {
|
||||||
|
return `CLI: ${version} (Outdated)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `CLI: ${version}`;
|
||||||
|
} catch (_) {
|
||||||
|
return 'CLI: Error';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ vi.mock('vscode', () => ({
|
|||||||
registerWebviewPanelSerializer: vi.fn(() => ({
|
registerWebviewPanelSerializer: vi.fn(() => ({
|
||||||
dispose: vi.fn(),
|
dispose: vi.fn(),
|
||||||
})),
|
})),
|
||||||
|
createStatusBarItem: vi.fn(() => ({
|
||||||
|
text: '',
|
||||||
|
tooltip: '',
|
||||||
|
command: '',
|
||||||
|
show: vi.fn(),
|
||||||
|
hide: vi.fn(),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
workspace: {
|
workspace: {
|
||||||
workspaceFolders: [],
|
workspaceFolders: [],
|
||||||
@@ -58,6 +66,10 @@ vi.mock('vscode', () => ({
|
|||||||
Uri: {
|
Uri: {
|
||||||
joinPath: vi.fn(),
|
joinPath: vi.fn(),
|
||||||
},
|
},
|
||||||
|
StatusBarAlignment: {
|
||||||
|
Left: 1,
|
||||||
|
Right: 2,
|
||||||
|
},
|
||||||
ExtensionMode: {
|
ExtensionMode: {
|
||||||
Development: 1,
|
Development: 1,
|
||||||
Production: 2,
|
Production: 2,
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ 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 { MIN_CLI_VERSION_FOR_SESSION_METHODS } from '../cli/cliVersionManager.js';
|
import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from '../cli/cliVersionManager.js';
|
||||||
import { processServerVersion } from '../cli/cliVersionChecker.js';
|
|
||||||
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
|
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
|
||||||
import { handleAuthenticateUpdate } from '../utils/authNotificationHandler.js';
|
import { handleAuthenticateUpdate } from '../utils/authNotificationHandler.js';
|
||||||
|
|
||||||
@@ -163,9 +162,6 @@ 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>;
|
||||||
const modes = obj['modes'] as
|
const modes = obj['modes'] as
|
||||||
| {
|
| {
|
||||||
@@ -288,71 +284,59 @@ export class QwenAgentManager {
|
|||||||
'[QwenAgentManager] Getting session list with version-aware strategy',
|
'[QwenAgentManager] Getting session list with version-aware strategy',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if CLI supports session/list method
|
try {
|
||||||
const cliContextManager = CliContextManager.getInstance();
|
console.log(
|
||||||
const supportsSessionList = cliContextManager.supportsSessionList();
|
'[QwenAgentManager] Attempting to get session list via ACP method',
|
||||||
|
);
|
||||||
|
const response = await this.connection.listSessions();
|
||||||
|
console.log('[QwenAgentManager] ACP session list response:', response);
|
||||||
|
|
||||||
console.log(
|
// sendRequest resolves with the JSON-RPC "result" directly
|
||||||
'[QwenAgentManager] CLI supports session/list:',
|
// Newer CLI returns an object: { items: [...], nextCursor?, hasMore }
|
||||||
supportsSessionList,
|
// Older prototypes might return an array. Support both.
|
||||||
);
|
const res: unknown = response;
|
||||||
|
let items: Array<Record<string, unknown>> = [];
|
||||||
|
|
||||||
// Try ACP method first if supported
|
// Note: AcpSessionManager resolves `sendRequest` with the JSON-RPC
|
||||||
if (supportsSessionList) {
|
// "result" directly (not the full AcpResponse). Treat it as unknown
|
||||||
try {
|
// and carefully narrow before accessing `items` to satisfy strict TS.
|
||||||
console.log(
|
if (res && typeof res === 'object' && 'items' in res) {
|
||||||
'[QwenAgentManager] Attempting to get session list via ACP method',
|
const itemsValue = (res as { items?: unknown }).items;
|
||||||
);
|
items = Array.isArray(itemsValue)
|
||||||
const response = await this.connection.listSessions();
|
? (itemsValue as Array<Record<string, unknown>>)
|
||||||
console.log('[QwenAgentManager] ACP session list response:', response);
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
// sendRequest resolves with the JSON-RPC "result" directly
|
console.log(
|
||||||
// Newer CLI returns an object: { items: [...], nextCursor?, hasMore }
|
'[QwenAgentManager] Sessions retrieved via ACP:',
|
||||||
// Older prototypes might return an array. Support both.
|
res,
|
||||||
const res: unknown = response;
|
items.length,
|
||||||
let items: Array<Record<string, unknown>> = [];
|
);
|
||||||
|
if (items.length > 0) {
|
||||||
// Note: AcpSessionManager resolves `sendRequest` with the JSON-RPC
|
const sessions = items.map((item) => ({
|
||||||
// "result" directly (not the full AcpResponse). Treat it as unknown
|
id: item.sessionId || item.id,
|
||||||
// and carefully narrow before accessing `items` to satisfy strict TS.
|
sessionId: item.sessionId || item.id,
|
||||||
if (res && typeof res === 'object' && 'items' in res) {
|
title: item.title || item.name || item.prompt || 'Untitled Session',
|
||||||
const itemsValue = (res as { items?: unknown }).items;
|
name: item.title || item.name || item.prompt || 'Untitled Session',
|
||||||
items = Array.isArray(itemsValue)
|
startTime: item.startTime,
|
||||||
? (itemsValue as Array<Record<string, unknown>>)
|
lastUpdated: item.mtime || item.lastUpdated,
|
||||||
: [];
|
messageCount: item.messageCount || 0,
|
||||||
}
|
projectHash: item.projectHash,
|
||||||
|
filePath: item.filePath,
|
||||||
|
cwd: item.cwd,
|
||||||
|
}));
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'[QwenAgentManager] Sessions retrieved via ACP:',
|
'[QwenAgentManager] Sessions retrieved via ACP:',
|
||||||
res,
|
sessions.length,
|
||||||
items.length,
|
|
||||||
);
|
|
||||||
if (items.length > 0) {
|
|
||||||
const sessions = items.map((item) => ({
|
|
||||||
id: item.sessionId || item.id,
|
|
||||||
sessionId: item.sessionId || item.id,
|
|
||||||
title: item.title || item.name || item.prompt || 'Untitled Session',
|
|
||||||
name: item.title || item.name || item.prompt || 'Untitled Session',
|
|
||||||
startTime: item.startTime,
|
|
||||||
lastUpdated: item.mtime || item.lastUpdated,
|
|
||||||
messageCount: item.messageCount || 0,
|
|
||||||
projectHash: item.projectHash,
|
|
||||||
filePath: item.filePath,
|
|
||||||
cwd: item.cwd,
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[QwenAgentManager] Sessions retrieved via ACP:',
|
|
||||||
sessions.length,
|
|
||||||
);
|
|
||||||
return sessions;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(
|
|
||||||
'[QwenAgentManager] ACP session list failed, falling back to file system method:',
|
|
||||||
error,
|
|
||||||
);
|
);
|
||||||
|
return sessions;
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
'[QwenAgentManager] ACP session list failed, falling back to file system method:',
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always fall back to file system method
|
// Always fall back to file system method
|
||||||
@@ -409,62 +393,52 @@ export class QwenAgentManager {
|
|||||||
const size = params?.size ?? 20;
|
const size = params?.size ?? 20;
|
||||||
const cursor = params?.cursor;
|
const cursor = params?.cursor;
|
||||||
|
|
||||||
const cliContextManager = CliContextManager.getInstance();
|
try {
|
||||||
const supportsSessionList = cliContextManager.supportsSessionList();
|
const response = await this.connection.listSessions({
|
||||||
|
size,
|
||||||
|
...(cursor !== undefined ? { cursor } : {}),
|
||||||
|
});
|
||||||
|
// sendRequest resolves with the JSON-RPC "result" directly
|
||||||
|
const res: unknown = response;
|
||||||
|
let items: Array<Record<string, unknown>> = [];
|
||||||
|
|
||||||
if (supportsSessionList) {
|
if (Array.isArray(res)) {
|
||||||
try {
|
items = res;
|
||||||
const response = await this.connection.listSessions({
|
} else if (typeof res === 'object' && res !== null && 'items' in res) {
|
||||||
size,
|
const responseObject = res as {
|
||||||
...(cursor !== undefined ? { cursor } : {}),
|
items?: Array<Record<string, unknown>>;
|
||||||
});
|
};
|
||||||
// sendRequest resolves with the JSON-RPC "result" directly
|
items = Array.isArray(responseObject.items) ? responseObject.items : [];
|
||||||
const res: unknown = response;
|
|
||||||
let items: Array<Record<string, unknown>> = [];
|
|
||||||
|
|
||||||
if (Array.isArray(res)) {
|
|
||||||
items = res;
|
|
||||||
} else if (typeof res === 'object' && res !== null && 'items' in res) {
|
|
||||||
const responseObject = res as {
|
|
||||||
items?: Array<Record<string, unknown>>;
|
|
||||||
};
|
|
||||||
items = Array.isArray(responseObject.items)
|
|
||||||
? responseObject.items
|
|
||||||
: [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapped = items.map((item) => ({
|
|
||||||
id: item.sessionId || item.id,
|
|
||||||
sessionId: item.sessionId || item.id,
|
|
||||||
title: item.title || item.name || item.prompt || 'Untitled Session',
|
|
||||||
name: item.title || item.name || item.prompt || 'Untitled Session',
|
|
||||||
startTime: item.startTime,
|
|
||||||
lastUpdated: item.mtime || item.lastUpdated,
|
|
||||||
messageCount: item.messageCount || 0,
|
|
||||||
projectHash: item.projectHash,
|
|
||||||
filePath: item.filePath,
|
|
||||||
cwd: item.cwd,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const nextCursor: number | undefined =
|
|
||||||
typeof res === 'object' && res !== null && 'nextCursor' in res
|
|
||||||
? typeof res.nextCursor === 'number'
|
|
||||||
? res.nextCursor
|
|
||||||
: undefined
|
|
||||||
: undefined;
|
|
||||||
const hasMore: boolean =
|
|
||||||
typeof res === 'object' && res !== null && 'hasMore' in res
|
|
||||||
? Boolean(res.hasMore)
|
|
||||||
: false;
|
|
||||||
|
|
||||||
return { sessions: mapped, nextCursor, hasMore };
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(
|
|
||||||
'[QwenAgentManager] Paged ACP session list failed:',
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
// fall through to file system
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mapped = items.map((item) => ({
|
||||||
|
id: item.sessionId || item.id,
|
||||||
|
sessionId: item.sessionId || item.id,
|
||||||
|
title: item.title || item.name || item.prompt || 'Untitled Session',
|
||||||
|
name: item.title || item.name || item.prompt || 'Untitled Session',
|
||||||
|
startTime: item.startTime,
|
||||||
|
lastUpdated: item.mtime || item.lastUpdated,
|
||||||
|
messageCount: item.messageCount || 0,
|
||||||
|
projectHash: item.projectHash,
|
||||||
|
filePath: item.filePath,
|
||||||
|
cwd: item.cwd,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const nextCursor: number | undefined =
|
||||||
|
typeof res === 'object' && res !== null && 'nextCursor' in res
|
||||||
|
? typeof res.nextCursor === 'number'
|
||||||
|
? res.nextCursor
|
||||||
|
: undefined
|
||||||
|
: undefined;
|
||||||
|
const hasMore: boolean =
|
||||||
|
typeof res === 'object' && res !== null && 'hasMore' in res
|
||||||
|
? Boolean(res.hasMore)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
return { sessions: mapped, nextCursor, hasMore };
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[QwenAgentManager] Paged ACP session list failed:', error);
|
||||||
|
// fall through to file system
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: file system for current project only (to match ACP semantics)
|
// Fallback: file system for current project only (to match ACP semantics)
|
||||||
@@ -513,32 +487,28 @@ export class QwenAgentManager {
|
|||||||
*/
|
*/
|
||||||
async getSessionMessages(sessionId: string): Promise<ChatMessage[]> {
|
async getSessionMessages(sessionId: string): Promise<ChatMessage[]> {
|
||||||
try {
|
try {
|
||||||
// Prefer reading CLI's JSONL if we can find filePath from session/list
|
try {
|
||||||
const cliContextManager = CliContextManager.getInstance();
|
const list = await this.getSessionList();
|
||||||
if (cliContextManager.supportsSessionList()) {
|
const item = list.find(
|
||||||
try {
|
(s) => s.sessionId === sessionId || s.id === sessionId,
|
||||||
const list = await this.getSessionList();
|
);
|
||||||
const item = list.find(
|
console.log(
|
||||||
(s) => s.sessionId === sessionId || s.id === sessionId,
|
'[QwenAgentManager] Session list item for filePath lookup:',
|
||||||
);
|
item,
|
||||||
console.log(
|
);
|
||||||
'[QwenAgentManager] Session list item for filePath lookup:',
|
if (
|
||||||
item,
|
typeof item === 'object' &&
|
||||||
);
|
item !== null &&
|
||||||
if (
|
'filePath' in item &&
|
||||||
typeof item === 'object' &&
|
typeof item.filePath === 'string'
|
||||||
item !== null &&
|
) {
|
||||||
'filePath' in item &&
|
const messages = await this.readJsonlMessages(item.filePath);
|
||||||
typeof item.filePath === 'string'
|
// Even if messages array is empty, we should return it rather than falling back
|
||||||
) {
|
// This ensures we don't accidentally show messages from a different session format
|
||||||
const messages = await this.readJsonlMessages(item.filePath);
|
return messages;
|
||||||
// Even if messages array is empty, we should return it rather than falling back
|
|
||||||
// This ensures we don't accidentally show messages from a different session format
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('[QwenAgentManager] JSONL read path lookup failed:', e);
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[QwenAgentManager] JSONL read path lookup failed:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: legacy JSON session files
|
// Fallback: legacy JSON session files
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ 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;
|
||||||
@@ -50,18 +49,15 @@ export class QwenConnectionHandler {
|
|||||||
let sessionCreated = false;
|
let sessionCreated = false;
|
||||||
let requiresAuth = false;
|
let requiresAuth = false;
|
||||||
|
|
||||||
// Lightweight check if CLI exists (without version info for faster performance)
|
// Check if CLI exists using standard detection (with cached results for better performance)
|
||||||
const detectionResult = await CliDetector.detectQwenCliLightweight(
|
const detectionResult = await CliDetector.detectQwenCli(
|
||||||
/* forceRefresh */ true,
|
/* forceRefresh */ false, // Use cached results when available for better performance
|
||||||
);
|
);
|
||||||
if (!detectionResult.isInstalled) {
|
if (!detectionResult.isInstalled) {
|
||||||
throw new Error(detectionResult.error || 'Qwen CLI not found');
|
throw new Error(detectionResult.error || 'Qwen CLI not found');
|
||||||
}
|
}
|
||||||
console.log('[QwenAgentManager] CLI detected at:', detectionResult.cliPath);
|
console.log('[QwenAgentManager] CLI detected at:', detectionResult.cliPath);
|
||||||
|
|
||||||
// Show warning if CLI version is below minimum requirement
|
|
||||||
await checkCliVersionAndWarn();
|
|
||||||
|
|
||||||
// Build extra CLI arguments (only essential parameters)
|
// Build extra CLI arguments (only essential parameters)
|
||||||
const extraArgs: string[] = [];
|
const extraArgs: string[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import type { AuthenticateUpdateNotification } from '../types/acpTypes.js';
|
import type { AuthenticateUpdateNotification } from '../types/acpTypes.js';
|
||||||
|
|
||||||
|
// Store reference to the authentication notification to allow auto-closing
|
||||||
|
let authNotificationDisposable: { dispose: () => void } | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle authentication update notifications by showing a VS Code notification
|
* Handle authentication update notifications by showing a VS Code notification
|
||||||
* with the authentication URI and a copy button.
|
* with the authentication URI and a copy button.
|
||||||
@@ -18,23 +21,49 @@ export function handleAuthenticateUpdate(
|
|||||||
): void {
|
): void {
|
||||||
const authUri = data._meta.authUri;
|
const authUri = data._meta.authUri;
|
||||||
|
|
||||||
|
// Dismiss any existing authentication notification
|
||||||
|
if (authNotificationDisposable) {
|
||||||
|
authNotificationDisposable.dispose();
|
||||||
|
authNotificationDisposable = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Show an information message with the auth URI and copy button
|
// Show an information message with the auth URI and copy button
|
||||||
vscode.window
|
const notificationPromise = vscode.window.showInformationMessage(
|
||||||
.showInformationMessage(
|
`Qwen Code needs authentication. Click the button below to open the authentication page or copy the link to your browser.`,
|
||||||
`Qwen Code needs authentication. Click the button below to open the authentication page or copy the link to your browser.`,
|
'Open in Browser',
|
||||||
'Open in Browser',
|
'Copy Link',
|
||||||
'Copy Link',
|
);
|
||||||
)
|
|
||||||
.then((selection) => {
|
// Create a simple disposable object
|
||||||
if (selection === 'Open in Browser') {
|
authNotificationDisposable = {
|
||||||
// Open the authentication URI in the default browser
|
dispose: () => {
|
||||||
vscode.env.openExternal(vscode.Uri.parse(authUri));
|
// We can't actually cancel the promise, but we can clear our reference
|
||||||
} else if (selection === 'Copy Link') {
|
},
|
||||||
// Copy the authentication URI to clipboard
|
};
|
||||||
vscode.env.clipboard.writeText(authUri);
|
|
||||||
vscode.window.showInformationMessage(
|
notificationPromise.then((selection) => {
|
||||||
'Authentication link copied to clipboard!',
|
if (selection === 'Open in Browser') {
|
||||||
);
|
// Open the authentication URI in the default browser
|
||||||
}
|
vscode.env.openExternal(vscode.Uri.parse(authUri));
|
||||||
});
|
} else if (selection === 'Copy Link') {
|
||||||
|
// Copy the authentication URI to clipboard
|
||||||
|
vscode.env.clipboard.writeText(authUri);
|
||||||
|
vscode.window.showInformationMessage(
|
||||||
|
'Authentication link copied to clipboard!',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the notification reference after user interaction
|
||||||
|
authNotificationDisposable = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss the authentication notification if it's currently shown
|
||||||
|
*/
|
||||||
|
export function dismissAuthenticateUpdate(): void {
|
||||||
|
if (authNotificationDisposable) {
|
||||||
|
authNotificationDisposable.dispose();
|
||||||
|
authNotificationDisposable = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export const App: React.FC = () => {
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [planEntries, setPlanEntries] = useState<PlanEntry[]>([]);
|
const [planEntries, setPlanEntries] = useState<PlanEntry[]>([]);
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true); // Track if we're still initializing/loading
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(
|
const messagesEndRef = useRef<HTMLDivElement>(
|
||||||
null,
|
null,
|
||||||
) as React.RefObject<HTMLDivElement>;
|
) as React.RefObject<HTMLDivElement>;
|
||||||
@@ -360,6 +361,14 @@ export const App: React.FC = () => {
|
|||||||
completedToolCalls,
|
completedToolCalls,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Set loading state to false after initial mount and when we have authentication info
|
||||||
|
useEffect(() => {
|
||||||
|
// If we have determined authentication status, we're done loading
|
||||||
|
if (isAuthenticated !== null) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
// Handle permission response
|
// Handle permission response
|
||||||
const handlePermissionResponse = useCallback(
|
const handlePermissionResponse = useCallback(
|
||||||
(optionId: string) => {
|
(optionId: string) => {
|
||||||
@@ -666,7 +675,19 @@ export const App: React.FC = () => {
|
|||||||
allMessages.length > 0;
|
allMessages.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-container">
|
<div className="chat-container relative">
|
||||||
|
{/* Top-level loading overlay */}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="bg-background/80 absolute inset-0 z-50 flex items-center justify-center backdrop-blur-sm">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="border-primary mx-auto mb-2 h-8 w-8 animate-spin rounded-full border-b-2"></div>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Preparing Qwen Code...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<SessionSelector
|
<SessionSelector
|
||||||
visible={sessionManagement.showSessionSelector}
|
visible={sessionManagement.showSessionSelector}
|
||||||
sessions={sessionManagement.filteredSessions}
|
sessions={sessionManagement.filteredSessions}
|
||||||
@@ -693,7 +714,7 @@ export const App: React.FC = () => {
|
|||||||
ref={messagesContainerRef}
|
ref={messagesContainerRef}
|
||||||
className="chat-messages messages-container flex-1 overflow-y-auto overflow-x-hidden pt-5 pr-5 pl-5 pb-[140px] flex flex-col relative min-w-0 focus:outline-none [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-thumb]:rounded-sm [&::-webkit-scrollbar-thumb]:hover:bg-white/30 [&>*]:flex [&>*]:gap-0 [&>*]:items-start [&>*]:text-left [&>*]:py-2 [&>*:not(:last-child)]:pb-[8px] [&>*]:flex-col [&>*]:relative [&>*]:animate-[fadeIn_0.2s_ease-in]"
|
className="chat-messages messages-container flex-1 overflow-y-auto overflow-x-hidden pt-5 pr-5 pl-5 pb-[140px] flex flex-col relative min-w-0 focus:outline-none [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-thumb]:rounded-sm [&::-webkit-scrollbar-thumb]:hover:bg-white/30 [&>*]:flex [&>*]:gap-0 [&>*]:items-start [&>*]:text-left [&>*]:py-2 [&>*:not(:last-child)]:pb-[8px] [&>*]:flex-col [&>*]:relative [&>*]:animate-[fadeIn_0.2s_ease-in]"
|
||||||
>
|
>
|
||||||
{!hasContent ? (
|
{!hasContent && !isLoading ? (
|
||||||
isAuthenticated === false ? (
|
isAuthenticated === false ? (
|
||||||
<Onboarding
|
<Onboarding
|
||||||
onLogin={() => {
|
onLogin={() => {
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ import { PanelManager } from '../webview/PanelManager.js';
|
|||||||
import { MessageHandler } from '../webview/MessageHandler.js';
|
import { MessageHandler } from '../webview/MessageHandler.js';
|
||||||
import { WebViewContent } from '../webview/WebViewContent.js';
|
import { WebViewContent } from '../webview/WebViewContent.js';
|
||||||
import { CliInstaller } from '../cli/cliInstaller.js';
|
import { CliInstaller } from '../cli/cliInstaller.js';
|
||||||
|
import { CliVersionChecker } from '../cli/cliVersionChecker.js';
|
||||||
import { getFileName } from './utils/webviewUtils.js';
|
import { getFileName } from './utils/webviewUtils.js';
|
||||||
import { type ApprovalModeValue } from '../types/approvalModeValueTypes.js';
|
import { type ApprovalModeValue } from '../types/approvalModeValueTypes.js';
|
||||||
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
|
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
|
||||||
|
import { dismissAuthenticateUpdate } from '../utils/authNotificationHandler.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WebView Provider Class
|
* WebView Provider Class
|
||||||
@@ -46,7 +48,7 @@ export class WebViewProvider {
|
|||||||
private currentModeId: ApprovalModeValue | null = null;
|
private currentModeId: ApprovalModeValue | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
context: vscode.ExtensionContext,
|
private context: vscode.ExtensionContext,
|
||||||
private extensionUri: vscode.Uri,
|
private extensionUri: vscode.Uri,
|
||||||
) {
|
) {
|
||||||
this.agentManager = new QwenAgentManager();
|
this.agentManager = new QwenAgentManager();
|
||||||
@@ -619,6 +621,21 @@ export class WebViewProvider {
|
|||||||
console.log('[WebViewProvider] CLI path:', cliDetection.cliPath);
|
console.log('[WebViewProvider] CLI path:', cliDetection.cliPath);
|
||||||
console.log('[WebViewProvider] CLI version:', cliDetection.version);
|
console.log('[WebViewProvider] CLI version:', cliDetection.version);
|
||||||
|
|
||||||
|
// Perform version check with throttled notifications
|
||||||
|
const versionChecker = CliVersionChecker.getInstance(this.context);
|
||||||
|
const versionCheckResult = await versionChecker.checkCliVersion(false); // Silent check to avoid popup spam
|
||||||
|
|
||||||
|
if (!versionCheckResult.isSupported) {
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] Qwen CLI version is outdated or unsupported',
|
||||||
|
versionCheckResult,
|
||||||
|
);
|
||||||
|
// Log to output channel instead of showing popup
|
||||||
|
console.warn(
|
||||||
|
`Qwen Code CLI version issue: Installed=${versionCheckResult.version || 'unknown'}, Supported=${versionCheckResult.isSupported}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[WebViewProvider] Connecting to agent...');
|
console.log('[WebViewProvider] Connecting to agent...');
|
||||||
|
|
||||||
@@ -630,6 +647,22 @@ export class WebViewProvider {
|
|||||||
);
|
);
|
||||||
console.log('[WebViewProvider] Agent connected successfully');
|
console.log('[WebViewProvider] Agent connected successfully');
|
||||||
this.agentInitialized = true;
|
this.agentInitialized = true;
|
||||||
|
|
||||||
|
// If authentication is required and autoAuthenticate is false,
|
||||||
|
// send authState message and return without creating session
|
||||||
|
if (connectResult.requiresAuth && !autoAuthenticate) {
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] Authentication required but auto-auth disabled, sending authState and returning',
|
||||||
|
);
|
||||||
|
this.sendMessageToWebView({
|
||||||
|
type: 'authState',
|
||||||
|
data: { authenticated: false },
|
||||||
|
});
|
||||||
|
// Initialize empty conversation to allow browsing history
|
||||||
|
await this.initializeEmptyConversation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (connectResult.requiresAuth) {
|
if (connectResult.requiresAuth) {
|
||||||
this.sendMessageToWebView({
|
this.sendMessageToWebView({
|
||||||
type: 'authState',
|
type: 'authState',
|
||||||
@@ -641,6 +674,9 @@ export class WebViewProvider {
|
|||||||
const sessionReady = await this.loadCurrentSessionMessages(options);
|
const sessionReady = await this.loadCurrentSessionMessages(options);
|
||||||
|
|
||||||
if (sessionReady) {
|
if (sessionReady) {
|
||||||
|
// Dismiss any authentication notifications
|
||||||
|
dismissAuthenticateUpdate();
|
||||||
|
|
||||||
// Notify webview that agent is connected
|
// Notify webview that agent is connected
|
||||||
this.sendMessageToWebView({
|
this.sendMessageToWebView({
|
||||||
type: 'agentConnected',
|
type: 'agentConnected',
|
||||||
@@ -715,6 +751,9 @@ export class WebViewProvider {
|
|||||||
'[WebViewProvider] Force re-login completed successfully',
|
'[WebViewProvider] Force re-login completed successfully',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Dismiss any authentication notifications
|
||||||
|
dismissAuthenticateUpdate();
|
||||||
|
|
||||||
// Send success notification to WebView
|
// Send success notification to WebView
|
||||||
this.sendMessageToWebView({
|
this.sendMessageToWebView({
|
||||||
type: 'loginSuccess',
|
type: 'loginSuccess',
|
||||||
@@ -769,6 +808,9 @@ export class WebViewProvider {
|
|||||||
'[WebViewProvider] Connection refresh completed successfully',
|
'[WebViewProvider] Connection refresh completed successfully',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Dismiss any authentication notifications
|
||||||
|
dismissAuthenticateUpdate();
|
||||||
|
|
||||||
// Notify webview that agent is connected after refresh
|
// Notify webview that agent is connected after refresh
|
||||||
this.sendMessageToWebView({
|
this.sendMessageToWebView({
|
||||||
type: 'agentConnected',
|
type: 'agentConnected',
|
||||||
|
|||||||
@@ -27,24 +27,33 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
|||||||
|
|
||||||
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">
|
||||||
{/* Loading overlay */}
|
|
||||||
{loadingMessage && (
|
|
||||||
<div className="bg-background/80 absolute inset-0 z-50 flex items-center justify-center backdrop-blur-sm">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="border-primary mx-auto mb-2 h-8 w-8 animate-spin rounded-full border-b-2"></div>
|
|
||||||
<p className="text-muted-foreground text-sm">{loadingMessage}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-8 w-full">
|
<div className="flex flex-col items-center gap-8 w-full">
|
||||||
{/* Qwen Logo */}
|
{/* Qwen Logo */}
|
||||||
<div className="flex flex-col items-center gap-6">
|
<div className="flex flex-col items-center gap-6">
|
||||||
<img
|
{iconUri ? (
|
||||||
src={iconUri}
|
<img
|
||||||
alt="Qwen Logo"
|
src={iconUri}
|
||||||
className="w-[60px] h-[60px] object-contain"
|
alt="Qwen Logo"
|
||||||
/>
|
className="w-[60px] h-[60px] object-contain"
|
||||||
|
onError={(e) => {
|
||||||
|
// Fallback to a div with text if image fails to load
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
target.style.display = 'none';
|
||||||
|
const parent = target.parentElement;
|
||||||
|
if (parent) {
|
||||||
|
const fallback = document.createElement('div');
|
||||||
|
fallback.className =
|
||||||
|
'w-[60px] h-[60px] flex items-center justify-center text-2xl font-bold';
|
||||||
|
fallback.textContent = 'Q';
|
||||||
|
parent.appendChild(fallback);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-[60px] h-[60px] flex items-center justify-center text-2xl font-bold bg-gray-200 rounded">
|
||||||
|
Q
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-[15px] text-app-primary-foreground leading-normal font-normal max-w-[400px]">
|
<div className="text-[15px] text-app-primary-foreground leading-normal font-normal max-w-[400px]">
|
||||||
{description}
|
{description}
|
||||||
|
|||||||
Reference in New Issue
Block a user