Merge remote-tracking branch 'origin/main'

This commit is contained in:
程序员阿江(Relakkes) 2026-05-26 00:11:06 +08:00
commit 990733cf2c
6 changed files with 86 additions and 8 deletions

View File

@ -799,6 +799,33 @@ describe('Sidebar', () => {
expect(screen.getByRole('button', { name: /Drive Project Session/ })).toBeInTheDocument()
})
it('does not restore a hidden Windows drive root when creating a child project session', async () => {
window.localStorage.setItem(PROJECT_HIDDEN_STORAGE_KEY, JSON.stringify(['D:\\']))
createSession.mockResolvedValue('child-new')
const now = new Date().toISOString()
useSessionStore.setState({
sessions: [
makeSession('child-1', 'Child Session', 'D:\\workspace\\code\\cc-haha', now),
],
})
useTabStore.setState({
tabs: [{ sessionId: 'child-1', title: 'Child Session', type: 'session', status: 'idle' }],
activeTabId: 'child-1',
})
render(<Sidebar />)
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'New Session' }))
})
await waitFor(() => {
expect(createSession).toHaveBeenCalledWith('D:\\workspace\\code\\cc-haha')
})
expect(JSON.parse(window.localStorage.getItem(PROJECT_HIDDEN_STORAGE_KEY) ?? '[]')).toEqual(['D:\\'])
expect(desktopUiPreferencesApiMock.updateSidebarPreferences).not.toHaveBeenCalled()
})
it('right-aligns running status, worktree marker, and update time on session rows', () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-05-19T12:00:00.000Z'))

View File

@ -1657,17 +1657,21 @@ function normalizeProjectKeyList(values: unknown): string[] {
}
function normalizeProjectPathForComparison(value: string): string {
const normalized = value.replace(/\\/g, '/').replace(/\/+$/g, '')
return normalized || value
const normalized = value.replace(/\\/g, '/').replace(/\/+$/g, '') || value
return isWindows ? normalized.toLowerCase() : normalized
}
function isDriveRootComparisonPath(value: string): boolean {
return /^[a-z]:$/i.test(value)
}
function projectPathMatches(projectKey: string, workDir: string): boolean {
const normalizedProjectKey = normalizeProjectPathForComparison(projectKey)
const normalizedWorkDir = normalizeProjectPathForComparison(workDir)
return normalizedProjectKey === normalizedWorkDir
|| normalizedWorkDir.startsWith(`${normalizedProjectKey}/`)
|| normalizedProjectKey.startsWith(`${normalizedWorkDir}/`)
if (normalizedProjectKey === normalizedWorkDir) return true
if (isDriveRootComparisonPath(normalizedProjectKey)) return false
return normalizedWorkDir.startsWith(`${normalizedProjectKey}/`)
}
function hasSidebarProjectPreferences(preferences: SidebarProjectPreferences): boolean {

View File

@ -1,9 +1,14 @@
import { describe, expect, it } from 'bun:test'
import * as fs from 'node:fs/promises'
import * as os from 'node:os'
import * as path from 'node:path'
import {
isSameOrInsidePathForPlatform,
normalizeDriveRootPathForPlatform,
} from '../services/windowsDrivePath.js'
import { getRepositoryContext } from '../services/repositoryLaunchService.js'
import { SessionService } from '../services/sessionService.js'
import { sanitizePath } from '../../utils/sessionStoragePortable.js'
describe('Windows drive root path handling', () => {
it('normalizes drive-relative root inputs to absolute drive roots on Windows', () => {
@ -27,4 +32,46 @@ describe('Windows drive root path handling', () => {
expect(isSameOrInsidePathForPlatform('D:\\project-extra', 'D:\\project', 'win32')).toBe(false)
expect(isSameOrInsidePathForPlatform('E:\\child', 'D:\\', 'win32')).toBe(false)
})
it('keeps realpathed drive roots from resolving to the current drive directory', async () => {
if (process.platform !== 'win32') return
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR
const configDir = await fs.mkdtemp(path.join(os.tmpdir(), 'drive-root-session-'))
const driveRoot = path.parse(process.cwd()).root
const sessionId = crypto.randomUUID()
try {
process.env.CLAUDE_CONFIG_DIR = configDir
const projectDir = path.join(configDir, 'projects', sanitizePath(driveRoot))
await fs.mkdir(projectDir, { recursive: true })
await fs.writeFile(
path.join(projectDir, `${sessionId}.jsonl`),
JSON.stringify({
type: 'session-meta',
isMeta: true,
workDir: driveRoot,
timestamp: new Date().toISOString(),
}) + '\n',
'utf-8',
)
const service = new SessionService()
const { sessions } = await service.listSessions({ limit: 5 })
const session = sessions.find((item) => item.id === sessionId)
expect(session?.workDir).toBe(driveRoot)
expect(session?.projectRoot).toBe(driveRoot)
const context = await getRepositoryContext(driveRoot)
expect(context.workDir).toBe(driveRoot)
expect(context.repoRoot).not.toBe(process.cwd())
} finally {
if (originalConfigDir === undefined) {
delete process.env.CLAUDE_CONFIG_DIR
} else {
process.env.CLAUDE_CONFIG_DIR = originalConfigDir
}
await fs.rm(configDir, { recursive: true, force: true })
}
})
})

View File

@ -172,7 +172,7 @@ async function resolveDirectory(workDir: string): Promise<string> {
const resolved = path.resolve(normalizeDriveRootPathForPlatform(workDir))
let realPath: string
try {
realPath = await fs.realpath(resolved)
realPath = normalizeDriveRootPathForPlatform(await fs.realpath(resolved))
} catch {
throw repositoryBadRequest(
REPOSITORY_ERROR.workdirMissing,

View File

@ -366,7 +366,7 @@ export class SessionService {
private async canonicalizeProjectPath(projectPath: string): Promise<string> {
try {
return (await fs.realpath(projectPath)).normalize('NFC')
return normalizeDriveRootPathForPlatform(await fs.realpath(projectPath)).normalize('NFC')
} catch {
return projectPath.normalize('NFC')
}

View File

@ -1030,7 +1030,7 @@ export class WorkspaceService {
return {
kind: 'ok',
workspaceRoot: workDir,
canonicalWorkspaceRoot: await fs.realpath(workDir),
canonicalWorkspaceRoot: normalizeDriveRootPathForPlatform(await fs.realpath(workDir)),
}
} catch (error) {
return {