fix(projects): separate registration from workspace setup

Adding or cloning a project now registers it and opens workspace setup
instead of creating a workspace implicitly. Keep Add Project independently
mounted from Search so closing one cannot control the other.
This commit is contained in:
Mohamed Boudra
2026-07-15 23:13:55 +02:00
parent 38cfe109c9
commit 943d03ad99
23 changed files with 340 additions and 295 deletions
@@ -0,0 +1,11 @@
import { AddProjectFlow } from "@/components/add-project-flow";
import { useAddProjectFlowStore } from "@/stores/add-project-flow-store";
export function AddProjectFlowHost() {
const request = useAddProjectFlowStore((state) => state.request);
const close = useAddProjectFlowStore((state) => state.close);
if (!request) return null;
return <AddProjectFlow key={request.id} request={request} onClose={close} />;
}
@@ -1,4 +1,5 @@
import { router } from "expo-router";
import type { WorkspaceProjectDescriptorPayload } from "@getpaseo/protocol/messages";
import {
ArrowLeft,
Folder,
@@ -63,7 +64,7 @@ import { pickDirectory } from "@/desktop/pick-directory";
import { useFetchQuery } from "@/data/query";
import { getOpenProjectFailureReason, registerProjectDescriptor } from "@/hooks/open-project";
import { useIsLocalDaemon, useLocalDaemonServerId } from "@/hooks/use-is-local-daemon";
import { useOpenGithubRepo, useOpenProject } from "@/hooks/use-open-project";
import { useCloneGithubProject, useOpenProject } from "@/hooks/use-open-project";
import {
useHosts,
useHostRuntimeClient,
@@ -75,7 +76,7 @@ import { useRecommendedProjectPaths } from "@/stores/session-store-hooks";
import type { AddProjectFlowRequest } from "@/stores/add-project-flow-store";
import type { Theme } from "@/styles/theme";
import { shortenPath } from "@/utils/shorten-path";
import { buildSettingsAddHostRoute } from "@/utils/host-routes";
import { buildNewWorkspaceRoute, buildSettingsAddHostRoute } from "@/utils/host-routes";
interface AddProjectFlowProps {
request: AddProjectFlowRequest;
@@ -197,7 +198,7 @@ function pageTitle(page: AddProjectPage): string {
case "github-search":
return "Clone from GitHub";
case "github-location":
return `Where should Paseo create ${pathBaseName(page.repository.nameWithOwner)}?`;
return "Choose destination";
case "new-directory-parent":
return "Choose parent directory";
case "new-directory-name":
@@ -296,8 +297,8 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) {
const hostIds = useMemo(() => hosts.map((host) => host.serverId), [hosts]);
const connectionStatuses = useHostRuntimeConnectionStatuses(hostIds);
const projectAddByHost = useHostFeatureMap(hostIds, "projectAdd");
// COMPAT(workspaceGithubClone): added in v0.1.108, remove gate after 2027-01-15.
const githubCloneByHost = useHostFeatureMap(hostIds, "workspaceGithubClone");
// COMPAT(projectGithubClone): added in v0.1.108, remove gate after 2027-01-15.
const githubCloneByHost = useHostFeatureMap(hostIds, "projectGithubClone");
// COMPAT(workspaceGithubRepositorySearch): added in v0.1.108, remove gate after 2027-01-15.
const githubSearchByHost = useHostFeatureMap(hostIds, "workspaceGithubRepositorySearch");
// COMPAT(projectCreateDirectory): added in v0.1.108, remove gate after 2027-01-15.
@@ -343,7 +344,7 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) {
const isLocalDaemon = useIsLocalDaemon(hostId ?? "");
const recommendedPaths = useRecommendedProjectPaths(hostId);
const openProject = useOpenProject(hostId);
const openGithubRepo = useOpenGithubRepo(hostId);
const cloneGithubProject = useCloneGithubProject(hostId);
const addEmptyProject = useSessionStore((store) => store.addEmptyProject);
const setHasHydratedWorkspaces = useSessionStore((store) => store.setHasHydratedWorkspaces);
const inputRef = useRef<TextInput>(null);
@@ -416,9 +417,24 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) {
});
}, [onClose]);
const openNewWorkspaceForProject = useCallback(
(serverId: string, project: WorkspaceProjectDescriptorPayload) => {
onClose();
router.push(
buildNewWorkspaceRoute({
serverId,
projectId: project.projectId,
sourceDirectory: project.projectRootPath,
displayName: project.projectDisplayName,
}),
);
},
[onClose],
);
const openAddedProject = useCallback(
async (path: string, sourceKind: "directory-search" | "method") => {
if (submissionInFlightRef.current) return;
if (!hostId || submissionInFlightRef.current) return;
submissionInFlightRef.current = true;
setState((current) =>
setPageStatus(current, sourceKind, { isSubmitting: true, error: null }),
@@ -426,7 +442,7 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) {
try {
const result = await openProject(path);
if (result.ok) {
onClose();
openNewWorkspaceForProject(hostId, result.project);
return;
}
const reason = getOpenProjectFailureReason(result);
@@ -446,7 +462,7 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) {
submissionInFlightRef.current = false;
}
},
[onClose, openProject],
[hostId, openNewWorkspaceForProject, openProject],
);
const browse = useCallback(async () => {
@@ -501,34 +517,34 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) {
setPageStatus(current, "github-location", { isSubmitting: true, error: null }),
);
try {
const opened = await openGithubRepo(
const result = await cloneGithubProject(
locationPage.repository.cloneUrl,
parentPath,
locationPage.repository.cloneProtocol,
);
if (opened) {
if (result.ok) {
lastCloneParentByHost.set(locationPage.hostId, parentPath);
onClose();
openNewWorkspaceForProject(locationPage.hostId, result.project);
return;
}
setState((current) =>
setPageStatus(current, "github-location", {
isSubmitting: false,
error: "Unable to clone repository",
error: result.error ?? "Unable to clone repository",
}),
);
} catch {
} catch (error) {
setState((current) =>
setPageStatus(current, "github-location", {
isSubmitting: false,
error: "Unable to clone repository",
error: error instanceof Error ? error.message : "Unable to clone repository",
}),
);
} finally {
submissionInFlightRef.current = false;
}
},
[onClose, openGithubRepo],
[cloneGithubProject, openNewWorkspaceForProject],
);
const rows = useMemo<FlowRowOption[]>(() => {
if (page.kind === "host") {
@@ -702,7 +718,7 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) {
addEmptyProject,
setHasHydratedWorkspaces,
});
onClose();
openNewWorkspaceForProject(page.hostId, payload.project);
} catch {
setState((current) =>
setPageStatus(current, "new-directory-name", {
@@ -713,7 +729,7 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) {
} finally {
submissionInFlightRef.current = false;
}
}, [addEmptyProject, client, onClose, page, setHasHydratedWorkspaces]);
}, [addEmptyProject, client, openNewWorkspaceForProject, page, setHasHydratedWorkspaces]);
const submitActive = useCallback(() => {
if (page.kind === "new-directory-name") {
@@ -21,8 +21,6 @@ import {
import { AgentStatusDot } from "@/components/agent-status-dot";
import { Shortcut } from "@/components/ui/shortcut";
import { isNative, isWeb } from "@/constants/platform";
import { AddProjectFlow } from "@/components/add-project-flow";
import { useAddProjectFlowStore } from "@/stores/add-project-flow-store";
import { useIsCompactFormFactor } from "@/constants/layout";
import {
IsolatedBottomSheetModal,
@@ -338,8 +336,6 @@ export function CommandCenter() {
handleSelectItem,
handleKeyEvent,
} = useCommandCenter();
const addProjectRequest = useAddProjectFlowStore((state) => state.request);
const isCompact = useIsCompactFormFactor();
const showBottomSheet = isCompact && isNative;
const rowRefs = useRef<Map<number, View>>(new Map());
@@ -493,16 +489,6 @@ export function CommandCenter() {
const snapPoints = useMemo(() => ["60%", "90%"], []);
if (open && addProjectRequest) {
return (
<AddProjectFlow
key={addProjectRequest.id}
request={addProjectRequest}
onClose={handleClose}
/>
);
}
const resultList =
items.length === 0 ? (
<Text style={emptyTextStyle}>{t("shell.commandCenter.noMatches")}</Text>