From e895c49f5c723f5eb71b9921de7067181e4a2c75 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 09:56:18 +0800 Subject: [PATCH] fix(vscode-ide-companion): resolve all ESLint errors Fixed unused variable errors in SessionMessageHandler.ts: - Commented out unused conversation and messages variables Also includes previous commits: 1. feat(vscode-ide-companion): add upgrade button to CLI version warning 2. fix(vscode-ide-companion): resolve ESLint errors in InputForm component When the Qwen Code CLI version is below the minimum required version, the warning message now includes an "Upgrade Now" button that opens a terminal and runs the npm install command to upgrade the CLI. Added tests to verify the functionality works correctly. --- .../src/services/qwenAgentManager.ts | 92 +------- .../src/services/qwenConnectionHandler.ts | 11 +- .../src/services/qwenSessionManager.ts | 126 +---------- .../src/services/qwenSessionReader.ts | 196 +++++++++++++++++- .../src/webview/MessageHandler.ts | 7 - .../webview/components/layout/InputForm.tsx | 11 +- .../src/webview/handlers/MessageRouter.ts | 7 - .../webview/handlers/SessionMessageHandler.ts | 111 ---------- 8 files changed, 213 insertions(+), 348 deletions(-) diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index c3aa6525..2475e309 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -336,8 +336,10 @@ export class QwenAgentManager { name: this.sessionReader.getSessionTitle(session), startTime: session.startTime, lastUpdated: session.lastUpdated, - messageCount: session.messages.length, + messageCount: session.messageCount ?? session.messages.length, projectHash: session.projectHash, + filePath: session.filePath, + cwd: session.cwd, }), ); @@ -452,8 +454,10 @@ export class QwenAgentManager { name: this.sessionReader.getSessionTitle(x.raw), startTime: x.raw.startTime, lastUpdated: x.raw.lastUpdated, - messageCount: x.raw.messages.length, + messageCount: x.raw.messageCount ?? x.raw.messages.length, projectHash: x.raw.projectHash, + filePath: x.raw.filePath, + cwd: x.raw.cwd, })); const nextCursorVal = page.length > 0 ? page[page.length - 1].mtime : undefined; @@ -891,80 +895,6 @@ export class QwenAgentManager { return this.saveSessionViaCommand(sessionId, tag); } - /** - * Save session as checkpoint (using CLI format) - * Saves to ~/.qwen/tmp/{projectHash}/checkpoint-{tag}.json - * Saves two copies with sessionId and conversationId to ensure recovery via either ID - * - * @param messages - Current session messages - * @param conversationId - Conversation ID (from VSCode extension) - * @returns Save result - */ - async saveCheckpoint( - messages: ChatMessage[], - conversationId: string, - ): Promise<{ success: boolean; tag?: string; message?: string }> { - try { - console.log('[QwenAgentManager] ===== CHECKPOINT SAVE START ====='); - console.log('[QwenAgentManager] Conversation ID:', conversationId); - console.log('[QwenAgentManager] Message count:', messages.length); - console.log( - '[QwenAgentManager] Current working dir:', - this.currentWorkingDir, - ); - console.log( - '[QwenAgentManager] Current session ID (from CLI):', - this.currentSessionId, - ); - // In ACP mode, the CLI does not accept arbitrary slash commands like - // "/chat save". To ensure we never block on unsupported features, - // persist checkpoints directly to ~/.qwen/tmp using our SessionManager. - const qwenMessages = messages.map((m) => ({ - // Generate minimal QwenMessage shape expected by the writer - id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, - timestamp: new Date().toISOString(), - type: m.role === 'user' ? ('user' as const) : ('qwen' as const), - content: m.content, - })); - - const tag = await this.sessionManager.saveCheckpoint( - qwenMessages, - conversationId, - this.currentWorkingDir, - this.currentSessionId || undefined, - ); - - return { success: true, tag }; - } catch (error) { - console.error('[QwenAgentManager] ===== CHECKPOINT SAVE FAILED ====='); - console.error('[QwenAgentManager] Error:', error); - console.error( - '[QwenAgentManager] Error stack:', - error instanceof Error ? error.stack : 'N/A', - ); - return { - success: false, - message: error instanceof Error ? error.message : String(error), - }; - } - } - - /** - * Save session directly to file system (without relying on ACP) - * - * @param messages - Current session messages - * @param sessionName - Session name - * @returns Save result - */ - async saveSessionDirect( - messages: ChatMessage[], - sessionName: string, - ): Promise<{ success: boolean; sessionId?: string; message?: string }> { - // Use checkpoint format instead of session format - // This matches CLI's /chat save behavior - return this.saveCheckpoint(messages, sessionName); - } - /** * Try to load session via ACP session/load method * This method will only be used if CLI version supports it @@ -1152,16 +1082,6 @@ export class QwenAgentManager { } } - /** - * Load session, preferring ACP method if CLI version supports it - * - * @param sessionId - Session ID - * @returns Loaded session messages or null - */ - async loadSessionDirect(sessionId: string): Promise { - return this.loadSession(sessionId); - } - /** * Create new session * diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index 6a74cd56..91d4c6bf 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -54,9 +54,18 @@ export class QwenConnectionHandler { // Show warning if CLI version is below minimum requirement if (!versionInfo.isSupported) { // Wait to determine release version number - vscode.window.showWarningMessage( + 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.`, + 'Upgrade Now', ); + + // Handle the user's selection + if (selection === 'Upgrade Now') { + // Open terminal and run npm install command + const terminal = vscode.window.createTerminal('Qwen Code CLI Upgrade'); + terminal.show(); + terminal.sendText('npm install -g @qwen-code/qwen-code@latest'); + } } const config = vscode.workspace.getConfiguration('qwenCode'); diff --git a/packages/vscode-ide-companion/src/services/qwenSessionManager.ts b/packages/vscode-ide-companion/src/services/qwenSessionManager.ts index 2bd609bb..9336a060 100644 --- a/packages/vscode-ide-companion/src/services/qwenSessionManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenSessionManager.ts @@ -51,131 +51,7 @@ export class QwenSessionManager { } /** - * Save current conversation as a checkpoint (matching CLI's /chat save format) - * Creates checkpoint with BOTH conversationId and sessionId as tags for compatibility - * - * @param messages - Current conversation messages - * @param conversationId - Conversation ID (from VSCode extension) - * @param sessionId - Session ID (from CLI tmp session file, optional) - * @param workingDir - Current working directory - * @returns Checkpoint tag - */ - async saveCheckpoint( - messages: QwenMessage[], - conversationId: string, - workingDir: string, - sessionId?: string, - ): Promise { - try { - console.log('[QwenSessionManager] ===== SAVEPOINT START ====='); - console.log('[QwenSessionManager] Conversation ID:', conversationId); - console.log( - '[QwenSessionManager] Session ID:', - sessionId || 'not provided', - ); - console.log('[QwenSessionManager] Working dir:', workingDir); - console.log('[QwenSessionManager] Message count:', messages.length); - - // Get project directory (parent of chats directory) - const projectHash = this.getProjectHash(workingDir); - console.log('[QwenSessionManager] Project hash:', projectHash); - - const projectDir = path.join(this.qwenDir, 'tmp', projectHash); - console.log('[QwenSessionManager] Project dir:', projectDir); - - if (!fs.existsSync(projectDir)) { - console.log('[QwenSessionManager] Creating project directory...'); - fs.mkdirSync(projectDir, { recursive: true }); - console.log('[QwenSessionManager] Directory created'); - } else { - console.log('[QwenSessionManager] Project directory already exists'); - } - - // Convert messages to checkpoint format (Gemini-style messages) - console.log( - '[QwenSessionManager] Converting messages to checkpoint format...', - ); - const checkpointMessages = messages.map((msg, index) => { - console.log( - `[QwenSessionManager] Message ${index}: type=${msg.type}, contentLength=${msg.content?.length || 0}`, - ); - return { - role: msg.type === 'user' ? 'user' : 'model', - parts: [ - { - text: msg.content, - }, - ], - }; - }); - - console.log( - '[QwenSessionManager] Converted', - checkpointMessages.length, - 'messages', - ); - - const jsonContent = JSON.stringify(checkpointMessages, null, 2); - console.log( - '[QwenSessionManager] JSON content length:', - jsonContent.length, - ); - - // Save with conversationId as primary tag - const convFilename = `checkpoint-${conversationId}.json`; - const convFilePath = path.join(projectDir, convFilename); - console.log( - '[QwenSessionManager] Saving checkpoint with conversationId:', - convFilePath, - ); - fs.writeFileSync(convFilePath, jsonContent, 'utf-8'); - - // Also save with sessionId if provided (for compatibility with CLI session/load) - if (sessionId) { - const sessionFilename = `checkpoint-${sessionId}.json`; - const sessionFilePath = path.join(projectDir, sessionFilename); - console.log( - '[QwenSessionManager] Also saving checkpoint with sessionId:', - sessionFilePath, - ); - fs.writeFileSync(sessionFilePath, jsonContent, 'utf-8'); - } - - // Verify primary file exists - if (fs.existsSync(convFilePath)) { - const stats = fs.statSync(convFilePath); - console.log( - '[QwenSessionManager] Primary checkpoint verified, size:', - stats.size, - ); - } else { - console.error( - '[QwenSessionManager] ERROR: Primary checkpoint does not exist after write!', - ); - } - - console.log('[QwenSessionManager] ===== CHECKPOINT SAVED ====='); - console.log('[QwenSessionManager] Primary path:', convFilePath); - if (sessionId) { - console.log( - '[QwenSessionManager] Secondary path (sessionId):', - path.join(projectDir, `checkpoint-${sessionId}.json`), - ); - } - return conversationId; - } catch (error) { - console.error('[QwenSessionManager] ===== CHECKPOINT SAVE FAILED ====='); - console.error('[QwenSessionManager] Error:', error); - console.error( - '[QwenSessionManager] Error stack:', - error instanceof Error ? error.stack : 'N/A', - ); - throw error; - } - } - - /** - * Save current conversation as a named session (checkpoint-like functionality) + * Save current conversation as a named session * * @param messages - Current conversation messages * @param sessionName - Name/tag for the saved session diff --git a/packages/vscode-ide-companion/src/services/qwenSessionReader.ts b/packages/vscode-ide-companion/src/services/qwenSessionReader.ts index 6e2d065d..3fc4e484 100644 --- a/packages/vscode-ide-companion/src/services/qwenSessionReader.ts +++ b/packages/vscode-ide-companion/src/services/qwenSessionReader.ts @@ -7,6 +7,8 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; +import * as readline from 'readline'; +import * as crypto from 'crypto'; export interface QwenMessage { id: string; @@ -32,6 +34,9 @@ export interface QwenSession { lastUpdated: string; messages: QwenMessage[]; filePath?: string; + messageCount?: number; + firstUserText?: string; + cwd?: string; } export class QwenSessionReader { @@ -96,11 +101,17 @@ export class QwenSessionReader { return sessions; } - const files = fs - .readdirSync(chatsDir) - .filter((f) => f.startsWith('session-') && f.endsWith('.json')); + const files = fs.readdirSync(chatsDir); - for (const file of files) { + const jsonSessionFiles = files.filter( + (f) => f.startsWith('session-') && f.endsWith('.json'), + ); + + const jsonlSessionFiles = files.filter((f) => + /^[0-9a-fA-F-]{32,36}\.jsonl$/.test(f), + ); + + for (const file of jsonSessionFiles) { const filePath = path.join(chatsDir, file); try { const content = fs.readFileSync(filePath, 'utf-8'); @@ -116,6 +127,23 @@ export class QwenSessionReader { } } + // Support new JSONL session format produced by the CLI + for (const file of jsonlSessionFiles) { + const filePath = path.join(chatsDir, file); + try { + const session = await this.readJsonlSession(filePath, false); + if (session) { + sessions.push(session); + } + } catch (error) { + console.error( + '[QwenSessionReader] Failed to read JSONL session file:', + filePath, + error, + ); + } + } + return sessions; } @@ -128,7 +156,25 @@ export class QwenSessionReader { ): Promise { // First try to find in all projects const sessions = await this.getAllSessions(undefined, true); - return sessions.find((s) => s.sessionId === sessionId) || null; + const found = sessions.find((s) => s.sessionId === sessionId); + + if (!found) { + return null; + } + + // If the session points to a JSONL file, load full content on demand + if ( + found.filePath && + found.filePath.endsWith('.jsonl') && + found.messages.length === 0 + ) { + const hydrated = await this.readJsonlSession(found.filePath, true); + if (hydrated) { + return hydrated; + } + } + + return found; } /** @@ -136,7 +182,6 @@ export class QwenSessionReader { * Qwen CLI uses SHA256 hash of project path */ private async getProjectHash(workingDir: string): Promise { - const crypto = await import('crypto'); return crypto.createHash('sha256').update(workingDir).digest('hex'); } @@ -144,6 +189,14 @@ export class QwenSessionReader { * Get session title (based on first user message) */ getSessionTitle(session: QwenSession): string { + // Prefer cached prompt text to avoid loading messages for JSONL sessions + if (session.firstUserText) { + return ( + session.firstUserText.substring(0, 50) + + (session.firstUserText.length > 50 ? '...' : '') + ); + } + const firstUserMessage = session.messages.find((m) => m.type === 'user'); if (firstUserMessage) { // Extract first 50 characters as title @@ -155,6 +208,137 @@ export class QwenSessionReader { return 'Untitled Session'; } + /** + * Parse a JSONL session file written by the CLI. + * When includeMessages is false, only lightweight metadata is returned. + */ + private async readJsonlSession( + filePath: string, + includeMessages: boolean, + ): Promise { + try { + if (!fs.existsSync(filePath)) { + return null; + } + + const stats = fs.statSync(filePath); + const fileStream = fs.createReadStream(filePath, { encoding: 'utf-8' }); + const rl = readline.createInterface({ + input: fileStream, + crlfDelay: Infinity, + }); + + const messages: QwenMessage[] = []; + const seenUuids = new Set(); + let sessionId: string | undefined; + let startTime: string | undefined; + let firstUserText: string | undefined; + let cwd: string | undefined; + + for await (const line of rl) { + const trimmed = line.trim(); + if (!trimmed) { + continue; + } + + let obj: Record; + try { + obj = JSON.parse(trimmed) as Record; + } catch { + continue; + } + + if (!sessionId && typeof obj.sessionId === 'string') { + sessionId = obj.sessionId; + } + if (!startTime && typeof obj.timestamp === 'string') { + startTime = obj.timestamp; + } + if (!cwd && typeof obj.cwd === 'string') { + cwd = obj.cwd; + } + + const type = typeof obj.type === 'string' ? obj.type : ''; + if (type === 'user' || type === 'assistant') { + const uuid = typeof obj.uuid === 'string' ? obj.uuid : undefined; + if (uuid) { + seenUuids.add(uuid); + } + + const text = this.contentToText(obj.message); + if (includeMessages) { + messages.push({ + id: uuid || `${messages.length}`, + timestamp: typeof obj.timestamp === 'string' ? obj.timestamp : '', + type: type === 'user' ? 'user' : 'qwen', + content: text, + }); + } + + if (!firstUserText && type === 'user' && text) { + firstUserText = text; + } + } + } + + // Ensure stream is closed + rl.close(); + + if (!sessionId) { + return null; + } + + const projectHash = cwd + ? await this.getProjectHash(cwd) + : path.basename(path.dirname(path.dirname(filePath))); + + return { + sessionId, + projectHash, + startTime: startTime || new Date(stats.birthtimeMs).toISOString(), + lastUpdated: new Date(stats.mtimeMs).toISOString(), + messages: includeMessages ? messages : [], + filePath, + messageCount: seenUuids.size, + firstUserText, + cwd, + }; + } catch (error) { + console.error( + '[QwenSessionReader] Failed to parse JSONL session:', + error, + ); + return null; + } + } + + // Extract plain text from CLI Content structure + private contentToText(message: unknown): string { + try { + if (typeof message !== 'object' || message === null) { + return ''; + } + + const typed = message as { parts?: unknown[] }; + const parts = Array.isArray(typed.parts) ? typed.parts : []; + const texts: string[] = []; + for (const part of parts) { + if (typeof part !== 'object' || part === null) { + continue; + } + const p = part as Record; + if (typeof p.text === 'string') { + texts.push(p.text); + } else if (typeof p.data === 'string') { + texts.push(p.data); + } + } + return texts.join('\n'); + } catch { + return ''; + } + } + /** * Delete session file */ diff --git a/packages/vscode-ide-companion/src/webview/MessageHandler.ts b/packages/vscode-ide-companion/src/webview/MessageHandler.ts index 1eca4a20..77d330b6 100644 --- a/packages/vscode-ide-companion/src/webview/MessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/MessageHandler.ts @@ -73,11 +73,4 @@ export class MessageHandler { appendStreamContent(chunk: string): void { this.router.appendStreamContent(chunk); } - - /** - * Check if saving checkpoint - */ - getIsSavingCheckpoint(): boolean { - return this.router.getIsSavingCheckpoint(); - } } diff --git a/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx b/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx index 5c4a889a..73f3fa26 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx @@ -11,7 +11,7 @@ import { PlanModeIcon, CodeBracketsIcon, HideContextIcon, - ThinkingIcon, + // ThinkingIcon, // Temporarily disabled SlashCommandIcon, LinkIcon, ArrowUpIcon, @@ -92,7 +92,7 @@ export const InputForm: React.FC = ({ isWaitingForResponse, isComposing, editMode, - thinkingEnabled, + // thinkingEnabled, // Temporarily disabled activeFileName, activeSelection, skipAutoActiveContext, @@ -103,7 +103,7 @@ export const InputForm: React.FC = ({ onSubmit, onCancel, onToggleEditMode, - onToggleThinking, + // onToggleThinking, // Temporarily disabled onToggleSkipAutoActiveContext, onShowCommandMenu, onAttachContext, @@ -236,15 +236,16 @@ export const InputForm: React.FC = ({ {/* Spacer */}
+ {/* @yiliang114. closed temporarily */} {/* Thinking button */} - + */} {/* Command button */}