Start new workspaces from pasted pull requests (#2290)

* fix(workspaces): start pasted pull requests from their branch

Recognized pull request links now select their branch directly instead of
waiting for a separate confirmation. Ensure single-branch clones also record
the fetched branch so the new worktree can track it.

* fix(workspaces): keep pasted PR checkout target-safe

* fix(workspaces): make pasted PR resolution deterministic

* fix(workspaces): preserve explicit checkout selection

A recognized pull request selects itself only when its attachment is added. Once the user chooses a branch, later composer edits must not derive checkout state from existing attachments.

* fix(composer): ignore stale pull request lookups

A lookup may finish after the workspace target changes. Only apply its attachment and selection event when the host, working directory, client, and remote still match the target that started it.

* fix(workspaces): make checkout choices authoritative

Arm automatic pull request selection only when a new PR link is detected. An explicit picker choice cancels that pending selection, existing draft attachments never infer checkout state, and target changes suppress carried-over lookups.

* fix(workspaces): stabilize pull request auto-selection

Keep the first PR selected when one edit contains several links, and accept a completed lookup when only the transport client changed for the same host and checkout.

* fix(composer): release removed pull request lookups

Release each auto-attach invocation when its composer state is abandoned, while continuing to discard any stale response.
This commit is contained in:
Mohamed Boudra
2026-07-21 15:20:05 +02:00
committed by GitHub
parent fd4e13735c
commit 2cb1b041dd
20 changed files with 768 additions and 313 deletions
+107
View File
@@ -17,11 +17,14 @@ import {
expectPickerOpen,
expectPickerSelected,
expectStartingRefPickerTriggerPr,
fillNewWorkspaceDraft,
openGlobalNewWorkspaceComposer,
openBranchPicker,
openNewWorkspaceComposer,
openProjectViaDaemon,
openStartingRefPicker,
pasteGithubPrUrl,
searchAndSelectBranchInPicker,
selectBranchInPicker,
selectGitHubPrInPicker,
selectPickerOptionByKeyboard,
@@ -30,9 +33,11 @@ import {
} from "./helpers/new-workspace";
import { createTempGitRepo, readWorktreeBranchInfo } from "./helpers/workspace";
import {
createLocalGithubPrFixture,
cloneGithubRepoDefaultBranchOnly,
createTempGithubRepo,
hasGithubAuth,
type LocalGhPrFixture,
} from "./helpers/github-fixtures";
import { getServerId } from "./helpers/server-id";
import { getE2EDaemonPort } from "./helpers/daemon-port";
@@ -191,6 +196,7 @@ test.describe("New workspace flow", () => {
let client: Awaited<ReturnType<typeof connectNewWorkspaceDaemonClient>>;
const localWorkspaceIds = new Set<string>();
const createdWorktreeDirectories = new Set<string>();
const localGithubFixtures = new Set<LocalGhPrFixture>();
test.describe.configure({ timeout: 240_000 });
@@ -212,6 +218,13 @@ test.describe("New workspace flow", () => {
await client?.close().catch(() => undefined);
});
test.afterAll(async () => {
for (const fixture of localGithubFixtures) {
await fixture.cleanup();
}
localGithubFixtures.clear();
});
test("adds a project from the selected empty host", async ({ page }) => {
const repo = await createTempGitRepo("new-workspace-project-picker-");
const primaryServerId = getServerId();
@@ -807,6 +820,100 @@ test.describe("New workspace flow", () => {
}
});
test("pasted GitHub PR replaces a selected branch and creates its worktree", async ({
page,
context,
}) => {
const fixture = await createLocalGithubPrFixture();
localGithubFixtures.add(fixture);
const { pr, mainCheckout } = fixture;
const openedProject = await openProjectViaDaemon(client, mainCheckout.path);
localWorkspaceIds.add(openedProject.workspaceId);
await gotoAppShell(page);
await waitForSidebarHydration(page);
await openNewWorkspaceComposer(page, {
projectKey: openedProject.projectKey,
projectDisplayName: openedProject.projectDisplayName,
});
await selectWorkspaceIsolation(page, "worktree");
await openStartingRefPicker(page);
await selectBranchInPicker(page, "main");
await pasteGithubPrUrl(page, context, pr.url);
await expect(page.getByTestId("workspace-create-submit")).toBeDisabled();
await expectComposerGithubAttachmentPill(page, {
number: pr.number,
title: pr.title,
});
await expectStartingRefPickerTriggerPr(page, {
number: pr.number,
title: pr.title,
headRef: pr.branch,
});
await submitNewWorkspaceWithoutPrompt(page);
const worktree = await assertNewWorkspaceSidebarAndHeader(page, {
serverId: getServerId(),
client,
previousWorkspaceId: openedProject.workspaceId,
projectDisplayName: openedProject.projectDisplayName,
});
createdWorktreeDirectories.add(worktree.workspaceDirectory);
const branchInfo = await readWorktreeBranchInfo({
worktreePath: worktree.workspaceDirectory,
});
expect(branchInfo.currentBranch).toBe(pr.branch);
expect(existsSync(path.join(worktree.workspaceDirectory, "pr-1.txt"))).toBe(true);
});
test("branches remain searchable after a pasted PR and determine the created worktree", async ({
page,
context,
}) => {
const fixture = await createLocalGithubPrFixture();
localGithubFixtures.add(fixture);
const { pr, mainCheckout } = fixture;
const openedProject = await openProjectViaDaemon(client, mainCheckout.path);
localWorkspaceIds.add(openedProject.workspaceId);
await gotoAppShell(page);
await waitForSidebarHydration(page);
await openNewWorkspaceComposer(page, {
projectKey: openedProject.projectKey,
projectDisplayName: openedProject.projectDisplayName,
});
await selectWorkspaceIsolation(page, "worktree");
await pasteGithubPrUrl(page, context, pr.url);
await expectStartingRefPickerTriggerPr(page, {
number: pr.number,
title: pr.title,
headRef: pr.branch,
});
await openStartingRefPicker(page);
await searchAndSelectBranchInPicker(page, "main");
await expectPickerSelected(page, "main");
await fillNewWorkspaceDraft(page, `${pr.url}\nKeep this checkout on main`);
await expectPickerSelected(page, "main");
await submitNewWorkspaceWithoutPrompt(page);
const worktree = await assertNewWorkspaceSidebarAndHeader(page, {
serverId: getServerId(),
client,
previousWorkspaceId: openedProject.workspaceId,
projectDisplayName: openedProject.projectDisplayName,
});
createdWorktreeDirectories.add(worktree.workspaceDirectory);
expect(existsSync(path.join(worktree.workspaceDirectory, "pr-1.txt"))).toBe(false);
});
test("selected GitHub PR creates the worktree from the PR head even when the head branch is not fetched", async ({
page,
}) => {