From 3c3eb39fc0e12f5839075156e0aed215a3e9d5d3 Mon Sep 17 00:00:00 2001 From: Sami Jawhar Date: Wed, 4 Mar 2026 23:54:41 +0000 Subject: [PATCH] feat(app): improve workspace/worktree directory resolution for JJ co-located repos --- .../src/components/session/session-header.tsx | 5 +- .../components/session/session-new-view.tsx | 6 +- packages/app/src/pages/layout.tsx | 56 ++++++++++++++++++- packages/app/src/pages/layout/helpers.ts | 2 +- .../src/pages/layout/sidebar-workspace.tsx | 7 ++- 5 files changed, 66 insertions(+), 10 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 4947ad06a9b..2a1871003f8 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -145,9 +145,12 @@ export function SessionHeader() { return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory)) }) const name = createMemo(() => { + const dir = projectDirectory() const current = project() + // When in a workspace (sandbox), show the workspace name, not the project root + if (current && dir && dir !== current.worktree) return getFilename(dir) if (current) return current.name || getFilename(current.worktree) - return getFilename(projectDirectory()) + return getFilename(dir) }) const hotkey = createMemo(() => command.keybind("file.open")) const os = createMemo(() => detectOS(platform)) diff --git a/packages/app/src/components/session/session-new-view.tsx b/packages/app/src/components/session/session-new-view.tsx index e4ef3639362..99b0dca1ed3 100644 --- a/packages/app/src/components/session/session-new-view.tsx +++ b/packages/app/src/components/session/session-new-view.tsx @@ -27,7 +27,7 @@ export function NewSessionView(props: NewSessionViewProps) { if (options().includes(selection)) return selection return MAIN_WORKTREE }) - const projectRoot = createMemo(() => sync.project?.worktree ?? sdk.directory) + const displayDir = createMemo(() => sdk.directory) const isWorktree = createMemo(() => { const project = sync.project if (!project) return false @@ -59,8 +59,8 @@ export function NewSessionView(props: NewSessionViewProps) {
- {getDirectory(projectRoot())} - {getFilename(projectRoot())} + {getDirectory(displayDir())} + {getFilename(displayDir())}
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index ab2687dcab9..1f834b0cdb9 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -72,6 +72,7 @@ import { effectiveWorkspaceOrder, errorMessage, latestRootSession, + sortSessions, sortedRootSessions, workspaceKey, } from "./layout/helpers" @@ -572,6 +573,17 @@ export default function Layout(props: ParentProps) { return projects.find((p) => p.worktree === root) }) + // Auto-enable workspaces when the current directory is a sandbox of a project + createEffect(() => { + const dir = currentDir() + const project = currentProject() + if (!dir || !project) return + if (dir === project.worktree) return + if (project.vcs !== "git") return + if (layout.sidebar.workspaces(project.worktree)()) return + layout.sidebar.setWorkspaces(project.worktree, true) + }) + createEffect( on( () => ({ ready: pageReady(), layoutReady: layoutReady(), dir: params.dir, list: layout.projects.list() }), @@ -620,8 +632,11 @@ export default function Layout(props: ParentProps) { setStore("workspaceBranchName", projectId, branch, next) } - const workspaceLabel = (directory: string, branch?: string, projectId?: string) => - workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory) + const workspaceLabel = (directory: string, branch?: string, projectId?: string) => { + // For JJ co-located workspaces, branch is "HEAD" (detached) — fall through to directory name + const effectiveBranch = branch && branch !== "HEAD" ? branch : undefined + return workspaceName(directory, projectId, effectiveBranch) ?? effectiveBranch ?? getFilename(directory) + } const workspaceSetting = createMemo(() => { const project = currentProject() @@ -1234,6 +1249,12 @@ export default function Layout(props: ParentProps) { const root = projectRoot(directory) server.projects.touch(root) const project = layout.projects.list().find((item) => item.worktree === root) + // Auto-enable workspaces when navigating to a sandbox directory + if (project && directory !== root && project.vcs === "git") { + if (!layout.sidebar.workspaces(root)()) { + layout.sidebar.setWorkspaces(root, true) + } + } let dirs = project ? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root]) : [root] @@ -1269,6 +1290,37 @@ export default function Layout(props: ParentProps) { return true } + // When user explicitly opened a specific workspace (not the project root), + // navigate directly to that workspace instead of searching across all workspaces. + if (workspaceKey(directory) !== workspaceKey(root)) { + await refreshDirs(directory) + if (canOpen(directory)) { + const [dirStore] = globalSync.child(directory, { bootstrap: false }) + const latest = sortedRootSessions(dirStore, Date.now())[0] + if (latest) { + setStore("lastProjectSession", root, { directory, id: latest.id, at: Date.now() }) + navigateWithSidebarReset(`/${base64Encode(directory)}/session/${latest.id}`) + return + } + // Try fetching sessions from server for this specific workspace + const fetched = await globalSDK.client.session + .list({ directory }) + .then((x) => x.data ?? []) + .catch(() => [] as Session[]) + const visible = fetched + .filter((s) => !s.parentID && !s.time?.archived) + .sort(sortSessions(Date.now())) + if (visible[0]) { + setStore("lastProjectSession", root, { directory, id: visible[0].id, at: Date.now() }) + navigateWithSidebarReset(`/${base64Encode(directory)}/session/${visible[0].id}`) + return + } + } + // No sessions in this workspace — navigate to it anyway (empty state) + navigateWithSidebarReset(`/${base64Encode(directory)}/session`) + return + } + const projectSession = store.lastProjectSession[root] if (projectSession?.id) { await refreshDirs(projectSession.directory) diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts index be4ce9f5742..76f50372488 100644 --- a/packages/app/src/pages/layout/helpers.ts +++ b/packages/app/src/pages/layout/helpers.ts @@ -8,7 +8,7 @@ export const workspaceKey = (directory: string) => { return directory.replace(/[\\/]+$/, "") } -function sortSessions(now: number) { +export function sortSessions(now: number) { const oneMinuteAgo = now - 60 * 1000 return (a: Session, b: Session) => { const aUpdated = a.time.updated ?? a.time.created diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index 86ede774e63..1147de12b9e 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -110,7 +110,7 @@ const WorkspaceHeader = (props: { when={!props.local()} fallback={ - {props.branch() ?? getFilename(props.directory)} + {props.branch() && props.branch() !== "HEAD" ? props.branch() : getFilename(props.directory)} } > @@ -326,8 +326,9 @@ export const SortableWorkspace = (props: { const active = createMemo(() => props.ctx.currentDir() === props.directory) const workspaceValue = createMemo(() => { const branch = workspaceStore.vcs?.branch - const name = branch ?? getFilename(props.directory) - return props.ctx.workspaceName(props.directory, props.project.id, branch) ?? name + const effectiveBranch = branch && branch !== "HEAD" ? branch : undefined + const name = effectiveBranch ?? getFilename(props.directory) + return props.ctx.workspaceName(props.directory, props.project.id, effectiveBranch) ?? name }) const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local())) const boot = createMemo(() => open() || active())