diff --git a/packages/app/src/components/worktree-setup-callout-policy.test.ts b/packages/app/src/components/worktree-setup-callout-policy.test.ts index 03257be24..dc5bc19dc 100644 --- a/packages/app/src/components/worktree-setup-callout-policy.test.ts +++ b/packages/app/src/components/worktree-setup-callout-policy.test.ts @@ -22,6 +22,7 @@ describe("selectActiveGitWorkspaceProject", () => { it("selects the active git workspace project from checkout metadata", () => { expect(selectActiveGitWorkspaceProject("server-1", gitWorkspace())).toEqual({ serverId: "server-1", + projectId: "project-1", projectKey: "project-1", repoRoot: "/repo/main-project-1", }); @@ -35,11 +36,27 @@ describe("selectActiveGitWorkspaceProject", () => { ), ).toEqual({ serverId: "server-1", + projectId: "project-1", projectKey: "project-1", repoRoot: "/repo/project-1", }); }); + it("uses the persisted project group key for the settings route", () => { + expect( + selectActiveGitWorkspaceProject( + "server-1", + gitWorkspace({ + projectId: "prj_local", + projectGroupKey: "remote:github.com/acme/project", + }), + ), + ).toMatchObject({ + projectId: "prj_local", + projectKey: "remote:github.com/acme/project", + }); + }); + it("ignores non-git workspaces and blank project coordinates", () => { expect( selectActiveGitWorkspaceProject("server-1", gitWorkspace({ projectKind: "local" })), @@ -85,6 +102,7 @@ describe("buildWorktreeSetupCalloutPolicy", () => { expect( buildWorktreeSetupCalloutPolicy({ serverId: "server-1", + projectId: "project-1", projectKey: "project-1", repoRoot: "/repo/project-1", }), diff --git a/packages/app/src/components/worktree-setup-callout-policy.ts b/packages/app/src/components/worktree-setup-callout-policy.ts index 371af8f22..43942bbed 100644 --- a/packages/app/src/components/worktree-setup-callout-policy.ts +++ b/packages/app/src/components/worktree-setup-callout-policy.ts @@ -1,9 +1,11 @@ import type { PaseoConfigRaw } from "@getpaseo/protocol/messages"; import { i18n } from "@/i18n/i18next"; +import { resolveProjectGroupKey } from "@/projects/project-group-key"; import { buildProjectSettingsRoute } from "@/utils/host-routes"; export interface WorktreeSetupWorkspaceInput { projectId: string; + projectGroupKey?: string | null; projectKind: string; projectRootPath: string; project?: { @@ -15,6 +17,7 @@ export interface WorktreeSetupWorkspaceInput { export interface ActiveGitWorkspaceProject { serverId: string; + projectId: string; projectKey: string; repoRoot: string; } @@ -43,13 +46,18 @@ export function selectActiveGitWorkspaceProject( return null; } - const projectKey = workspace.projectId.trim(); + const projectId = workspace.projectId.trim(); + const projectKey = resolveProjectGroupKey({ + serverId, + projectId, + projectGroupKey: workspace.projectGroupKey, + }); const repoRoot = (workspace.project?.checkout?.mainRepoRoot ?? workspace.projectRootPath).trim(); - if (!projectKey || !repoRoot) { + if (!projectId || !repoRoot) { return null; } - return { serverId, projectKey, repoRoot }; + return { serverId, projectId, projectKey, repoRoot }; } export function shouldShowWorktreeSetupCallout(readResult: ReadProjectConfigResult | undefined) { diff --git a/packages/app/src/components/worktree-setup-callout-source.tsx b/packages/app/src/components/worktree-setup-callout-source.tsx index 2cba8dfe6..6ee2f940f 100644 --- a/packages/app/src/components/worktree-setup-callout-source.tsx +++ b/packages/app/src/components/worktree-setup-callout-source.tsx @@ -3,6 +3,7 @@ import { useRouter } from "expo-router"; import { useEffect, useMemo } from "react"; import { useSidebarCallouts } from "@/contexts/sidebar-callout-context"; import { useStableEvent } from "@/hooks/use-stable-event"; +import { useProjects } from "@/hooks/use-projects"; import { useHostRuntimeClient } from "@/runtime/host-runtime"; import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store"; import { useWorkspaceFields } from "@/stores/session-store-hooks"; @@ -14,11 +15,25 @@ import { export function WorktreeSetupCalloutSource() { const selection = useActiveWorkspaceSelection(); - const activeProject = useWorkspaceFields( + const selectedWorkspaceProject = useWorkspaceFields( selection?.serverId ?? null, selection?.workspaceId ?? null, (workspace) => selectActiveGitWorkspaceProject(selection?.serverId ?? "", workspace), ); + const { projects } = useProjects(); + const activeProject = useMemo(() => { + if (!selectedWorkspaceProject) return null; + const structuralProject = projects.find((project) => + project.hosts.some( + (host) => + host.serverId === selectedWorkspaceProject.serverId && + host.projectId === selectedWorkspaceProject.projectId, + ), + ); + return structuralProject + ? { ...selectedWorkspaceProject, projectKey: structuralProject.projectKey } + : selectedWorkspaceProject; + }, [projects, selectedWorkspaceProject]); const client = useHostRuntimeClient(activeProject?.serverId ?? ""); const callouts = useSidebarCallouts(); const router = useRouter(); diff --git a/packages/server/src/server/project-group-key.test.ts b/packages/server/src/server/project-group-key.test.ts index 611d2c571..6be6120e5 100644 --- a/packages/server/src/server/project-group-key.test.ts +++ b/packages/server/src/server/project-group-key.test.ts @@ -16,6 +16,19 @@ describe("deriveProjectGroupKey", () => { ).toBe("remote:git.example.com:8443/acme/app"); }); + test("accepts a root-level remote repository path", () => { + const rootPath = path.resolve("repo"); + + expect( + deriveProjectGroupKey({ + rootPath, + remoteUrl: "https://git.example.com/repo.git", + worktreeRoot: rootPath, + mainRepoRoot: null, + }), + ).toBe("remote:git.example.com/repo"); + }); + test("includes the selected path within a repository", () => { const worktreeRoot = path.resolve("repo"); const rootPath = path.join(worktreeRoot, "packages", "app"); diff --git a/packages/server/src/server/project-group-key.ts b/packages/server/src/server/project-group-key.ts index 31c5f6ee3..81dbfa7db 100644 --- a/packages/server/src/server/project-group-key.ts +++ b/packages/server/src/server/project-group-key.ts @@ -55,6 +55,6 @@ function deriveRemoteProjectGroupKey(remoteUrl: string | null): string | null { if (!host || !remotePath) return null; let cleanedPath = remotePath.trim().replace(/^\/+/, "").replace(/\/+$/, ""); if (cleanedPath.endsWith(".git")) cleanedPath = cleanedPath.slice(0, -4); - if (!cleanedPath.includes("/")) return null; + if (!cleanedPath) return null; return `remote:${host.toLowerCase()}/${cleanedPath}`; } diff --git a/packages/server/src/server/workspace-registry-bootstrap-legacy.ts b/packages/server/src/server/workspace-registry-bootstrap-legacy.ts index a97924060..c3bf8736f 100644 --- a/packages/server/src/server/workspace-registry-bootstrap-legacy.ts +++ b/packages/server/src/server/workspace-registry-bootstrap-legacy.ts @@ -1,4 +1,4 @@ -import { resolve } from "node:path"; +import { basename, resolve } from "node:path"; import type { ProjectCheckoutLitePayload } from "@getpaseo/protocol/messages"; @@ -46,7 +46,7 @@ export function classifyDirectoryForProjectMembership(input: { workspaceKind: deriveWorkspaceKind(checkout), workspaceDisplayName: deriveWorkspaceDisplayName({ cwd, checkout }), projectKey, - projectName: deriveProjectGroupingName(projectKey), + projectName: deriveProjectGroupingName(projectKey, cwd), projectRootPath: deriveProjectRootPath({ cwd, checkout }), projectKind: deriveProjectKind(checkout), }; @@ -58,7 +58,8 @@ function deriveWorkspaceDirectoryKey(cwd: string, checkout: ProjectCheckoutLiteP return worktreeRoot && resolve(worktreeRoot) === selectedRoot ? worktreeRoot : selectedRoot; } -function deriveProjectGroupingName(projectKey: string): string { +function deriveProjectGroupingName(projectKey: string, selectedRoot: string): string { + if (projectKey.includes("#subdir:")) return basename(selectedRoot); if (projectKey.startsWith("remote:")) { const pathSegments = projectKey.slice("remote:".length).split("/").filter(Boolean).slice(1); if (pathSegments.length >= 2) return pathSegments.slice(-2).join("/"); diff --git a/packages/server/src/server/workspace-registry-bootstrap.test.ts b/packages/server/src/server/workspace-registry-bootstrap.test.ts index e1c78926e..033fdc6dc 100644 --- a/packages/server/src/server/workspace-registry-bootstrap.test.ts +++ b/packages/server/src/server/workspace-registry-bootstrap.test.ts @@ -426,9 +426,19 @@ describe("bootstrapWorkspaceRegistries", () => { logger, }); - expect((await projectRegistry.list()).map((project) => project.projectId).sort()).toEqual([ - "remote:github.com/acme/legacy-project#subdir:packages/app", - "remote:github.com/acme/legacy-project#subdir:packages/server", + expect( + (await projectRegistry.list()) + .map((project) => ({ projectId: project.projectId, displayName: project.displayName })) + .sort((left, right) => left.projectId.localeCompare(right.projectId)), + ).toEqual([ + { + projectId: "remote:github.com/acme/legacy-project#subdir:packages/app", + displayName: "app", + }, + { + projectId: "remote:github.com/acme/legacy-project#subdir:packages/server", + displayName: "server", + }, ]); expect( new Set((await workspaceRegistry.list()).map((workspace) => workspace.projectId)),