Show New Workspace isolation controls immediately (#2702)

* fix(workspaces): show isolation without checkout delay

* fix(workspaces): trust hydrated project capability

* fix(workspaces): resolve branch before creation

* fix(workspaces): honor invalidated checkout status

* fix(workspaces): preserve isolation while projects load

* fix(workspaces): keep provisional project routes usable

Route-derived projects do not know their worktree capability. Represent that uncertainty at the placement boundary so remembered worktree intent survives hydration without disabling the New Workspace screen indefinitely.
This commit is contained in:
Mohamed Boudra
2026-07-31 17:50:35 +02:00
committed by GitHub
parent 35f985baff
commit de44c8cd66
20 changed files with 333 additions and 105 deletions
+25 -3
View File
@@ -190,6 +190,29 @@ export async function openGlobalNewWorkspaceComposer(page: Page): Promise<void>
});
}
export async function openMissingProjectNewWorkspaceComposer(
page: Page,
input: { serverId: string; projectId: string; sourceDirectory: string },
): Promise<void> {
const query = new URLSearchParams({
serverId: input.serverId,
projectId: input.projectId,
dir: input.sourceDirectory,
name: "Missing project",
});
await page.goto(`/new?${query.toString()}`);
await expect(page).toHaveURL(/\/new\?.*projectId=/u, { timeout: 30_000 });
}
export async function expectNewWorkspaceControlsEnabled(page: Page): Promise<void> {
await expect(page.getByRole("button", { name: "Workspace project" })).toBeEnabled({
timeout: 30_000,
});
await expect(page.getByRole("textbox", { name: "Message agent..." })).toBeEditable({
timeout: 30_000,
});
}
export async function openNewWorkspaceProjectPickerWithShortcut(page: Page): Promise<void> {
await page.keyboard.press("Control+P");
@@ -286,9 +309,8 @@ export async function selectWorkspaceIsolation(
await expect(trigger).toBeVisible({ timeout: 30_000 });
await trigger.click();
// "New worktree" is only listed once the checkout status query confirms the
// selected project is a git repo, so wait for the option to appear before
// clicking it.
// Isolation options are derived from project capability. Wait for the option
// so this helper also covers route-to-project reconciliation.
const option = page.getByTestId(`workspace-create-isolation-${isolation}`);
await expect(option).toBeVisible({ timeout: 30_000 });
await option.click();
+15 -1
View File
@@ -2,8 +2,10 @@ import { expect, test } from "./fixtures";
import { gotoAppShell } from "./helpers/app";
import {
connectNewWorkspaceDaemonClient,
expectNewWorkspaceControlsEnabled,
expectNewWorkspaceProjectSelected,
openGlobalNewWorkspaceComposer,
openMissingProjectNewWorkspaceComposer,
openNewWorkspaceComposer,
openNewWorkspaceProjectPickerWithShortcut,
} from "./helpers/new-workspace";
@@ -13,7 +15,7 @@ import { seedSavedSettingsHosts } from "./helpers/settings";
import { getServerId } from "./helpers/server-id";
import { projectEquivalenceViewKey } from "./helpers/project-view-key";
import { clickArchiveWorkspaceMenuItem, expectWorkspaceAbsentFromSidebar } from "./helpers/sidebar";
import { waitForSidebarHydration } from "./helpers/workspace-ui";
import { waitForNoProjectsInSidebar, waitForSidebarHydration } from "./helpers/workspace-ui";
// Model B entry points into the New Workspace screen. The surviving entries are
// the global button (universal) and each project's per-row New workspace icon
@@ -108,6 +110,18 @@ test.describe("New workspace entry points", () => {
}
});
test("a stale project route keeps the New Workspace controls enabled", async ({ page }) => {
await gotoAppShell(page);
await waitForNoProjectsInSidebar(page);
await openMissingProjectNewWorkspaceComposer(page, {
serverId: getServerId(),
projectId: "missing-project",
sourceDirectory: "/tmp/missing-project",
});
await expectNewWorkspaceControlsEnabled(page);
});
test("Ctrl+P opens the project picker with search focused", async ({ page }) => {
const seeded: SeededWorkspace = await seedWorkspace({ repoPrefix: "entry-shortcut-" });