From acd6ebecb0b090f0e7a686c4b12dbae39acdca04 Mon Sep 17 00:00:00 2001 From: natewill <50088025+natewill@users.noreply.github.com> Date: Sun, 8 Mar 2026 23:26:23 -0400 Subject: [PATCH 1/3] fix(app): resolve opened directories to canonical project root --- packages/app/src/context/layout.tsx | 43 +++++++++++++++++++++++++++-- packages/app/src/pages/layout.tsx | 29 +++++++++---------- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 5199e5a26be..381167c4fd4 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -90,6 +90,16 @@ export function pruneSessionKeys(input: { .slice(input.max) } +type RootResult = { + root: string + project?: { + id?: string + worktree?: string + vcs?: string + sandboxes?: string[] + } +} + function nextSessionTabsForOpen(current: SessionTabs | undefined, tab: string): SessionTabs { const all = current?.all ?? [] if (tab === "review") return { all: all.filter((x) => x !== "review"), active: tab } @@ -457,6 +467,27 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( return directory } + const resolveRoot = (directory: string): Promise => { + return globalSdk + .createClient({ directory, throwOnError: true }) + .project.current() + .then((x) => { + const project = x.data + return { + root: project?.worktree && project.id !== "global" ? project.worktree : directory, + project: project + ? { + id: project.id, + worktree: project.worktree, + vcs: project.vcs, + sandboxes: project.sandboxes, + } + : undefined, + } + }) + .catch(() => ({ root: directory })) + } + createEffect(() => { const projects = server.projects.list() const seen = new Set(projects.map((project) => project.worktree)) @@ -565,11 +596,17 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, projects: { list, - open(directory: string) { - const root = rootFor(directory) - if (server.projects.list().find((x) => x.worktree === root)) return + resolve(directory: string) { + return resolveRoot(directory) + }, + async open(directory: string, resolved?: RootResult) { + const value = resolved ?? (await resolveRoot(directory)) + const root = value.root + const exists = server.projects.list().some((x) => x.worktree === root) + if (exists) return value globalSync.project.loadSessions(root) server.projects.open(root) + return value }, close(directory: string) { server.projects.close(directory) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 9c359aafbda..0f17c89781a 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1238,20 +1238,22 @@ export default function Layout(props: ParentProps) { navigateWithSidebarReset(`/${base64Encode(session.directory)}/session/${session.id}`) } - function openProject(directory: string, navigate = true) { - layout.projects.open(directory) - if (navigate) navigateToProject(directory) + async function openProject(directory: string, navigate = true) { + const { root, project } = await layout.projects.resolve(directory) + await layout.projects.open(directory, { root, project }) + if (navigate) await navigateToProject(root) + return root } const handleDeepLinks = (urls: string[]) => { if (!server.isLocal()) return for (const directory of collectOpenProjectDeepLinks(urls)) { - openProject(directory) + void openProject(directory) } for (const link of collectNewSessionDeepLinks(urls)) { - openProject(link.directory, false) + void openProject(link.directory, false) const slug = base64Encode(link.directory) if (link.prompt) { setSessionHandoff(slug, { prompt: link.prompt }) @@ -1331,14 +1333,13 @@ export default function Layout(props: ParentProps) { const showEditProjectDialog = (project: LocalProject) => dialog.show(() => ) async function chooseProject() { - function resolve(result: string | string[] | null) { + async function resolve(result: string | string[] | null) { if (Array.isArray(result)) { - for (const directory of result) { - openProject(directory, false) - } - navigateToProject(result[0]) + const roots = await Promise.all(result.map((directory) => openProject(directory, false))) + if (!roots[0]) return + await navigateToProject(roots[0]) } else if (result) { - openProject(result) + await openProject(result) } } @@ -1347,11 +1348,11 @@ export default function Layout(props: ParentProps) { title: language.t("command.project.open"), multiple: true, }) - resolve(result) + void resolve(result) } else { dialog.show( - () => , - () => resolve(null), + () => void resolve(result)} />, + () => void resolve(null), ) } } From a210976af986e80dd3da30e25b72bf24da4c65a6 Mon Sep 17 00:00:00 2001 From: natewill <50088025+natewill@users.noreply.github.com> Date: Mon, 9 Mar 2026 00:55:10 -0400 Subject: [PATCH 2/3] fix(app): normalize project root comparisons for dedupe --- packages/app/src/context/layout.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 381167c4fd4..48fd53c2074 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -100,6 +100,15 @@ type RootResult = { } } +const norm = (dir: string) => { + const drive = dir.match(/^([A-Za-z]:)[\\/]+$/) + if (drive) return `${drive[1].toLowerCase()}/` + if (/^[\\/]+$/.test(dir)) return "/" + const key = dir.replace(/[\\/]+$/, "").replaceAll("\\", "/") + if (/^[A-Za-z]:\//.test(key) || key.startsWith("//")) return key.toLowerCase() + return key +} + function nextSessionTabsForOpen(current: SessionTabs | undefined, tab: string): SessionTabs { const all = current?.all ?? [] if (tab === "review") return { all: all.filter((x) => x !== "review"), active: tab } @@ -490,7 +499,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( createEffect(() => { const projects = server.projects.list() - const seen = new Set(projects.map((project) => project.worktree)) + const seen = new Set(projects.map((project) => norm(project.worktree))) batch(() => { for (const project of projects) { @@ -499,9 +508,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( server.projects.close(project.worktree) - if (!seen.has(root)) { + const key = norm(root) + if (!seen.has(key)) { server.projects.open(root) - seen.add(root) + seen.add(key) } if (project.expanded) server.projects.expand(root) @@ -602,7 +612,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( async open(directory: string, resolved?: RootResult) { const value = resolved ?? (await resolveRoot(directory)) const root = value.root - const exists = server.projects.list().some((x) => x.worktree === root) + const key = norm(root) + const exists = server.projects.list().some((x) => norm(x.worktree) === key) if (exists) return value globalSync.project.loadSessions(root) server.projects.open(root) From 54076ff0da52b1b1dc87b404bbc4f281991cdec6 Mon Sep 17 00:00:00 2001 From: natewill <50088025+natewill@users.noreply.github.com> Date: Mon, 9 Mar 2026 01:04:02 -0400 Subject: [PATCH 3/3] fix(app): skip root resolve for already-open project --- packages/app/src/pages/layout.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 0f17c89781a..ff0950dde05 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1239,6 +1239,12 @@ export default function Layout(props: ParentProps) { } async function openProject(directory: string, navigate = true) { + const match = layout.projects.list().find((item) => workspaceKey(item.worktree) === workspaceKey(directory)) + if (match) { + if (navigate) await navigateToProject(match.worktree) + return match.worktree + } + const { root, project } = await layout.projects.resolve(directory) await layout.projects.open(directory, { root, project }) if (navigate) await navigateToProject(root)