Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
adec893
fix(app): reuse open project root for nested folders
tsubasakong Mar 9, 2026
7dde0f1
Merge remote-tracking branch 'upstream/dev' into sync/pr-16686
tsubasakong Mar 10, 2026
8888c21
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 10, 2026
27221f0
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 10, 2026
9b285be
Merge remote-tracking branch 'upstream/dev' into fix/project-picker-n…
tsubasakong Mar 10, 2026
860b7ae
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 10, 2026
044e2d4
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 10, 2026
8e3be1e
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 10, 2026
9e5248f
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 10, 2026
4982bae
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 10, 2026
933ffe7
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 10, 2026
1e5a4e1
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 10, 2026
9340472
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 10, 2026
4022661
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 11, 2026
7d53837
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 11, 2026
a148030
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 11, 2026
71bcabc
Merge remote-tracking branch 'upstream/dev' into fix/project-picker-n…
tsubasakong Mar 11, 2026
ec02625
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 11, 2026
1eb3fc3
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 11, 2026
b17a0a2
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 11, 2026
9f87ba2
Merge remote-tracking branch 'upstream/dev' into fix/project-picker-n…
tsubasakong Mar 11, 2026
c8219c8
Merge remote-tracking branch 'upstream/dev' into fix/project-picker-n…
tsubasakong Mar 11, 2026
67ec954
Merge remote-tracking branch 'upstream/dev' into fix/project-picker-n…
tsubasakong Mar 11, 2026
91dd393
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 11, 2026
58f33ab
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 11, 2026
dd307ac
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 12, 2026
523c13c
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 12, 2026
402980f
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 12, 2026
809d8d9
Merge remote-tracking branch 'upstream/dev' into fix/project-picker-n…
tsubasakong Mar 12, 2026
6fb6a9d
Merge remote-tracking branch 'upstream/dev' into HEAD
tsubasakong Mar 12, 2026
825fd29
Merge remote-tracking branch 'upstream/dev' into fix/project-picker-n…
tsubasakong Mar 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions packages/app/src/context/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<string>()
const chain = [directory]

Expand All @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions packages/app/src/pages/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { Titlebar } from "@/components/titlebar"
import { useServer } from "@/context/server"
import { useLanguage, type Locale } from "@/context/language"
import {
containingWorkspaceRoot,
displayName,
effectiveWorkspaceOrder,
errorMessage,
Expand Down Expand Up @@ -1127,6 +1128,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))
Expand Down
21 changes: 19 additions & 2 deletions packages/app/src/pages/layout/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Session> & Pick<Session, "id" | "directory">) =>
({
Expand Down Expand Up @@ -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"])
Expand Down
18 changes: 18 additions & 0 deletions packages/app/src/pages/layout/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading