diff --git a/desktop/src/components/layout/Sidebar.test.tsx b/desktop/src/components/layout/Sidebar.test.tsx
index e6e41dbf..62960d1e 100644
--- a/desktop/src/components/layout/Sidebar.test.tsx
+++ b/desktop/src/components/layout/Sidebar.test.tsx
@@ -782,6 +782,23 @@ describe('Sidebar', () => {
expect(screen.getAllByText('worktree')).toHaveLength(1)
})
+ it('keeps a Windows drive root session separate from sessions in child projects', () => {
+ const now = new Date().toISOString()
+ useSessionStore.setState({
+ sessions: [
+ makeSession('drive-root', 'Drive Root Session', 'D:\\', now),
+ makeSession('drive-project', 'Drive Project Session', 'D:\\SomeProject', now),
+ ],
+ })
+
+ render()
+
+ expect(screen.getByText('D:')).toBeInTheDocument()
+ expect(screen.getByText('SomeProject')).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: /Drive Root Session/ })).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: /Drive Project Session/ })).toBeInTheDocument()
+ })
+
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'))
diff --git a/src/server/__tests__/windows-drive-path.test.ts b/src/server/__tests__/windows-drive-path.test.ts
new file mode 100644
index 00000000..b48a9c7c
--- /dev/null
+++ b/src/server/__tests__/windows-drive-path.test.ts
@@ -0,0 +1,30 @@
+import { describe, expect, it } from 'bun:test'
+import {
+ isSameOrInsidePathForPlatform,
+ normalizeDriveRootPathForPlatform,
+} from '../services/windowsDrivePath.js'
+import { SessionService } from '../services/sessionService.js'
+
+describe('Windows drive root path handling', () => {
+ it('normalizes drive-relative root inputs to absolute drive roots on Windows', () => {
+ expect(normalizeDriveRootPathForPlatform('D:', 'win32')).toBe('D:\\')
+ expect(normalizeDriveRootPathForPlatform('d:', 'win32')).toBe('d:\\')
+ expect(normalizeDriveRootPathForPlatform('D:\\', 'win32')).toBe('D:\\')
+ expect(normalizeDriveRootPathForPlatform('D:\\project', 'win32')).toBe('D:\\project')
+ expect(normalizeDriveRootPathForPlatform('D:', 'darwin')).toBe('D:')
+ })
+
+ it('recovers sanitized Windows drive-root transcript directories', () => {
+ const service = new SessionService()
+ expect(service.desanitizePath('D--')).toBe('D:\\')
+ expect(service.desanitizePath('D--project')).toBe('D:\\project')
+ })
+
+ it('treats absolute Windows drive-root children as inside the selected root', () => {
+ expect(isSameOrInsidePathForPlatform('D:\\', 'D:', 'win32')).toBe(true)
+ expect(isSameOrInsidePathForPlatform('D:\\child', 'D:', 'win32')).toBe(true)
+ expect(isSameOrInsidePathForPlatform('D:\\child', 'D:\\', 'win32')).toBe(true)
+ expect(isSameOrInsidePathForPlatform('D:\\project-extra', 'D:\\project', 'win32')).toBe(false)
+ expect(isSameOrInsidePathForPlatform('E:\\child', 'D:\\', 'win32')).toBe(false)
+ })
+})
diff --git a/src/server/api/filesystem.ts b/src/server/api/filesystem.ts
index 61814227..ab8ef011 100644
--- a/src/server/api/filesystem.ts
+++ b/src/server/api/filesystem.ts
@@ -13,6 +13,10 @@ import { findGitRoot, gitExe } from '../../utils/git.js'
import { ripGrep } from '../../utils/ripgrep.js'
import { getInitialSettings } from '../../utils/settings/settings.js'
import { isWithinRegisteredFilesystemRoot } from '../services/filesystemAccessRoots.js'
+import {
+ isSameOrInsidePathForPlatform,
+ normalizeDriveRootPathForPlatform,
+} from '../services/windowsDrivePath.js'
type FilesystemEntry = {
name: string
@@ -41,14 +45,7 @@ const IMAGE_MIME_TYPES: Record = {
}
function isWithinRoot(targetPath: string, rootPath: string): boolean {
- const target = normalizeComparablePath(targetPath)
- const root = normalizeComparablePath(rootPath)
- return target === root || target.startsWith(`${root}${path.sep}`)
-}
-
-function normalizeComparablePath(filePath: string): string {
- const resolved = path.resolve(filePath)
- return process.platform === 'win32' ? resolved.toLowerCase() : resolved
+ return isSameOrInsidePathForPlatform(targetPath, rootPath)
}
function isVcsMetadataDirectoryName(name: string): boolean {
@@ -56,7 +53,7 @@ function isVcsMetadataDirectoryName(name: string): boolean {
}
function isAllowedFilesystemPath(targetPath: string): boolean {
- const resolvedPath = path.resolve(targetPath)
+ const resolvedPath = path.resolve(normalizeDriveRootPathForPlatform(targetPath))
const homeDir = path.resolve(os.homedir())
if (isWithinRoot(resolvedPath, homeDir) || isWithinRoot(resolvedPath, '/tmp')) {
@@ -93,7 +90,7 @@ async function handleServeFile(url: URL): Promise {
return json({ error: 'Missing path parameter' }, 400)
}
- const resolvedPath = path.resolve(filePath)
+ const resolvedPath = path.resolve(normalizeDriveRootPathForPlatform(filePath))
if (!isAllowedFilesystemPath(resolvedPath)) {
return json({ error: 'Access denied: path outside allowed directory' }, 403)
@@ -132,7 +129,7 @@ async function handleServeFile(url: URL): Promise {
async function handleBrowse(url: URL): Promise {
const targetPath = url.searchParams.get('path') || os.homedir() || '/'
- const resolvedPath = path.resolve(targetPath)
+ const resolvedPath = path.resolve(normalizeDriveRootPathForPlatform(targetPath))
if (!isAllowedFilesystemPath(resolvedPath)) {
return json({ error: 'Access denied: path outside allowed directory' }, 403)
diff --git a/src/server/services/filesystemAccessRoots.ts b/src/server/services/filesystemAccessRoots.ts
index a77ace6b..0b3aeb5a 100644
--- a/src/server/services/filesystemAccessRoots.ts
+++ b/src/server/services/filesystemAccessRoots.ts
@@ -1,21 +1,18 @@
import * as path from 'node:path'
+import {
+ isSameOrInsidePathForPlatform,
+ normalizeDriveRootPathForPlatform,
+} from './windowsDrivePath.js'
const registeredRoots = new Set()
-function normalizeComparablePath(filePath: string): string {
- const resolved = path.resolve(filePath)
- return process.platform === 'win32' ? resolved.toLowerCase() : resolved
-}
-
function isWithinRoot(targetPath: string, rootPath: string): boolean {
- const target = normalizeComparablePath(targetPath)
- const root = normalizeComparablePath(rootPath)
- return target === root || target.startsWith(`${root}${path.sep}`)
+ return isSameOrInsidePathForPlatform(targetPath, rootPath)
}
export function registerFilesystemAccessRoot(rootPath: string | null | undefined): void {
if (!rootPath) return
- registeredRoots.add(path.resolve(rootPath))
+ registeredRoots.add(path.resolve(normalizeDriveRootPathForPlatform(rootPath)))
}
export function isWithinRegisteredFilesystemRoot(targetPath: string): boolean {
diff --git a/src/server/services/repositoryLaunchService.ts b/src/server/services/repositoryLaunchService.ts
index 03a546e8..b2487732 100644
--- a/src/server/services/repositoryLaunchService.ts
+++ b/src/server/services/repositoryLaunchService.ts
@@ -5,6 +5,7 @@ import { promisify } from 'node:util'
import { ApiError } from '../middleware/errorHandler.js'
import { findCanonicalGitRoot, findGitRoot } from '../../utils/git.js'
import { registerFilesystemAccessRoot } from './filesystemAccessRoots.js'
+import { normalizeDriveRootPathForPlatform } from './windowsDrivePath.js'
import {
ensureWorktreesDirExcluded,
performPostCreationSetup,
@@ -168,7 +169,7 @@ async function runGit(
}
async function resolveDirectory(workDir: string): Promise {
- const resolved = path.resolve(workDir)
+ const resolved = path.resolve(normalizeDriveRootPathForPlatform(workDir))
let realPath: string
try {
realPath = await fs.realpath(resolved)
@@ -332,7 +333,7 @@ export async function getRepositoryContext(workDir: string): Promise= 0; i--) {
const entry = entries[i]
if (entry.type === 'session-meta' && typeof (entry as Record).workDir === 'string') {
- return (entry as Record).workDir as string
+ return normalizeDriveRootPathForPlatform((entry as Record).workDir as string)
}
}
for (let i = entries.length - 1; i >= 0; i--) {
const cwd = entries[i]?.cwd
if (typeof cwd === 'string' && cwd.trim()) {
- return cwd
+ return normalizeDriveRootPathForPlatform(cwd)
}
}
@@ -915,7 +916,7 @@ export class SessionService {
// Optionally filter to a specific project
if (projectFilter) {
- const sanitized = this.sanitizePath(projectFilter)
+ const sanitized = this.sanitizePath(normalizeDriveRootPathForPlatform(projectFilter))
projectDirs = projectDirs.filter((d) => d === sanitized)
}
@@ -966,6 +967,11 @@ export class SessionService {
return `${windowsDrivePath[1]}:${path.win32.sep}${windowsDrivePath[2].replace(/-/g, path.win32.sep)}`
}
+ const windowsDriveRoot = sanitized.match(/^([a-zA-Z])--$/)
+ if (windowsDriveRoot) {
+ return `${windowsDriveRoot[1]}:${path.win32.sep}`
+ }
+
// On POSIX the original path starts with '/', so the sanitized form starts with '-'.
// UNC-style Windows paths also recover to a leading double separator on Windows.
return sanitized.replace(/-/g, path.sep)
@@ -1644,7 +1650,7 @@ export class SessionService {
async clearSessionTranscript(sessionId: string, fallbackWorkDir?: string): Promise {
let found = await this.findSessionFile(sessionId)
if (!found && fallbackWorkDir) {
- const resolvedPath = path.resolve(fallbackWorkDir)
+ const resolvedPath = path.resolve(normalizeDriveRootPathForPlatform(fallbackWorkDir))
const absWorkDir = await fs.realpath(resolvedPath).catch(() => resolvedPath)
const dirPath = path.join(this.getProjectsDir(), this.sanitizePath(absWorkDir))
await fs.mkdir(dirPath, { recursive: true })
@@ -1710,14 +1716,15 @@ export class SessionService {
}
}
- const targetProjectDir = this.sanitizePath(metadata.workDir)
+ const normalizedWorkDir = normalizeDriveRootPathForPlatform(metadata.workDir)
+ const targetProjectDir = this.sanitizePath(normalizedWorkDir)
const targetFilePath = path.join(this.getProjectsDir(), targetProjectDir, `${sessionId}.jsonl`)
await fs.mkdir(path.dirname(targetFilePath), { recursive: true })
await this.appendJsonlEntry(targetFilePath, {
type: 'session-meta',
isMeta: true,
- workDir: metadata.workDir,
+ workDir: normalizedWorkDir,
repository,
timestamp: new Date().toISOString(),
})
@@ -1746,7 +1753,7 @@ export class SessionService {
throw err
}
- const keepProjectDir = this.sanitizePath(keepWorkDir)
+ const keepProjectDir = this.sanitizePath(normalizeDriveRootPathForPlatform(keepWorkDir))
let removed = 0
for (const projectDir of projectDirs) {
if (!projectDir.isDirectory()) continue
diff --git a/src/server/services/windowsDrivePath.ts b/src/server/services/windowsDrivePath.ts
new file mode 100644
index 00000000..82574ce3
--- /dev/null
+++ b/src/server/services/windowsDrivePath.ts
@@ -0,0 +1,30 @@
+import * as path from 'node:path'
+
+export function normalizeDriveRootPathForPlatform(
+ filePath: string,
+ platform: NodeJS.Platform = process.platform,
+): string {
+ if (platform !== 'win32') return filePath
+
+ const driveRootMatch = filePath.match(/^([a-zA-Z]):$/)
+ if (!driveRootMatch) return filePath
+
+ return `${driveRootMatch[1]}:\\`
+}
+
+export function isSameOrInsidePathForPlatform(
+ targetPath: string,
+ rootPath: string,
+ platform: NodeJS.Platform = process.platform,
+): boolean {
+ const pathApi = platform === 'win32' ? path.win32 : path
+ const normalize = (filePath: string) => {
+ const resolved = pathApi.resolve(normalizeDriveRootPathForPlatform(filePath, platform))
+ return platform === 'win32' ? resolved.toLowerCase() : resolved
+ }
+ const target = normalize(targetPath)
+ const root = normalize(rootPath)
+ const relative = pathApi.relative(root, target)
+
+ return relative === '' || (!!relative && !relative.startsWith('..') && !pathApi.isAbsolute(relative))
+}
diff --git a/src/server/services/workspaceService.ts b/src/server/services/workspaceService.ts
index 4df47b9b..b9ba02cb 100644
--- a/src/server/services/workspaceService.ts
+++ b/src/server/services/workspaceService.ts
@@ -6,6 +6,10 @@ import { diffLines } from 'diff'
import type { MessageEntry } from './sessionService.js'
import type { FileHistorySnapshot } from '../../utils/fileHistory.js'
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
+import {
+ isSameOrInsidePathForPlatform,
+ normalizeDriveRootPathForPlatform,
+} from './windowsDrivePath.js'
const MAX_PREVIEW_BYTES = 1024 * 1024
const MAX_UNTRACKED_STAT_BYTES = 256 * 1024
@@ -1004,7 +1008,7 @@ export class WorkspaceService {
if (!workDir) {
throw new Error(`Session not found: ${sessionId}`)
}
- return path.resolve(workDir)
+ return path.resolve(normalizeDriveRootPathForPlatform(workDir))
}
private async getWorkspaceRoot(
@@ -1149,14 +1153,7 @@ export class WorkspaceService {
}
private isWithinRoot(targetPath: string, rootPath: string): boolean {
- const target = this.normalizeComparableAbsolutePath(targetPath)
- const root = this.normalizeComparableAbsolutePath(rootPath)
- return target === root || target.startsWith(`${root}${path.sep}`)
- }
-
- private normalizeComparableAbsolutePath(filePath: string): string {
- const resolved = path.resolve(filePath)
- return process.platform === 'win32' ? resolved.toLowerCase() : resolved
+ return isSameOrInsidePathForPlatform(targetPath, rootPath)
}
private normalizeRelativePath(filePath: string): string {