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
+30 -50
View File
@@ -1,27 +1,20 @@
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import type {
ProjectGithubCloneProtocol,
ProjectAddResponse,
WorkspaceGithubCloneProtocol,
WorkspaceProjectDescriptorPayload,
} from "@getpaseo/protocol/messages";
import {
normalizeEmptyProjectDescriptor as normalizeProjectWithoutWorkspacesDescriptor,
normalizeWorkspaceDescriptor,
type EmptyProjectDescriptor as ProjectWithoutWorkspacesDescriptor,
type WorkspaceDescriptor,
} from "@/stores/session-store";
import { generateDraftId } from "@/stores/draft-keys";
import type { NavigateToWorkspaceInput } from "@/stores/navigation-active-workspace-store";
import { buildWorkspaceTabPersistenceKey } from "@/stores/workspace-tabs-store";
type OpenProjectPayload = ProjectAddResponse["payload"];
type OpenProjectErrorCode = NonNullable<OpenProjectPayload["errorCode"]>;
type WorkspaceOpenPayload =
| Awaited<ReturnType<DaemonClient["openProject"]>>
| Awaited<ReturnType<DaemonClient["cloneGithubWorkspace"]>>;
export interface OpenProjectSuccess {
ok: true;
project: WorkspaceProjectDescriptorPayload;
}
export interface OpenProjectFailure {
@@ -32,7 +25,7 @@ export interface OpenProjectFailure {
export type OpenProjectResult = OpenProjectSuccess | OpenProjectFailure;
export type OpenProjectFailureReason = "directory_not_found" | "open_failed";
export type { WorkspaceGithubCloneProtocol };
export type { ProjectGithubCloneProtocol };
export function getOpenProjectFailureReason(
result: OpenProjectResult,
@@ -58,12 +51,11 @@ export interface OpenProjectDirectlyInput {
setHasHydratedWorkspaces: (serverId: string, hydrated: boolean) => void;
}
interface WorkspaceOpenCallbacks {
interface ProjectRegistrationCallbacks {
serverId: string;
isConnected: boolean;
mergeWorkspaces: (serverId: string, workspaces: Iterable<WorkspaceDescriptor>) => void;
addEmptyProject: (serverId: string, project: ProjectWithoutWorkspacesDescriptor) => void;
setHasHydratedWorkspaces: (serverId: string, hydrated: boolean) => void;
navigateToWorkspace: (input: NavigateToWorkspaceInput) => string;
}
export interface RegisterProjectDescriptorInput {
@@ -81,11 +73,11 @@ export function registerProjectDescriptor(input: RegisterProjectDescriptorInput)
return true;
}
export interface OpenGithubRepoDirectlyInput extends WorkspaceOpenCallbacks {
export interface CloneGithubProjectDirectlyInput extends ProjectRegistrationCallbacks {
repo: string;
targetDirectory: string;
cloneProtocol?: WorkspaceGithubCloneProtocol;
client: Pick<DaemonClient, "cloneGithubWorkspace"> | null;
cloneProtocol?: ProjectGithubCloneProtocol;
client: Pick<DaemonClient, "cloneGithubProject"> | null;
}
export async function openProjectDirectly(
@@ -114,44 +106,20 @@ export async function openProjectDirectly(
};
}
registerProjectDescriptor({
const registered = registerProjectDescriptor({
serverId: normalizedServerId,
project: payload.project,
addEmptyProject: input.addEmptyProject,
setHasHydratedWorkspaces: input.setHasHydratedWorkspaces,
});
return { ok: true };
return registered
? { ok: true, project: payload.project }
: { ok: false, errorCode: null, error: "Unable to register project" };
}
function finishWorkspaceOpen(
input: WorkspaceOpenCallbacks,
payload: WorkspaceOpenPayload,
): boolean {
const normalizedServerId = input.serverId.trim();
if (!normalizedServerId || payload.error || !payload.workspace) {
return false;
}
const workspace = normalizeWorkspaceDescriptor(payload.workspace);
const workspaceKey = buildWorkspaceTabPersistenceKey({
serverId: normalizedServerId,
workspaceId: workspace.id,
});
if (!workspaceKey) {
return false;
}
input.mergeWorkspaces(normalizedServerId, [workspace]);
input.setHasHydratedWorkspaces(normalizedServerId, true);
input.navigateToWorkspace({
serverId: normalizedServerId,
workspaceId: workspace.id,
target: { kind: "draft", draftId: generateDraftId() },
});
return true;
}
export async function openGithubRepoDirectly(input: OpenGithubRepoDirectlyInput): Promise<boolean> {
export async function cloneGithubProjectDirectly(
input: CloneGithubProjectDirectlyInput,
): Promise<OpenProjectResult> {
const normalizedServerId = input.serverId.trim();
const trimmedRepo = input.repo.trim();
const trimmedTargetDirectory = input.targetDirectory.trim();
@@ -162,13 +130,25 @@ export async function openGithubRepoDirectly(input: OpenGithubRepoDirectlyInput)
!input.client ||
!input.isConnected
) {
return false;
return { ok: false, errorCode: null, error: null };
}
const payload = await input.client.cloneGithubWorkspace({
const payload = await input.client.cloneGithubProject({
repo: trimmedRepo,
targetDirectory: trimmedTargetDirectory,
...(input.cloneProtocol ? { cloneProtocol: input.cloneProtocol } : {}),
});
return finishWorkspaceOpen(input, payload);
if (payload.error || !payload.project) {
return { ok: false, errorCode: null, error: payload.error };
}
const registered = registerProjectDescriptor({
serverId: normalizedServerId,
project: payload.project,
addEmptyProject: input.addEmptyProject,
setHasHydratedWorkspaces: input.setHasHydratedWorkspaces,
});
return registered
? { ok: true, project: payload.project }
: { ok: false, errorCode: null, error: "Unable to register project" };
}