diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts
index c1ea0e061b..a6de9b227d 100644
--- a/desktop/playwright.config.ts
+++ b/desktop/playwright.config.ts
@@ -107,6 +107,7 @@ export default defineConfig({
"**/project-commit-detail.spec.ts",
"**/project-inbox.spec.ts",
"**/project-issue-comments.spec.ts",
+ "**/projects-empty-state.spec.ts",
"**/project-pr-review.spec.ts",
"**/persona-model-combobox-screenshots.spec.ts",
"**/drafts-screenshots.spec.ts",
diff --git a/desktop/src/features/projects/ui/ProjectsView.tsx b/desktop/src/features/projects/ui/ProjectsView.tsx
index 78ba09934d..35029c89b5 100644
--- a/desktop/src/features/projects/ui/ProjectsView.tsx
+++ b/desktop/src/features/projects/ui/ProjectsView.tsx
@@ -592,9 +592,10 @@ export function ProjectsView() {
);
}
- if (projects.length === 0) {
- return ;
- }
+ // Zero projects still renders the full chrome — the create menu lives in the
+ // toolbar, so returning early here would leave no way to create the first
+ // project from the UI.
+ const hasProjects = projects.length > 0;
const projectItems =
visibleProjects.length === 0 ? (
@@ -850,7 +851,9 @@ export function ProjectsView() {
- {filter === "all" ? (
+ {!hasProjects ? (
+
+ ) : filter === "all" ? (
{
+ window.localStorage.setItem(
+ featureOverridesStorageKey,
+ JSON.stringify({ projects: true }),
+ );
+ window.localStorage.setItem(
+ "buzz.projects.hidden-cards.v1",
+ JSON.stringify(coordinates),
+ );
+ },
+ {
+ coordinates: ALL_MOCK_PROJECT_COORDINATES,
+ featureOverridesStorageKey: FEATURE_OVERRIDES_STORAGE_KEY,
+ },
+ );
+ await installMockBridge(page);
+ await page.goto("/", { waitUntil: "domcontentloaded" });
+ await page.getByTestId("open-projects-view").click();
+}
+
+test("projects header and create menu stay mounted with zero projects", async ({
+ page,
+}) => {
+ await openEmptyProjectsView(page);
+
+ await expect(
+ page.getByRole("heading", { level: 1, name: "Projects" }),
+ ).toBeVisible();
+ await expect(page.getByText("No projects yet")).toBeVisible();
+ await expect(
+ page.getByRole("button", { name: "Repositories", exact: true }),
+ ).toBeVisible();
+
+ // The regression: the create menu was unreachable until a project already
+ // existed, so the first project could never be created from the UI.
+ await page.getByTestId("projects-create-menu").hover();
+ await page.getByRole("menuitem", { name: "Project" }).click();
+ await expect(page.getByTestId("create-project-dialog")).toBeVisible();
+ await expect(page.getByTestId("create-project-name")).toBeVisible();
+});
+
+test("empty state replaces only the content area on every filter", async ({
+ page,
+}) => {
+ await openEmptyProjectsView(page);
+
+ for (const filter of ["Repositories", "Pull Requests", "Issues"]) {
+ await page.getByRole("button", { name: filter, exact: true }).click();
+ await expect(page.getByText("No projects yet")).toBeVisible();
+ await expect(page.getByTestId("projects-create-menu")).toBeVisible();
+ }
+});