From 32e17f8b5877676677cc0097a99eb809374df61a Mon Sep 17 00:00:00 2001 From: LaZzyMan Date: Mon, 9 Feb 2026 11:38:20 +0800 Subject: [PATCH 1/4] fix: normalize Windows paths to lowercase for case-insensitive session matching Fixes #1760 Windows file system is case-insensitive (e:\work equals E:\work), but string hashing is case-sensitive, causing different session directories for the same physical path. Solution: normalize paths to lowercase on Windows before hashing to ensure consistent session directory across different case variations. --- packages/core/src/utils/paths.test.ts | 78 +++++++++++++++++++ packages/core/src/utils/paths.ts | 7 +- .../src/services/qwenSessionManager.ts | 9 ++- .../src/services/qwenSessionReader.ts | 9 ++- 4 files changed, 98 insertions(+), 5 deletions(-) diff --git a/packages/core/src/utils/paths.test.ts b/packages/core/src/utils/paths.test.ts index 1c4ee0225f5..9f8b63ef97b 100644 --- a/packages/core/src/utils/paths.test.ts +++ b/packages/core/src/utils/paths.test.ts @@ -17,6 +17,7 @@ import { isSubpath, shortenPath, tildeifyPath, + getProjectHash, } from './paths.js'; import type { Config } from '../config/config.js'; @@ -770,3 +771,80 @@ describe('shortenPath', () => { expect(result.length).toBeLessThanOrEqual(35); }); }); + +describe('getProjectHash', () => { + it('should generate consistent hashes for the same path', () => { + const projectRoot = '/test/project'; + const hash1 = getProjectHash(projectRoot); + const hash2 = getProjectHash(projectRoot); + + expect(hash1).toBe(hash2); + expect(hash1).toHaveLength(64); // SHA256 produces 64 hex characters + }); + + it('should generate different hashes for different paths', () => { + const hash1 = getProjectHash('/test/project1'); + const hash2 = getProjectHash('/test/project2'); + + expect(hash1).not.toBe(hash2); + }); + + it('should generate case-insensitive hashes on Windows', () => { + const platformSpy = vi.spyOn(os, 'platform'); + + // Simulate Windows platform + platformSpy.mockReturnValue('win32'); + + const lowerCasePath = 'c:\\users\\test\\project'; + const upperCasePath = 'C:\\Users\\Test\\Project'; + const mixedCasePath = 'c:\\Users\\TEST\\project'; + + const hash1 = getProjectHash(lowerCasePath); + const hash2 = getProjectHash(upperCasePath); + const hash3 = getProjectHash(mixedCasePath); + + // On Windows, all different case variations should produce the same hash + expect(hash1).toBe(hash2); + expect(hash2).toBe(hash3); + + platformSpy.mockRestore(); + }); + + it('should generate case-sensitive hashes on non-Windows platforms', () => { + const platformSpy = vi.spyOn(os, 'platform'); + + // Simulate Unix/Linux platform + platformSpy.mockReturnValue('linux'); + + const lowerCasePath = '/home/user/project'; + const upperCasePath = '/HOME/USER/PROJECT'; + + const hash1 = getProjectHash(lowerCasePath); + const hash2 = getProjectHash(upperCasePath); + + // On non-Windows platforms, different case should produce different hashes + expect(hash1).not.toBe(hash2); + + platformSpy.mockRestore(); + }); + + it('should handle Windows drive letter variations', () => { + const platformSpy = vi.spyOn(os, 'platform'); + platformSpy.mockReturnValue('win32'); + + // Common Windows scenarios where users might have different drive letter cases + const scenarios = [ + ['e:\\work', 'E:\\work'], + ['e:\\work', 'E:\\WORK'], + ['c:\\projects\\myapp', 'C:\\Projects\\MyApp'], + ]; + + for (const [path1, path2] of scenarios) { + const hash1 = getProjectHash(path1); + const hash2 = getProjectHash(path2); + expect(hash1).toBe(hash2); + } + + platformSpy.mockRestore(); + }); +}); diff --git a/packages/core/src/utils/paths.ts b/packages/core/src/utils/paths.ts index 6b492c92291..96856a5dcc6 100644 --- a/packages/core/src/utils/paths.ts +++ b/packages/core/src/utils/paths.ts @@ -190,11 +190,16 @@ export function unescapePath(filePath: string): string { /** * Generates a unique hash for a project based on its root path. + * On Windows, paths are case-insensitive, so we normalize to lowercase + * to ensure the same physical path always produces the same hash. * @param projectRoot The absolute path to the project's root directory. * @returns A SHA256 hash of the project root path. */ export function getProjectHash(projectRoot: string): string { - return crypto.createHash('sha256').update(projectRoot).digest('hex'); + // On Windows, normalize path to lowercase for case-insensitive matching + const normalizedPath = + os.platform() === 'win32' ? projectRoot.toLowerCase() : projectRoot; + return crypto.createHash('sha256').update(normalizedPath).digest('hex'); } /** diff --git a/packages/vscode-ide-companion/src/services/qwenSessionManager.ts b/packages/vscode-ide-companion/src/services/qwenSessionManager.ts index 9336a060bfd..5c9f3d20506 100644 --- a/packages/vscode-ide-companion/src/services/qwenSessionManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenSessionManager.ts @@ -29,10 +29,15 @@ export class QwenSessionManager { /** * Calculate project hash (same as CLI) - * Qwen CLI uses SHA256 hash of the project path + * Qwen CLI uses SHA256 hash of the project path. + * On Windows, paths are case-insensitive, so we normalize to lowercase + * to ensure the same physical path always produces the same hash. */ private getProjectHash(workingDir: string): string { - return crypto.createHash('sha256').update(workingDir).digest('hex'); + // On Windows, normalize path to lowercase for case-insensitive matching + const normalizedPath = + os.platform() === 'win32' ? workingDir.toLowerCase() : workingDir; + return crypto.createHash('sha256').update(normalizedPath).digest('hex'); } /** diff --git a/packages/vscode-ide-companion/src/services/qwenSessionReader.ts b/packages/vscode-ide-companion/src/services/qwenSessionReader.ts index 3fc4e484f23..612cd2425d2 100644 --- a/packages/vscode-ide-companion/src/services/qwenSessionReader.ts +++ b/packages/vscode-ide-companion/src/services/qwenSessionReader.ts @@ -179,10 +179,15 @@ export class QwenSessionReader { /** * Calculate project hash (needs to be consistent with Qwen CLI) - * Qwen CLI uses SHA256 hash of project path + * Qwen CLI uses SHA256 hash of project path. + * On Windows, paths are case-insensitive, so we normalize to lowercase + * to ensure the same physical path always produces the same hash. */ private async getProjectHash(workingDir: string): Promise { - return crypto.createHash('sha256').update(workingDir).digest('hex'); + // On Windows, normalize path to lowercase for case-insensitive matching + const normalizedPath = + os.platform() === 'win32' ? workingDir.toLowerCase() : workingDir; + return crypto.createHash('sha256').update(normalizedPath).digest('hex'); } /** From 37c3a38bb192491fb02347e45a6e6a2f2747834f Mon Sep 17 00:00:00 2001 From: LaZzyMan Date: Mon, 9 Feb 2026 14:23:37 +0800 Subject: [PATCH 2/4] fix: use centralized getProjectHash in Storage class Ensures consistent Windows path normalization across all path hashing. Previously Storage used its own getFilePathHash() which didn't apply Windows lowercase normalization, causing test failures on Windows CI. --- packages/core/src/config/storage.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/core/src/config/storage.ts b/packages/core/src/config/storage.ts index 8ef0283c509..4a710daf01e 100644 --- a/packages/core/src/config/storage.ts +++ b/packages/core/src/config/storage.ts @@ -6,8 +6,8 @@ import * as path from 'node:path'; import * as os from 'node:os'; -import * as crypto from 'node:crypto'; import * as fs from 'node:fs'; +import { getProjectHash } from '../utils/paths.js'; export const QWEN_DIR = '.qwen'; export const GOOGLE_ACCOUNTS_FILENAME = 'google_accounts.json'; @@ -88,7 +88,7 @@ export class Storage { } getProjectTempDir(): string { - const hash = this.getFilePathHash(this.getProjectRoot()); + const hash = getProjectHash(this.getProjectRoot()); const tempDir = Storage.getGlobalTempDir(); return path.join(tempDir, hash); } @@ -105,12 +105,8 @@ export class Storage { return this.targetDir; } - private getFilePathHash(filePath: string): string { - return crypto.createHash('sha256').update(filePath).digest('hex'); - } - getHistoryDir(): string { - const hash = this.getFilePathHash(this.getProjectRoot()); + const hash = getProjectHash(this.getProjectRoot()); const historyDir = path.join(Storage.getGlobalQwenDir(), 'history'); return path.join(historyDir, hash); } From d3dfc26dea18c93ff703497b77fd6397394568ce Mon Sep 17 00:00:00 2001 From: LaZzyMan Date: Mon, 9 Feb 2026 15:30:06 +0800 Subject: [PATCH 3/4] fix ci test --- packages/core/src/config/storage.ts | 42 +++++++++++++-- packages/core/src/core/logger.test.ts | 4 +- packages/core/src/utils/paths.test.ts | 53 +++++++++++++++++++ packages/core/src/utils/paths.ts | 11 ++++ .../src/services/qwenSessionManager.ts | 50 +++++++++++------ .../src/services/qwenSessionReader.ts | 49 +++++++++++------ scripts/telemetry_utils.js | 19 +++++-- 7 files changed, 186 insertions(+), 42 deletions(-) diff --git a/packages/core/src/config/storage.ts b/packages/core/src/config/storage.ts index 4a710daf01e..8589f5ae2d5 100644 --- a/packages/core/src/config/storage.ts +++ b/packages/core/src/config/storage.ts @@ -7,7 +7,7 @@ import * as path from 'node:path'; import * as os from 'node:os'; import * as fs from 'node:fs'; -import { getProjectHash } from '../utils/paths.js'; +import { getProjectHash, getLegacyProjectHash } from '../utils/paths.js'; export const QWEN_DIR = '.qwen'; export const GOOGLE_ACCOUNTS_FILENAME = 'google_accounts.json'; @@ -90,7 +90,25 @@ export class Storage { getProjectTempDir(): string { const hash = getProjectHash(this.getProjectRoot()); const tempDir = Storage.getGlobalTempDir(); - return path.join(tempDir, hash); + const targetDir = path.join(tempDir, hash); + + // Backward compatibility: On Windows, check if legacy directory exists + // and migrate it to the new normalized path + if (os.platform() === 'win32' && !fs.existsSync(targetDir)) { + const legacyHash = getLegacyProjectHash(this.getProjectRoot()); + const legacyDir = path.join(tempDir, legacyHash); + + if (fs.existsSync(legacyDir) && legacyHash !== hash) { + try { + // Attempt to rename/migrate the directory + fs.renameSync(legacyDir, targetDir); + } catch (_error) { + // Silent fallback: if migration fails, continue with the new path + } + } + } + + return targetDir; } ensureProjectTempDirExists(): void { @@ -108,7 +126,25 @@ export class Storage { getHistoryDir(): string { const hash = getProjectHash(this.getProjectRoot()); const historyDir = path.join(Storage.getGlobalQwenDir(), 'history'); - return path.join(historyDir, hash); + const targetDir = path.join(historyDir, hash); + + // Backward compatibility: On Windows, check if legacy directory exists + // and migrate it to the new normalized path + if (os.platform() === 'win32' && !fs.existsSync(targetDir)) { + const legacyHash = getLegacyProjectHash(this.getProjectRoot()); + const legacyDir = path.join(historyDir, legacyHash); + + if (fs.existsSync(legacyDir) && legacyHash !== hash) { + try { + // Attempt to rename/migrate the directory + fs.renameSync(legacyDir, targetDir); + } catch (_error) { + // Silent fallback: if migration fails, continue with the new path + } + } + } + + return targetDir; } getWorkspaceSettingsPath(): string { diff --git a/packages/core/src/core/logger.test.ts b/packages/core/src/core/logger.test.ts index de3fc3f787c..c973c02dd3b 100644 --- a/packages/core/src/core/logger.test.ts +++ b/packages/core/src/core/logger.test.ts @@ -21,11 +21,11 @@ import { decodeTagName, } from './logger.js'; import { Storage } from '../config/storage.js'; +import { getProjectHash } from '../utils/paths.js'; import { promises as fs, existsSync } from 'node:fs'; import path from 'node:path'; import type { Content } from '@google/genai'; -import crypto from 'node:crypto'; import os from 'node:os'; const GEMINI_DIR_NAME = '.qwen'; @@ -34,7 +34,7 @@ const LOG_FILE_NAME = 'logs.json'; const CHECKPOINT_FILE_NAME = 'checkpoint.json'; const projectDir = process.cwd(); -const hash = crypto.createHash('sha256').update(projectDir).digest('hex'); +const hash = getProjectHash(projectDir); const TEST_HOME_DIR = path.join(os.tmpdir(), 'qwen-core-logger-home'); let originalHome: string | undefined; diff --git a/packages/core/src/utils/paths.test.ts b/packages/core/src/utils/paths.test.ts index 9f8b63ef97b..2584e650352 100644 --- a/packages/core/src/utils/paths.test.ts +++ b/packages/core/src/utils/paths.test.ts @@ -18,6 +18,7 @@ import { shortenPath, tildeifyPath, getProjectHash, + getLegacyProjectHash, } from './paths.js'; import type { Config } from '../config/config.js'; @@ -848,3 +849,55 @@ describe('getProjectHash', () => { platformSpy.mockRestore(); }); }); + +describe('getLegacyProjectHash', () => { + it('should always be case-sensitive regardless of platform', () => { + const platformSpy = vi.spyOn(os, 'platform'); + + // Test on Windows - should still be case-sensitive + platformSpy.mockReturnValue('win32'); + const lowerCaseHash = getLegacyProjectHash('c:\\users\\test\\project'); + const upperCaseHash = getLegacyProjectHash('C:\\USERS\\TEST\\PROJECT'); + expect(lowerCaseHash).not.toBe(upperCaseHash); + + // Test on Linux - should be case-sensitive + platformSpy.mockReturnValue('linux'); + const lowerCaseHashLinux = getLegacyProjectHash('/home/user/project'); + const upperCaseHashLinux = getLegacyProjectHash('/HOME/USER/PROJECT'); + expect(lowerCaseHashLinux).not.toBe(upperCaseHashLinux); + + platformSpy.mockRestore(); + }); + + it('should generate different hash than getProjectHash on Windows with mixed case', () => { + const platformSpy = vi.spyOn(os, 'platform'); + platformSpy.mockReturnValue('win32'); + + const mixedCasePath = 'C:\\Users\\Test\\Project'; + const legacyHash = getLegacyProjectHash(mixedCasePath); + const newHash = getProjectHash(mixedCasePath); + + // They should be different because getProjectHash normalizes to lowercase + expect(legacyHash).not.toBe(newHash); + + // But both lowercase paths should match the new hash + const lowercaseNewHash = getProjectHash(mixedCasePath.toLowerCase()); + expect(newHash).toBe(lowercaseNewHash); + + platformSpy.mockRestore(); + }); + + it('should match getProjectHash on non-Windows platforms', () => { + const platformSpy = vi.spyOn(os, 'platform'); + platformSpy.mockReturnValue('linux'); + + const testPath = '/home/user/project'; + const legacyHash = getLegacyProjectHash(testPath); + const newHash = getProjectHash(testPath); + + // On non-Windows platforms, both should be identical + expect(legacyHash).toBe(newHash); + + platformSpy.mockRestore(); + }); +}); diff --git a/packages/core/src/utils/paths.ts b/packages/core/src/utils/paths.ts index 96856a5dcc6..f90e9b4e944 100644 --- a/packages/core/src/utils/paths.ts +++ b/packages/core/src/utils/paths.ts @@ -202,6 +202,17 @@ export function getProjectHash(projectRoot: string): string { return crypto.createHash('sha256').update(normalizedPath).digest('hex'); } +/** + * Generates a hash using the legacy algorithm (without case normalization). + * This is used for backward compatibility to locate session directories + * created before the case-insensitive fix on Windows. + * @param projectRoot The absolute path to the project's root directory. + * @returns A SHA256 hash of the project root path without normalization. + */ +export function getLegacyProjectHash(projectRoot: string): string { + return crypto.createHash('sha256').update(projectRoot).digest('hex'); +} + /** * Checks if a path is a subpath of another path. * @param parentPath The parent path. diff --git a/packages/vscode-ide-companion/src/services/qwenSessionManager.ts b/packages/vscode-ide-companion/src/services/qwenSessionManager.ts index 5c9f3d20506..922ca78e5a5 100644 --- a/packages/vscode-ide-companion/src/services/qwenSessionManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenSessionManager.ts @@ -8,6 +8,10 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import * as crypto from 'crypto'; +import { + getProjectHash, + getLegacyProjectHash, +} from '@qwen-code/qwen-code-core/src/utils/paths.js'; import type { QwenSession, QwenMessage } from './qwenSessionReader.js'; /** @@ -28,24 +32,36 @@ export class QwenSessionManager { } /** - * Calculate project hash (same as CLI) - * Qwen CLI uses SHA256 hash of the project path. - * On Windows, paths are case-insensitive, so we normalize to lowercase - * to ensure the same physical path always produces the same hash. - */ - private getProjectHash(workingDir: string): string { - // On Windows, normalize path to lowercase for case-insensitive matching - const normalizedPath = - os.platform() === 'win32' ? workingDir.toLowerCase() : workingDir; - return crypto.createHash('sha256').update(normalizedPath).digest('hex'); - } - - /** - * Get the session directory for a project + * Get the session directory for a project with backward compatibility */ private getSessionDir(workingDir: string): string { - const projectHash = this.getProjectHash(workingDir); - return path.join(this.qwenDir, 'tmp', projectHash, 'chats'); + const projectHash = getProjectHash(workingDir); + const sessionDir = path.join(this.qwenDir, 'tmp', projectHash, 'chats'); + + // Backward compatibility: On Windows, check if legacy directory exists + // and migrate it to the new normalized path + if (os.platform() === 'win32' && !fs.existsSync(sessionDir)) { + const legacyHash = getLegacyProjectHash(workingDir); + const legacySessionDir = path.join( + this.qwenDir, + 'tmp', + legacyHash, + 'chats', + ); + + if (fs.existsSync(legacySessionDir) && legacyHash !== projectHash) { + try { + // Migrate parent directory (hash directory, not just chats) + const newParentDir = path.join(this.qwenDir, 'tmp', projectHash); + const legacyParentDir = path.join(this.qwenDir, 'tmp', legacyHash); + fs.renameSync(legacyParentDir, newParentDir); + } catch (_error) { + // Silent fallback: if migration fails, continue with the new path + } + } + } + + return sessionDir; } /** @@ -92,7 +108,7 @@ export class QwenSessionManager { // Create session object const session: QwenSession = { sessionId, - projectHash: this.getProjectHash(workingDir), + projectHash: getProjectHash(workingDir), startTime: messages[0]?.timestamp || new Date().toISOString(), lastUpdated: new Date().toISOString(), messages, diff --git a/packages/vscode-ide-companion/src/services/qwenSessionReader.ts b/packages/vscode-ide-companion/src/services/qwenSessionReader.ts index 612cd2425d2..8215c8a2009 100644 --- a/packages/vscode-ide-companion/src/services/qwenSessionReader.ts +++ b/packages/vscode-ide-companion/src/services/qwenSessionReader.ts @@ -8,7 +8,10 @@ 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'; +import { + getProjectHash, + getLegacyProjectHash, +} from '@qwen-code/qwen-code-core/src/utils/paths.js'; export interface QwenMessage { id: string; @@ -58,8 +61,35 @@ export class QwenSessionReader { if (!allProjects && workingDir) { // Current project only - const projectHash = await this.getProjectHash(workingDir); + const projectHash = getProjectHash(workingDir); const chatsDir = path.join(this.qwenDir, 'tmp', projectHash, 'chats'); + + // Backward compatibility: On Windows, try legacy hash if new directory doesn't exist + if (os.platform() === 'win32' && !fs.existsSync(chatsDir)) { + const legacyHash = getLegacyProjectHash(workingDir); + const legacyChatsDir = path.join( + this.qwenDir, + 'tmp', + legacyHash, + 'chats', + ); + + if (fs.existsSync(legacyChatsDir) && legacyHash !== projectHash) { + try { + // Migrate parent directory + const newParentDir = path.join(this.qwenDir, 'tmp', projectHash); + const legacyParentDir = path.join( + this.qwenDir, + 'tmp', + legacyHash, + ); + fs.renameSync(legacyParentDir, newParentDir); + } catch (_error) { + // Silent fallback + } + } + } + const projectSessions = await this.readSessionsFromDir(chatsDir); sessions.push(...projectSessions); } else { @@ -177,19 +207,6 @@ export class QwenSessionReader { return found; } - /** - * Calculate project hash (needs to be consistent with Qwen CLI) - * Qwen CLI uses SHA256 hash of project path. - * On Windows, paths are case-insensitive, so we normalize to lowercase - * to ensure the same physical path always produces the same hash. - */ - private async getProjectHash(workingDir: string): Promise { - // On Windows, normalize path to lowercase for case-insensitive matching - const normalizedPath = - os.platform() === 'win32' ? workingDir.toLowerCase() : workingDir; - return crypto.createHash('sha256').update(normalizedPath).digest('hex'); - } - /** * Get session title (based on first user message) */ @@ -294,7 +311,7 @@ export class QwenSessionReader { } const projectHash = cwd - ? await this.getProjectHash(cwd) + ? getProjectHash(cwd) : path.basename(path.dirname(path.dirname(filePath))); return { diff --git a/scripts/telemetry_utils.js b/scripts/telemetry_utils.js index cb2010d5b69..504ed18cb8c 100644 --- a/scripts/telemetry_utils.js +++ b/scripts/telemetry_utils.js @@ -18,10 +18,21 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const projectRoot = path.resolve(__dirname, '..'); -const projectHash = crypto - .createHash('sha256') - .update(projectRoot) - .digest('hex'); + +/** + * Generates a unique hash for a project based on its root path. + * On Windows, paths are case-insensitive, so we normalize to lowercase + * to ensure the same physical path always produces the same hash. + * This logic must match getProjectHash() in packages/core/src/utils/paths.ts + */ +function getProjectHash(projectRoot) { + // On Windows, normalize path to lowercase for case-insensitive matching + const normalizedPath = + os.platform() === 'win32' ? projectRoot.toLowerCase() : projectRoot; + return crypto.createHash('sha256').update(normalizedPath).digest('hex'); +} + +const projectHash = getProjectHash(projectRoot); // User-level .gemini directory in home const USER_GEMINI_DIR = path.join(os.homedir(), '.qwen'); From 60fec71e469da756edf946d7ded1b3416bf9337d Mon Sep 17 00:00:00 2001 From: LaZzyMan Date: Tue, 10 Feb 2026 15:13:24 +0800 Subject: [PATCH 4/4] fix sanitizeCwd --- packages/core/src/config/storage.ts | 40 ++------------ packages/core/src/utils/paths.test.ts | 53 ------------------- packages/core/src/utils/paths.ts | 11 ---- .../src/services/qwenSessionManager.ts | 29 +--------- .../src/services/qwenSessionReader.ts | 32 +---------- 5 files changed, 6 insertions(+), 159 deletions(-) diff --git a/packages/core/src/config/storage.ts b/packages/core/src/config/storage.ts index 8589f5ae2d5..f9d0107e55a 100644 --- a/packages/core/src/config/storage.ts +++ b/packages/core/src/config/storage.ts @@ -7,7 +7,7 @@ import * as path from 'node:path'; import * as os from 'node:os'; import * as fs from 'node:fs'; -import { getProjectHash, getLegacyProjectHash } from '../utils/paths.js'; +import { getProjectHash } from '../utils/paths.js'; export const QWEN_DIR = '.qwen'; export const GOOGLE_ACCOUNTS_FILENAME = 'google_accounts.json'; @@ -91,23 +91,6 @@ export class Storage { const hash = getProjectHash(this.getProjectRoot()); const tempDir = Storage.getGlobalTempDir(); const targetDir = path.join(tempDir, hash); - - // Backward compatibility: On Windows, check if legacy directory exists - // and migrate it to the new normalized path - if (os.platform() === 'win32' && !fs.existsSync(targetDir)) { - const legacyHash = getLegacyProjectHash(this.getProjectRoot()); - const legacyDir = path.join(tempDir, legacyHash); - - if (fs.existsSync(legacyDir) && legacyHash !== hash) { - try { - // Attempt to rename/migrate the directory - fs.renameSync(legacyDir, targetDir); - } catch (_error) { - // Silent fallback: if migration fails, continue with the new path - } - } - } - return targetDir; } @@ -127,23 +110,6 @@ export class Storage { const hash = getProjectHash(this.getProjectRoot()); const historyDir = path.join(Storage.getGlobalQwenDir(), 'history'); const targetDir = path.join(historyDir, hash); - - // Backward compatibility: On Windows, check if legacy directory exists - // and migrate it to the new normalized path - if (os.platform() === 'win32' && !fs.existsSync(targetDir)) { - const legacyHash = getLegacyProjectHash(this.getProjectRoot()); - const legacyDir = path.join(historyDir, legacyHash); - - if (fs.existsSync(legacyDir) && legacyHash !== hash) { - try { - // Attempt to rename/migrate the directory - fs.renameSync(legacyDir, targetDir); - } catch (_error) { - // Silent fallback: if migration fails, continue with the new path - } - } - } - return targetDir; } @@ -176,6 +142,8 @@ export class Storage { } private sanitizeCwd(cwd: string): string { - return cwd.replace(/[^a-zA-Z0-9]/g, '-'); + // On Windows, normalize to lowercase for case-insensitive matching + const normalizedCwd = os.platform() === 'win32' ? cwd.toLowerCase() : cwd; + return normalizedCwd.replace(/[^a-zA-Z0-9]/g, '-'); } } diff --git a/packages/core/src/utils/paths.test.ts b/packages/core/src/utils/paths.test.ts index 2584e650352..9f8b63ef97b 100644 --- a/packages/core/src/utils/paths.test.ts +++ b/packages/core/src/utils/paths.test.ts @@ -18,7 +18,6 @@ import { shortenPath, tildeifyPath, getProjectHash, - getLegacyProjectHash, } from './paths.js'; import type { Config } from '../config/config.js'; @@ -849,55 +848,3 @@ describe('getProjectHash', () => { platformSpy.mockRestore(); }); }); - -describe('getLegacyProjectHash', () => { - it('should always be case-sensitive regardless of platform', () => { - const platformSpy = vi.spyOn(os, 'platform'); - - // Test on Windows - should still be case-sensitive - platformSpy.mockReturnValue('win32'); - const lowerCaseHash = getLegacyProjectHash('c:\\users\\test\\project'); - const upperCaseHash = getLegacyProjectHash('C:\\USERS\\TEST\\PROJECT'); - expect(lowerCaseHash).not.toBe(upperCaseHash); - - // Test on Linux - should be case-sensitive - platformSpy.mockReturnValue('linux'); - const lowerCaseHashLinux = getLegacyProjectHash('/home/user/project'); - const upperCaseHashLinux = getLegacyProjectHash('/HOME/USER/PROJECT'); - expect(lowerCaseHashLinux).not.toBe(upperCaseHashLinux); - - platformSpy.mockRestore(); - }); - - it('should generate different hash than getProjectHash on Windows with mixed case', () => { - const platformSpy = vi.spyOn(os, 'platform'); - platformSpy.mockReturnValue('win32'); - - const mixedCasePath = 'C:\\Users\\Test\\Project'; - const legacyHash = getLegacyProjectHash(mixedCasePath); - const newHash = getProjectHash(mixedCasePath); - - // They should be different because getProjectHash normalizes to lowercase - expect(legacyHash).not.toBe(newHash); - - // But both lowercase paths should match the new hash - const lowercaseNewHash = getProjectHash(mixedCasePath.toLowerCase()); - expect(newHash).toBe(lowercaseNewHash); - - platformSpy.mockRestore(); - }); - - it('should match getProjectHash on non-Windows platforms', () => { - const platformSpy = vi.spyOn(os, 'platform'); - platformSpy.mockReturnValue('linux'); - - const testPath = '/home/user/project'; - const legacyHash = getLegacyProjectHash(testPath); - const newHash = getProjectHash(testPath); - - // On non-Windows platforms, both should be identical - expect(legacyHash).toBe(newHash); - - platformSpy.mockRestore(); - }); -}); diff --git a/packages/core/src/utils/paths.ts b/packages/core/src/utils/paths.ts index f90e9b4e944..96856a5dcc6 100644 --- a/packages/core/src/utils/paths.ts +++ b/packages/core/src/utils/paths.ts @@ -202,17 +202,6 @@ export function getProjectHash(projectRoot: string): string { return crypto.createHash('sha256').update(normalizedPath).digest('hex'); } -/** - * Generates a hash using the legacy algorithm (without case normalization). - * This is used for backward compatibility to locate session directories - * created before the case-insensitive fix on Windows. - * @param projectRoot The absolute path to the project's root directory. - * @returns A SHA256 hash of the project root path without normalization. - */ -export function getLegacyProjectHash(projectRoot: string): string { - return crypto.createHash('sha256').update(projectRoot).digest('hex'); -} - /** * Checks if a path is a subpath of another path. * @param parentPath The parent path. diff --git a/packages/vscode-ide-companion/src/services/qwenSessionManager.ts b/packages/vscode-ide-companion/src/services/qwenSessionManager.ts index 922ca78e5a5..a5e817cad42 100644 --- a/packages/vscode-ide-companion/src/services/qwenSessionManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenSessionManager.ts @@ -8,10 +8,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import * as crypto from 'crypto'; -import { - getProjectHash, - getLegacyProjectHash, -} from '@qwen-code/qwen-code-core/src/utils/paths.js'; +import { getProjectHash } from '@qwen-code/qwen-code-core/src/utils/paths.js'; import type { QwenSession, QwenMessage } from './qwenSessionReader.js'; /** @@ -37,30 +34,6 @@ export class QwenSessionManager { private getSessionDir(workingDir: string): string { const projectHash = getProjectHash(workingDir); const sessionDir = path.join(this.qwenDir, 'tmp', projectHash, 'chats'); - - // Backward compatibility: On Windows, check if legacy directory exists - // and migrate it to the new normalized path - if (os.platform() === 'win32' && !fs.existsSync(sessionDir)) { - const legacyHash = getLegacyProjectHash(workingDir); - const legacySessionDir = path.join( - this.qwenDir, - 'tmp', - legacyHash, - 'chats', - ); - - if (fs.existsSync(legacySessionDir) && legacyHash !== projectHash) { - try { - // Migrate parent directory (hash directory, not just chats) - const newParentDir = path.join(this.qwenDir, 'tmp', projectHash); - const legacyParentDir = path.join(this.qwenDir, 'tmp', legacyHash); - fs.renameSync(legacyParentDir, newParentDir); - } catch (_error) { - // Silent fallback: if migration fails, continue with the new path - } - } - } - return sessionDir; } diff --git a/packages/vscode-ide-companion/src/services/qwenSessionReader.ts b/packages/vscode-ide-companion/src/services/qwenSessionReader.ts index 8215c8a2009..0a65b0cb69d 100644 --- a/packages/vscode-ide-companion/src/services/qwenSessionReader.ts +++ b/packages/vscode-ide-companion/src/services/qwenSessionReader.ts @@ -8,10 +8,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import * as readline from 'readline'; -import { - getProjectHash, - getLegacyProjectHash, -} from '@qwen-code/qwen-code-core/src/utils/paths.js'; +import { getProjectHash } from '@qwen-code/qwen-code-core/src/utils/paths.js'; export interface QwenMessage { id: string; @@ -63,33 +60,6 @@ export class QwenSessionReader { // Current project only const projectHash = getProjectHash(workingDir); const chatsDir = path.join(this.qwenDir, 'tmp', projectHash, 'chats'); - - // Backward compatibility: On Windows, try legacy hash if new directory doesn't exist - if (os.platform() === 'win32' && !fs.existsSync(chatsDir)) { - const legacyHash = getLegacyProjectHash(workingDir); - const legacyChatsDir = path.join( - this.qwenDir, - 'tmp', - legacyHash, - 'chats', - ); - - if (fs.existsSync(legacyChatsDir) && legacyHash !== projectHash) { - try { - // Migrate parent directory - const newParentDir = path.join(this.qwenDir, 'tmp', projectHash); - const legacyParentDir = path.join( - this.qwenDir, - 'tmp', - legacyHash, - ); - fs.renameSync(legacyParentDir, newParentDir); - } catch (_error) { - // Silent fallback - } - } - } - const projectSessions = await this.readSessionsFromDir(chatsDir); sessions.push(...projectSessions); } else {