From 8ce4ddfb519ff6d8ebd74fbf75f643754f337a67 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:36:19 +1000 Subject: [PATCH] fix(desktop): canonicalize picker containment --- .../directory-picker-domain.test.ts | 3 ++ .../src/components/directory-picker-domain.ts | 36 ++++++++++++++----- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/packages/app/src/components/directory-picker-domain.test.ts b/packages/app/src/components/directory-picker-domain.test.ts index cbdfdeb9e82..ed9f5917db9 100644 --- a/packages/app/src/components/directory-picker-domain.test.ts +++ b/packages/app/src/components/directory-picker-domain.test.ts @@ -76,6 +76,9 @@ test("accepts mutations only from the active navigation", () => { test("preserves POSIX case while matching Windows drives case-insensitively", () => { expect(treePathWithin("/repo", "/Repo")).toBeFalse() expect(treePathWithin("C:/Repo", "c:/repo/src")).toBeTrue() + expect(treePathWithin("/repo", "/repo/../tmp")).toBeFalse() + expect(treePathWithin("/", "/src")).toBeTrue() + expect(pickerMode("file", "C:/Repo").selection("c:/repo/src", "file.ts")).toBe("src/file.ts") }) test("displays paths using the selected server path format", () => { diff --git a/packages/app/src/components/directory-picker-domain.ts b/packages/app/src/components/directory-picker-domain.ts index 0a34d27e5f1..b1d463eb373 100644 --- a/packages/app/src/components/directory-picker-domain.ts +++ b/packages/app/src/components/directory-picker-domain.ts @@ -76,13 +76,36 @@ export function pickerAbsoluteInput(input: string, home: string) { } export function treePathWithin(base: string | undefined, path: string) { - if (!base) return false - const rootPath = absoluteTreePath(base, "") - const targetPath = absoluteTreePath(path, "") + return pickerRelativePath(base, path) !== undefined +} + +export function canonicalPickerPath(path: string) { + const value = normalizePickerDrive(path) + const root = pickerRoot(value) + const parts = value.slice(root.length).split("/") + const resolved = parts.reduce((output, part) => { + if (!part || part === ".") return output + if (part === "..") { + output.pop() + return output + } + output.push(part) + return output + }, []) + return joinPickerPath(root, resolved.join("/")) +} + +export function pickerRelativePath(base: string | undefined, path: string) { + if (!base) return + const rootPath = canonicalPickerPath(base) + const targetPath = canonicalPickerPath(path) const insensitive = /^[A-Za-z]:\//.test(rootPath) const root = insensitive ? rootPath.toLowerCase() : rootPath const target = insensitive ? targetPath.toLowerCase() : targetPath - return target === root || target.startsWith(root + "/") + if (target === root) return "" + const prefix = root === "/" ? root : root + "/" + if (!target.startsWith(prefix)) return + return targetPath.slice(prefix.length) } export function currentPickerSuggestions( @@ -136,10 +159,7 @@ export function selectedTreePath(root: string, path: string, mode: "directory" | if (directory) return if (!base) return path const absolute = absoluteTreePath(root, path) - const prefix = absoluteTreePath(base, "") - if (absolute === prefix) return "" - if (absolute.startsWith(prefix + "/")) return absolute.slice(prefix.length + 1) - return absolute + return pickerRelativePath(base, absolute) } return directory ? nativePickerPath(absoluteTreePath(root, path)) : undefined }