From adec893545c4dfef6a15628984f9cd48105aaa15 Mon Sep 17 00:00:00 2001 From: Frank <97429702+tsubasakong@users.noreply.github.com> Date: Sun, 8 Mar 2026 20:45:21 -0700 Subject: [PATCH] fix(app): reuse open project root for nested folders --- packages/app/src/context/layout.tsx | 12 ++++++++--- packages/app/src/pages/layout.tsx | 7 +++++++ packages/app/src/pages/layout/helpers.test.ts | 21 +++++++++++++++++-- packages/app/src/pages/layout/helpers.ts | 18 ++++++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 5199e5a26be4..39530f5e5ceb 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -11,6 +11,7 @@ import { decode64 } from "@/utils/base64" import { same } from "@/utils/same" import { createScrollPersistence, type SessionScroll } from "./layout-scroll" import { createPathHelpers } from "./file/path" +import { containingWorkspaceRoot } from "@/pages/layout/helpers" const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const const DEFAULT_PANEL_WIDTH = 344 @@ -437,8 +438,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const rootFor = (directory: string) => { const map = roots() - if (map.size === 0) return directory - const visited = new Set() const chain = [directory] @@ -447,7 +446,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( if (!current) return directory const next = map.get(current) - if (!next) return current + if (!next) { + return ( + containingWorkspaceRoot( + current, + server.projects.list().map((project) => project.worktree), + ) ?? current + ) + } if (visited.has(next)) return directory visited.add(next) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index b7ac28ae1a7e..00379b3e32bc 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -58,6 +58,7 @@ import { Titlebar } from "@/components/titlebar" import { useServer } from "@/context/server" import { useLanguage, type Locale } from "@/context/language" import { + containingWorkspaceRoot, displayName, effectiveWorkspaceOrder, errorMessage, @@ -1115,6 +1116,12 @@ export default function Layout(props: ParentProps) { } function projectRoot(directory: string) { + const openRoot = containingWorkspaceRoot( + directory, + layout.projects.list().map((item) => item.worktree), + ) + if (openRoot) return openRoot + const project = layout.projects .list() .find((item) => item.worktree === directory || item.sandboxes?.includes(directory)) diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts index d1569dbd9a69..4ab1997351e0 100644 --- a/packages/app/src/pages/layout/helpers.test.ts +++ b/packages/app/src/pages/layout/helpers.test.ts @@ -6,9 +6,17 @@ import { parseDeepLink, parseNewSessionDeepLink, } from "./deep-links" -import { displayName, errorMessage, getDraggableId, syncWorkspaceOrder, workspaceKey } from "./helpers" +import { + containingWorkspaceRoot, + displayName, + errorMessage, + getDraggableId, + hasProjectPermissions, + latestRootSession, + syncWorkspaceOrder, + workspaceKey, +} from "./helpers" import { type Session } from "@opencode-ai/sdk/v2/client" -import { hasProjectPermissions, latestRootSession } from "./helpers" const session = (input: Partial & Pick) => ({ @@ -109,6 +117,15 @@ describe("layout workspace helpers", () => { expect(workspaceKey("C:///")).toBe("C:/") }) + test("finds the deepest containing workspace root", () => { + expect(containingWorkspaceRoot("/repo/packages/app", ["/repo", "/repo/packages"])) + .toBe("/repo/packages") + }) + + test("does not match sibling workspace prefixes", () => { + expect(containingWorkspaceRoot("/repo-two/nested", ["/repo"])).toBeUndefined() + }) + test("keeps local first while preserving known order", () => { const result = syncWorkspaceOrder("/root", ["/root", "/b", "/c"], ["/root", "/c", "/a", "/b"]) expect(result).toEqual(["/root", "/c", "/b"]) diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts index 42315e5893ca..4ed646bb0f57 100644 --- a/packages/app/src/pages/layout/helpers.ts +++ b/packages/app/src/pages/layout/helpers.ts @@ -8,6 +8,24 @@ export const workspaceKey = (directory: string) => { return directory.replace(/[\\/]+$/, "") } +function comparableWorkspaceKey(directory: string) { + return workspaceKey(directory).replaceAll("\\", "/") +} + +function containsWorkspace(parent: string, child: string) { + const parentKey = comparableWorkspaceKey(parent) + const childKey = comparableWorkspaceKey(child) + if (parentKey === childKey) return true + if (parentKey === "/") return childKey.startsWith("/") + return childKey.startsWith(parentKey.endsWith("/") ? parentKey : `${parentKey}/`) +} + +export function containingWorkspaceRoot(directory: string, roots: string[]) { + return [...roots] + .sort((a, b) => comparableWorkspaceKey(b).length - comparableWorkspaceKey(a).length) + .find((root) => containsWorkspace(root, directory)) +} + export function sortSessions(now: number) { const oneMinuteAgo = now - 60 * 1000 return (a: Session, b: Session) => {