diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts
index 459fa75743..7a67438697 100644
--- a/desktop/playwright.config.ts
+++ b/desktop/playwright.config.ts
@@ -98,6 +98,7 @@ export default defineConfig({
"**/reaction-names.spec.ts",
"**/inbox-reactions.spec.ts",
"**/send-channel-binding.spec.ts",
+ "**/projects-empty.spec.ts",
"**/project-commit-detail.spec.ts",
"**/project-inbox.spec.ts",
"**/project-pr-review.spec.ts",
diff --git a/desktop/src/features/projects/ui/ProjectCards.tsx b/desktop/src/features/projects/ui/ProjectCards.tsx
index 52308f7a05..3f7a3f3531 100644
--- a/desktop/src/features/projects/ui/ProjectCards.tsx
+++ b/desktop/src/features/projects/ui/ProjectCards.tsx
@@ -263,7 +263,7 @@ function StatusPill({ status }: { status: string }) {
);
}
-export function EmptyState() {
+export function EmptyState({ onCreate }: { onCreate: () => void }) {
return (
@@ -273,6 +273,9 @@ export function EmptyState() {
Projects published to this relay will appear here.
+
);
}
diff --git a/desktop/src/features/projects/ui/ProjectsView.tsx b/desktop/src/features/projects/ui/ProjectsView.tsx
index 2f22bfe0c1..8c1a6fef5f 100644
--- a/desktop/src/features/projects/ui/ProjectsView.tsx
+++ b/desktop/src/features/projects/ui/ProjectsView.tsx
@@ -447,10 +447,6 @@ export function ProjectsView() {
);
}
- if (projects.length === 0) {
- return ;
- }
-
const repositoryItems =
visibleProjects.length === 0 ? (
@@ -646,7 +642,9 @@ export function ProjectsView() {
- {filter === "all" ? (
+ {projects.length === 0 ? (
+
setCreateProjectOpen(true)} />
+ ) : filter === "all" ? (
tag[0] === "a" && (tag[1] ?? "").startsWith("30617:"),
);
diff --git a/desktop/tests/e2e/projects-empty.spec.ts b/desktop/tests/e2e/projects-empty.spec.ts
new file mode 100644
index 0000000000..6605e86a32
--- /dev/null
+++ b/desktop/tests/e2e/projects-empty.spec.ts
@@ -0,0 +1,31 @@
+import { expect, test } from "@playwright/test";
+
+import { installMockBridge } from "../helpers/bridge";
+
+test("first repository can be created from the empty projects shell", async ({
+ page,
+}) => {
+ await installMockBridge(page, { emptyProjects: true });
+ await page.goto("/", { waitUntil: "domcontentloaded" });
+ await page.getByTestId("open-projects-view").click();
+
+ await expect(
+ page.getByRole("heading", { level: 1, name: "Projects" }),
+ ).toBeVisible();
+ await expect(page.getByTestId("projects-create-menu")).toBeVisible();
+ await expect(page.getByText("No projects yet")).toBeVisible();
+
+ await page.getByRole("button", { name: "Create repository" }).click();
+ await expect(page.getByTestId("create-project-dialog")).toBeVisible();
+
+ await page.getByTestId("create-project-name").fill("first-repository");
+ await page.getByTestId("create-project-submit").click();
+
+ await expect(page.getByTestId("create-project-dialog")).toBeHidden();
+ await expect(
+ page.getByText('Project "first-repository" created.'),
+ ).toBeVisible();
+ await expect(
+ page.getByText("first-repository", { exact: true }),
+ ).toBeVisible();
+});
diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts
index ca4d62ddd6..fead9edd7a 100644
--- a/desktop/tests/helpers/bridge.ts
+++ b/desktop/tests/helpers/bridge.ts
@@ -132,6 +132,8 @@ export type MockAgentMemoryListing = {
};
type MockBridgeOptions = {
+ /** Start the mock relay without seeded NIP-34 repositories. */
+ emptyProjects?: boolean;
/** Advertised HEAD for the first mock project without adding that branch. */
projectHeadBranch?: string;
/** Relay NIP-11 identity used to sign authoritative repository state. */