From d86a88e76f5d98ade87e3e6fe20260e7d2a8b56a Mon Sep 17 00:00:00 2001 From: usvimal Date: Fri, 2 Jan 2026 13:16:02 +0800 Subject: [PATCH 1/2] fix(app): allow browsing absolute paths on Windows On Windows, the directory selector was limited to browsing within the user's home directory (C:\Users\). This made it impossible to open projects on other drives like D:\ or outside the user profile. Changes: - Add isAbsolutePath() to detect Windows drive letters and Unix paths - Add getSearchRoot() to extract search directory from absolute paths - Modify fetchDirs() to search from absolute paths when detected - Update placeholder text to hint absolute path support Fixes #6490 --- .../components/dialog-select-directory.tsx | 79 ++++++++++++++++--- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/packages/app/src/components/dialog-select-directory.tsx b/packages/app/src/components/dialog-select-directory.tsx index bf4a1f9edd4..ba488198dbe 100644 --- a/packages/app/src/components/dialog-select-directory.tsx +++ b/packages/app/src/components/dialog-select-directory.tsx @@ -13,6 +13,42 @@ interface DialogSelectDirectoryProps { onSelect: (result: string | string[] | null) => void } +/** + * Check if a path is absolute (Windows drive letter or Unix root) + * Handles: C:\, D:\, /home, /c/, etc. + */ +function isAbsolutePath(path: string): boolean { + if (!path) return false + // Windows drive letter (C:\, D:/, etc.) + if (/^[a-zA-Z]:[\/]/.test(path)) return true + // Unix absolute path or Git Bash style (/c/, /d/, /home, etc.) + if (path.startsWith("/")) return true + return false +} + +/** + * Extract the search directory from an absolute path query + * Returns the directory portion to search within + */ +function getSearchRoot(query: string): string | null { + if (!isAbsolutePath(query)) return null + + // For Windows paths like "C:\Users" or "D:\Projects\foo" + // Return the path up to the last separator for searching + const normalized = query.replace(/\/g, "/") + const lastSlash = normalized.lastIndexOf("/") + + if (lastSlash <= 0) { + // Root of drive: "C:\" -> "C:/" + if (/^[a-zA-Z]:/.test(query)) { + return query.slice(0, 2) + "/" + } + return "/" + } + + return normalized.slice(0, lastSlash) || "/" +} + export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { const sync = useGlobalSync() const sdk = useGlobalSDK() @@ -22,8 +58,8 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { const root = createMemo(() => sync.data.path.home || sync.data.path.directory) function join(base: string | undefined, rel: string) { - const b = (base ?? "").replace(/[\\/]+$/, "") - const r = rel.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "") + const b = (base ?? "").replace(/[\/]+$/, "") + const r = rel.replace(/^[\/]+/, "").replace(/[\/]+$/, "") if (!b) return r if (!r) return b return b + "/" + r @@ -46,11 +82,14 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { if (!query) return query if (query.startsWith("~/")) return query.slice(2) + // If it's an absolute path, don't normalize - return as-is for absolute search + if (isAbsolutePath(query)) return query + if (h) { const lc = query.toLowerCase() const hc = h.toLowerCase() if (lc === hc || lc.startsWith(hc + "/") || lc.startsWith(hc + "\\")) { - return query.slice(h.length).replace(/^[\\/]+/, "") + return query.slice(h.length).replace(/^[\/]+/, "") } } @@ -58,15 +97,36 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { } async function fetchDirs(query: string) { - const directory = root() + // Check if query is an absolute path (Windows or Unix) + let directory: string + let searchQuery: string + + if (isAbsolutePath(query)) { + // For absolute paths, search from the path's parent directory + const searchRoot = getSearchRoot(query) + if (searchRoot) { + directory = searchRoot + // Extract just the filename/folder portion for the query + const normalized = query.replace(/\/g, "/") + const lastSlash = normalized.lastIndexOf("/") + searchQuery = lastSlash >= 0 ? normalized.slice(lastSlash + 1) : query + } else { + directory = root() || "" + searchQuery = query + } + } else { + directory = root() || "" + searchQuery = query + } + if (!directory) return [] as string[] const results = await sdk.client.find - .files({ directory, query, type: "directory", limit: 50 }) + .files({ directory, query: searchQuery, type: "directory", limit: 50 }) .then((x) => x.data ?? []) .catch(() => []) - return results.map((x) => x.replace(/[\\/]+$/, "")) + return results.map((x) => x.replace(/[\/]+$/, "")) } const directories = async (filter: string) => { @@ -75,7 +135,8 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { } function resolve(rel: string) { - const absolute = join(root(), rel) + // If the result is already an absolute path, use it directly + const absolute = isAbsolutePath(rel) ? rel : join(root(), rel) props.onSelect(props.multiple ? [absolute] : absolute) dialog.close() } @@ -83,7 +144,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { return ( x} @@ -93,7 +154,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { }} > {(rel) => { - const path = display(rel) + const path = isAbsolutePath(rel) ? rel : display(rel) return (
From 3296ad47f928fac5a39c5c34afd3c6cfc64aa4b3 Mon Sep 17 00:00:00 2001 From: usvimal Date: Fri, 2 Jan 2026 13:28:31 +0800 Subject: [PATCH 2/2] fix: use case-insensitive path comparison on Windows to prevent duplicate projects On Windows, file paths are case-insensitive but the project deduplication check used strict equality (===). This caused the same directory opened with different casing (e.g., C:\Users vs c:\Users) to appear as separate entries in the sidebar. Added isWindows() and pathsEqual() helper functions that perform case-insensitive comparison on Windows while maintaining case-sensitive comparison on other platforms. --- packages/app/src/context/server.tsx | 35 ++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx index f4b58e0e679..b4ec52c436c 100644 --- a/packages/app/src/context/server.tsx +++ b/packages/app/src/context/server.tsx @@ -30,6 +30,26 @@ function projectsKey(url: string) { return url } +/** + * Check if the current platform is Windows. + * Windows paths are case-insensitive, so we need special handling. + */ +function isWindows(): boolean { + return typeof navigator !== "undefined" && navigator.platform?.toLowerCase().includes("win") +} + +/** + * Compare two file paths for equality. + * On Windows, paths are compared case-insensitively. + * On other platforms, paths are compared case-sensitively. + */ +function pathsEqual(path1: string, path2: string): boolean { + if (isWindows()) { + return path1.toLowerCase() === path2.toLowerCase() + } + return path1 === path2 +} + export const { use: useServer, provider: ServerProvider } = createSimpleContext({ name: "Server", init: (props: { defaultUrl: string }) => { @@ -141,7 +161,8 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( const key = origin() if (!key) return const current = store.projects[key] ?? [] - if (current.find((x) => x.worktree === directory)) return + // Use case-insensitive comparison on Windows to prevent duplicate entries + if (current.find((x) => pathsEqual(x.worktree, directory))) return setStore("projects", key, [{ worktree: directory, expanded: true }, ...current]) }, close(directory: string) { @@ -151,28 +172,32 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( setStore( "projects", key, - current.filter((x) => x.worktree !== directory), + // Use case-insensitive comparison on Windows + current.filter((x) => !pathsEqual(x.worktree, directory)), ) }, expand(directory: string) { const key = origin() if (!key) return const current = store.projects[key] ?? [] - const index = current.findIndex((x) => x.worktree === directory) + // Use case-insensitive comparison on Windows + const index = current.findIndex((x) => pathsEqual(x.worktree, directory)) if (index !== -1) setStore("projects", key, index, "expanded", true) }, collapse(directory: string) { const key = origin() if (!key) return const current = store.projects[key] ?? [] - const index = current.findIndex((x) => x.worktree === directory) + // Use case-insensitive comparison on Windows + const index = current.findIndex((x) => pathsEqual(x.worktree, directory)) if (index !== -1) setStore("projects", key, index, "expanded", false) }, move(directory: string, toIndex: number) { const key = origin() if (!key) return const current = store.projects[key] ?? [] - const fromIndex = current.findIndex((x) => x.worktree === directory) + // Use case-insensitive comparison on Windows + const fromIndex = current.findIndex((x) => pathsEqual(x.worktree, directory)) if (fromIndex === -1 || fromIndex === toIndex) return const result = [...current] const [item] = result.splice(fromIndex, 1)