Add projects without creating workspaces
Keep the legacy open-project request creating a workspace for old clients; new clients now add projects and create workspaces through separate flows.
This commit is contained in:
@@ -35,7 +35,7 @@ function buildSeededStoragePayload() {
|
||||
* idle agent from the same client it uses for everything else.
|
||||
*/
|
||||
export interface IdleAgentSeedClient {
|
||||
openProject(cwd: string): Promise<{
|
||||
createWorkspace(input: { source: { kind: "directory"; path: string } }): Promise<{
|
||||
workspace: { id: string } | null;
|
||||
error: string | null;
|
||||
}>;
|
||||
@@ -58,16 +58,18 @@ export async function createIdleAgent(
|
||||
client: IdleAgentSeedClient,
|
||||
input: { cwd: string; title: string },
|
||||
): Promise<ArchiveTabAgent> {
|
||||
const opened = await client.openProject(input.cwd);
|
||||
if (!opened.workspace) {
|
||||
throw new Error(opened.error ?? `Failed to open project ${input.cwd}`);
|
||||
const createdWorkspace = await client.createWorkspace({
|
||||
source: { kind: "directory", path: input.cwd },
|
||||
});
|
||||
if (!createdWorkspace.workspace) {
|
||||
throw new Error(createdWorkspace.error ?? `Failed to create workspace ${input.cwd}`);
|
||||
}
|
||||
const created = await client.createAgent({
|
||||
provider: "opencode",
|
||||
model: "opencode/gpt-5-nano",
|
||||
modeId: "bypassPermissions",
|
||||
cwd: input.cwd,
|
||||
workspaceId: opened.workspace.id,
|
||||
workspaceId: createdWorkspace.workspace.id,
|
||||
title: input.title,
|
||||
});
|
||||
const snapshot = await client.waitForAgentUpsert(
|
||||
@@ -82,7 +84,7 @@ export async function createIdleAgent(
|
||||
id: created.id,
|
||||
title: input.title,
|
||||
cwd: input.cwd,
|
||||
workspaceId: opened.workspace.id,
|
||||
workspaceId: createdWorkspace.workspace.id,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -160,15 +160,19 @@ export async function startRunningMockAgent(
|
||||
|
||||
const repo = await createTempGitRepo(opts.prefix);
|
||||
const client = await connectSeedClient();
|
||||
const opened = await client.openProject(repo.path);
|
||||
if (!opened.workspace) throw new Error(opened.error ?? "Failed to open project");
|
||||
const createdWorkspace = await client.createWorkspace({
|
||||
source: { kind: "directory", path: repo.path },
|
||||
});
|
||||
if (!createdWorkspace.workspace) {
|
||||
throw new Error(createdWorkspace.error ?? "Failed to create workspace");
|
||||
}
|
||||
const agent = await client.createAgent({
|
||||
provider: "mock",
|
||||
cwd: repo.path,
|
||||
workspaceId: opened.workspace.id,
|
||||
workspaceId: createdWorkspace.workspace.id,
|
||||
model: opts.model,
|
||||
});
|
||||
const agentUrl = `${buildHostWorkspaceRoute(serverId, opened.workspace.id)}?open=${encodeURIComponent(`agent:${agent.id}`)}`;
|
||||
const agentUrl = `${buildHostWorkspaceRoute(serverId, createdWorkspace.workspace.id)}?open=${encodeURIComponent(`agent:${agent.id}`)}`;
|
||||
await page.goto(agentUrl);
|
||||
await expectComposerVisible(page);
|
||||
await client.sendAgentMessage(agent.id, opts.prompt);
|
||||
@@ -188,10 +192,14 @@ export async function openGithubWorkspace(
|
||||
repoPath: string,
|
||||
): Promise<GithubWorkspaceHandle> {
|
||||
const client = await connectWorkspaceSetupClient();
|
||||
const opened = await client.openProject(repoPath);
|
||||
if (!opened.workspace) throw new Error(opened.error ?? `Failed to open project ${repoPath}`);
|
||||
const createdWorkspace = await client.createWorkspace({
|
||||
source: { kind: "directory", path: repoPath },
|
||||
});
|
||||
if (!createdWorkspace.workspace) {
|
||||
throw new Error(createdWorkspace.error ?? `Failed to create workspace ${repoPath}`);
|
||||
}
|
||||
await gotoAppShell(page);
|
||||
await selectWorkspaceInSidebar(page, opened.workspace.id);
|
||||
await selectWorkspaceInSidebar(page, createdWorkspace.workspace.id);
|
||||
await waitForTabBar(page);
|
||||
return { cleanup: () => client.close().catch(() => undefined) };
|
||||
}
|
||||
|
||||
@@ -13,16 +13,16 @@ type NewWorkspaceDaemonClient = Pick<
|
||||
| "close"
|
||||
| "connect"
|
||||
| "createPaseoWorktree"
|
||||
| "createWorkspace"
|
||||
| "fetchWorkspaces"
|
||||
| "getPaseoWorktreeList"
|
||||
| "getDaemonConfig"
|
||||
| "openProject"
|
||||
| "patchDaemonConfig"
|
||||
>;
|
||||
|
||||
type OpenProjectPayload = Awaited<ReturnType<NewWorkspaceDaemonClient["openProject"]>>;
|
||||
type WorkspacePayload = Pick<OpenProjectPayload, "error" | "workspace">;
|
||||
type WorkspaceDescriptor = NonNullable<OpenProjectPayload["workspace"]>;
|
||||
type CreateWorkspacePayload = Awaited<ReturnType<NewWorkspaceDaemonClient["createWorkspace"]>>;
|
||||
type WorkspacePayload = Pick<CreateWorkspacePayload, "error" | "workspace">;
|
||||
type WorkspaceDescriptor = NonNullable<CreateWorkspacePayload["workspace"]>;
|
||||
|
||||
export interface OpenedProject {
|
||||
workspaceId: string;
|
||||
@@ -37,7 +37,7 @@ function requireWorkspace(payload: WorkspacePayload) {
|
||||
throw new Error(payload.error);
|
||||
}
|
||||
if (!payload.workspace) {
|
||||
throw new Error("openProject returned no workspace.");
|
||||
throw new Error("workspace.create returned no workspace.");
|
||||
}
|
||||
return payload.workspace;
|
||||
}
|
||||
@@ -96,7 +96,11 @@ export async function openProjectViaDaemon(
|
||||
client: NewWorkspaceDaemonClient,
|
||||
repoPath: string,
|
||||
): Promise<OpenedProject> {
|
||||
const workspace = requireWorkspace(await client.openProject(repoPath));
|
||||
const workspace = requireWorkspace(
|
||||
await client.createWorkspace({
|
||||
source: { kind: "directory", path: repoPath },
|
||||
}),
|
||||
);
|
||||
return openedProjectFromWorkspace(workspace);
|
||||
}
|
||||
|
||||
|
||||
@@ -171,25 +171,27 @@ export async function launchAgent(input: {
|
||||
execFileSync("git", ["add", "README.md"], { cwd: input.cwd, stdio: "ignore" });
|
||||
execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: input.cwd, stdio: "ignore" });
|
||||
const client = await connectSeedClient();
|
||||
const opened = await client.openProject(input.cwd);
|
||||
if (!opened.workspace) {
|
||||
throw new Error(opened.error ?? `Failed to open project ${input.cwd}`);
|
||||
const createdWorkspace = await client.createWorkspace({
|
||||
source: { kind: "directory", path: input.cwd },
|
||||
});
|
||||
if (!createdWorkspace.workspace) {
|
||||
throw new Error(createdWorkspace.error ?? `Failed to create workspace ${input.cwd}`);
|
||||
}
|
||||
const agent = await client.createAgent({
|
||||
...fullAccessConfig(input.provider),
|
||||
cwd: input.cwd,
|
||||
workspaceId: opened.workspace.id,
|
||||
workspaceId: createdWorkspace.workspace.id,
|
||||
title: `rewind-flow-${input.provider}-${randomUUID()}`,
|
||||
});
|
||||
const handle = {
|
||||
page: input.page,
|
||||
client,
|
||||
agentId: agent.id,
|
||||
workspaceId: opened.workspace.id,
|
||||
workspaceId: createdWorkspace.workspace.id,
|
||||
cwd: input.cwd,
|
||||
provider: input.provider,
|
||||
};
|
||||
await openAgent(input.page, { workspaceId: opened.workspace.id, agentId: agent.id });
|
||||
await openAgent(input.page, { workspaceId: createdWorkspace.workspace.id, agentId: agent.id });
|
||||
return handle;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,15 @@ import type { TerminalActivity } from "@getpaseo/protocol/terminal-activity";
|
||||
import { connectDaemonClient } from "./daemon-client-loader";
|
||||
import { createTempDirectory, createTempGitRepo } from "./workspace";
|
||||
|
||||
export interface SeedWorkspaceDescriptor {
|
||||
id: string;
|
||||
name: string;
|
||||
projectId: string;
|
||||
projectDisplayName: string;
|
||||
projectRootPath: string;
|
||||
workspaceDirectory: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The general-purpose E2E daemon client used to seed and drive state out of
|
||||
* band (workspaces, agents, terminals) while the UI is exercised through the
|
||||
@@ -13,17 +22,17 @@ import { createTempDirectory, createTempGitRepo } from "./workspace";
|
||||
export interface SeedDaemonClient {
|
||||
connect(): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
openProject(cwd: string): Promise<{
|
||||
workspace: {
|
||||
id: string;
|
||||
name: string;
|
||||
addProject(cwd: string): Promise<{
|
||||
project: {
|
||||
projectId: string;
|
||||
projectDisplayName: string;
|
||||
projectRootPath: string;
|
||||
workspaceDirectory: string;
|
||||
} | null;
|
||||
error: string | null;
|
||||
}>;
|
||||
fetchWorkspaces(options?: { filter?: { projectId?: string } }): Promise<{
|
||||
entries: SeedWorkspaceDescriptor[];
|
||||
}>;
|
||||
createWorkspace(input: {
|
||||
source:
|
||||
| { kind: "directory"; path: string; projectId?: string }
|
||||
@@ -39,7 +48,7 @@ export interface SeedDaemonClient {
|
||||
};
|
||||
title?: string;
|
||||
}): Promise<{
|
||||
workspace: { id: string; name: string } | null;
|
||||
workspace: SeedWorkspaceDescriptor | null;
|
||||
error: string | null;
|
||||
}>;
|
||||
/**
|
||||
@@ -126,7 +135,7 @@ export interface SeedDaemonClient {
|
||||
agentId: string,
|
||||
): Promise<{ agent: { id: string; archivedAt?: string | null } } | null>;
|
||||
getLastServerInfoMessage(): {
|
||||
features?: { worktreeRestore?: boolean } | null;
|
||||
features?: { projectAdd?: boolean; worktreeRestore?: boolean } | null;
|
||||
} | null;
|
||||
fetchAgentHistory(options?: {
|
||||
page?: { limit: number };
|
||||
@@ -184,18 +193,20 @@ export async function seedWorkspace(options: {
|
||||
: await createTempGitRepo(options.repoPrefix, options.repo);
|
||||
const client = await connectSeedClient();
|
||||
try {
|
||||
const opened = await client.openProject(project.path);
|
||||
if (!opened.workspace) {
|
||||
throw new Error(opened.error ?? `Failed to open project ${project.path}`);
|
||||
const created = await client.createWorkspace({
|
||||
source: { kind: "directory", path: project.path },
|
||||
});
|
||||
if (!created.workspace) {
|
||||
throw new Error(created.error ?? `Failed to create workspace ${project.path}`);
|
||||
}
|
||||
return {
|
||||
client,
|
||||
repoPath: project.path,
|
||||
workspaceId: opened.workspace.id,
|
||||
workspaceName: opened.workspace.name,
|
||||
workspaceDirectory: opened.workspace.workspaceDirectory,
|
||||
projectId: opened.workspace.projectId,
|
||||
projectDisplayName: opened.workspace.projectDisplayName,
|
||||
workspaceId: created.workspace.id,
|
||||
workspaceName: created.workspace.name,
|
||||
workspaceDirectory: created.workspace.workspaceDirectory,
|
||||
projectId: created.workspace.projectId,
|
||||
projectDisplayName: created.workspace.projectDisplayName,
|
||||
cleanup: async () => {
|
||||
await client.close().catch(() => undefined);
|
||||
await project.cleanup().catch(() => undefined);
|
||||
|
||||
@@ -43,7 +43,9 @@ export class TerminalE2EHarness {
|
||||
static async create(input: { tempPrefix: string }): Promise<TerminalE2EHarness> {
|
||||
const tempRepo = await createTempGitRepo(input.tempPrefix);
|
||||
const client = await connectSeedClient();
|
||||
const seedResult = await client.openProject(tempRepo.path);
|
||||
const seedResult = await client.createWorkspace({
|
||||
source: { kind: "directory", path: tempRepo.path },
|
||||
});
|
||||
if (!seedResult.workspace) {
|
||||
await client.close().catch(() => {});
|
||||
await tempRepo.cleanup().catch(() => {});
|
||||
|
||||
@@ -60,14 +60,19 @@ export function createWithWorkspace(page: Page): WithWorkspaceHandle {
|
||||
);
|
||||
worktrees.push({ repoPath: repo.path, worktreePath: workspacePath });
|
||||
// Register the parent project so the sidebar lists it before we navigate.
|
||||
await client.openProject(repo.path);
|
||||
const added = await client.addProject(repo.path);
|
||||
if (!added.project) {
|
||||
throw new Error(added.error ?? `Failed to add project ${repo.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
const opened = await client.openProject(workspacePath);
|
||||
if (!opened.workspace) {
|
||||
throw new Error(opened.error ?? `Failed to open project ${workspacePath}`);
|
||||
const created = await client.createWorkspace({
|
||||
source: { kind: "directory", path: workspacePath },
|
||||
});
|
||||
if (!created.workspace) {
|
||||
throw new Error(created.error ?? `Failed to create workspace ${workspacePath}`);
|
||||
}
|
||||
const workspaceId = opened.workspace.id;
|
||||
const workspaceId = created.workspace.id;
|
||||
|
||||
return {
|
||||
workspaceId,
|
||||
|
||||
@@ -11,13 +11,14 @@ import type { SessionOutboundMessage } from "@getpaseo/protocol/messages";
|
||||
type WorkspaceSetupDaemonClient = Pick<
|
||||
InternalDaemonClient,
|
||||
| "close"
|
||||
| "addProject"
|
||||
| "connect"
|
||||
| "createPaseoWorktree"
|
||||
| "createWorkspace"
|
||||
| "fetchAgent"
|
||||
| "fetchAgents"
|
||||
| "fetchWorkspaces"
|
||||
| "listTerminals"
|
||||
| "openProject"
|
||||
| "subscribeRawMessages"
|
||||
>;
|
||||
|
||||
@@ -36,9 +37,11 @@ export async function openProjectViaDaemon(
|
||||
client: WorkspaceSetupDaemonClient,
|
||||
repoPath: string,
|
||||
): Promise<{ id: string; name: string; workspaceDirectory: string }> {
|
||||
const result = await client.openProject(repoPath);
|
||||
const result = await client.createWorkspace({
|
||||
source: { kind: "directory", path: repoPath },
|
||||
});
|
||||
if (!result.workspace || result.error) {
|
||||
throw new Error(result.error ?? `Failed to open project ${repoPath}`);
|
||||
throw new Error(result.error ?? `Failed to create workspace ${repoPath}`);
|
||||
}
|
||||
return {
|
||||
id: result.workspace.id,
|
||||
@@ -51,7 +54,10 @@ export async function seedProjectForWorkspaceSetup(
|
||||
client: WorkspaceSetupDaemonClient,
|
||||
repoPath: string,
|
||||
): Promise<void> {
|
||||
await openProjectViaDaemon(client, repoPath);
|
||||
const result = await client.addProject(repoPath);
|
||||
if (!result.project || result.error) {
|
||||
throw new Error(result.error ?? `Failed to add project ${repoPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function projectNameFromPath(repoPath: string): string {
|
||||
|
||||
@@ -8,8 +8,8 @@ export async function openNewAgentComposer(page: Page): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the sidebar to show at least one project row, indicating that the
|
||||
* WebSocket connection is up and workspace hydration has completed.
|
||||
* Wait for the sidebar to show at least one project row. Use this after a spec
|
||||
* seeds a workspace/project; zero-project flows need their own assertion.
|
||||
*/
|
||||
export async function waitForSidebarHydration(page: Page, timeout = 60_000): Promise<void> {
|
||||
await page
|
||||
@@ -18,6 +18,10 @@ export async function waitForSidebarHydration(page: Page, timeout = 60_000): Pro
|
||||
.waitFor({ state: "visible", timeout });
|
||||
}
|
||||
|
||||
export async function waitForNoProjectsInSidebar(page: Page, timeout = 60_000): Promise<void> {
|
||||
await page.getByTestId("sidebar-project-empty-state").waitFor({ state: "visible", timeout });
|
||||
}
|
||||
|
||||
function workspaceRowLocator(page: Page, serverId: string, workspaceId: string) {
|
||||
return page.getByTestId(`sidebar-workspace-row-${serverId}:${workspaceId}`).first();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user