diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts
index ee7d4ec9bf0..9984046f3be 100644
--- a/desktop/playwright.config.ts
+++ b/desktop/playwright.config.ts
@@ -121,6 +121,7 @@ export default defineConfig({
"**/send-channel-binding.spec.ts",
"**/project-cold-start.spec.ts",
"**/project-commit-detail.spec.ts",
+ "**/project-empty-state-alignment.spec.ts",
"**/project-inbox.spec.ts",
"**/projects-v3-screenshots.spec.ts",
"**/project-issue-comments.spec.ts",
diff --git a/desktop/src/features/projects/ui/ProjectCards.tsx b/desktop/src/features/projects/ui/ProjectCards.tsx
index e9672d643bd..a1a48e89e47 100644
--- a/desktop/src/features/projects/ui/ProjectCards.tsx
+++ b/desktop/src/features/projects/ui/ProjectCards.tsx
@@ -5,6 +5,7 @@ import {
Folders,
GitCommit,
GitPullRequest,
+ Plus,
TerminalSquare,
Trash2,
} from "lucide-react";
@@ -304,7 +305,11 @@ function RepositoryUnavailableIndicator({
);
}
-export function EmptyState() {
+export function EmptyState({
+ onCreateProject,
+}: {
+ onCreateProject: () => void;
+}) {
return (
@@ -314,6 +319,10 @@ export function EmptyState() {
Projects published to this relay will appear here.
+
);
}
diff --git a/desktop/src/features/projects/ui/ProjectCreationDialog.tsx b/desktop/src/features/projects/ui/ProjectCreationDialog.tsx
new file mode 100644
index 00000000000..1039c087833
--- /dev/null
+++ b/desktop/src/features/projects/ui/ProjectCreationDialog.tsx
@@ -0,0 +1,36 @@
+import { toast } from "sonner";
+
+import { useAppNavigation } from "@/app/navigation/useAppNavigation";
+import { useCreateProjectMutation } from "@/features/projects/useCreateProject";
+import { CreateProjectDialog } from "@/features/projects/ui/CreateProjectDialog";
+
+/** Shared project-creation flow for populated and first-run project views. */
+export function ProjectCreationDialog({
+ onOpenChange,
+ open,
+}: {
+ onOpenChange: (open: boolean) => void;
+ open: boolean;
+}) {
+ const { goProject } = useAppNavigation();
+ const createProjectMutation = useCreateProjectMutation();
+
+ return (
+ {
+ const result = await createProjectMutation.mutateAsync(input);
+ if (result.compatibilityWarning) {
+ toast.warning("Created as a standalone project", {
+ description: result.compatibilityWarning,
+ });
+ } else {
+ toast.success(`Project "${result.project.name}" created.`);
+ }
+ await goProject(result.project.id);
+ }}
+ onOpenChange={onOpenChange}
+ open={open}
+ />
+ );
+}
diff --git a/desktop/src/features/projects/ui/ProjectHomeContextPanel.tsx b/desktop/src/features/projects/ui/ProjectHomeContextPanel.tsx
index 63245ab632c..080ec5aa32a 100644
--- a/desktop/src/features/projects/ui/ProjectHomeContextPanel.tsx
+++ b/desktop/src/features/projects/ui/ProjectHomeContextPanel.tsx
@@ -258,7 +258,7 @@ export function ProjectHomeContextPanel({
return (
diff --git a/desktop/src/features/projects/ui/ProjectsView.tsx b/desktop/src/features/projects/ui/ProjectsView.tsx
index f33659bcfc5..f79cf910cba 100644
--- a/desktop/src/features/projects/ui/ProjectsView.tsx
+++ b/desktop/src/features/projects/ui/ProjectsView.tsx
@@ -1,5 +1,4 @@
import * as React from "react";
-import { toast } from "sonner";
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
import { useManagedAgentsQuery } from "@/features/agents/hooks";
@@ -17,7 +16,6 @@ import {
useProjectsWorkItemsQuery,
} from "@/features/projects/hooks";
import { useRepositoryActivitySummariesQuery } from "@/features/projects/repositoryActivityHooks";
-import { useCreateProjectMutation } from "@/features/projects/useCreateProject";
import { isExplicitProject } from "@/features/projects/projectModels";
import { projectsWithWorkItemRepositories } from "@/features/projects/projectWorkItems";
import { useProjectsRepoSnapshotsQuery } from "@/features/projects/useProjectsRepoSnapshots";
@@ -52,7 +50,7 @@ import {
ProjectsOverviewProjectItems,
ProjectsOverviewRepositoryItems,
} from "@/features/projects/ui/ProjectsOverviewItems";
-import { CreateProjectDialog } from "@/features/projects/ui/CreateProjectDialog";
+import { ProjectCreationDialog } from "@/features/projects/ui/ProjectCreationDialog";
import { CreateProjectIssueDialog } from "@/features/projects/ui/CreateProjectIssueDialog";
import { CreatePullRequestDialog } from "@/features/projects/ui/CreatePullRequestDialog";
import { ProjectAgentChatPanel } from "@/features/projects/ui/ProjectAgentChatPanel";
@@ -183,7 +181,6 @@ export function ProjectsView() {
const [createIssueOpen, setCreateIssueOpen] = React.useState(false);
const [createPullRequestOpen, setCreatePullRequestOpen] =
React.useState(false);
- const createProjectMutation = useCreateProjectMutation();
const [storedViewMode, setStoredViewMode] =
React.useState(() => readStoredViewMode());
const [sort, setSort] = React.useState(() => readStoredSort());
@@ -566,7 +563,15 @@ export function ProjectsView() {
}
if (projectReadModels.length === 0) {
- return ;
+ return (
+ <>
+
+ setCreateProjectOpen(true)} />
+ >
+ );
}
const projectItems = (
@@ -740,19 +745,7 @@ export function ProjectsView() {
overviewDetached ? "projects-overview-content-pod" : undefined
}
>
- {
- const result = await createProjectMutation.mutateAsync(input);
- if (result.compatibilityWarning) {
- toast.warning("Created as a standalone project", {
- description: result.compatibilityWarning,
- });
- } else {
- toast.success(`Project "${result.project.name}" created.`);
- }
- await goProject(result.project.id);
- }}
+
diff --git a/desktop/src/features/projects/ui/projectsOverviewContext.test.mjs b/desktop/src/features/projects/ui/projectsOverviewContext.test.mjs
index 60e3b650a20..cef99805f15 100644
--- a/desktop/src/features/projects/ui/projectsOverviewContext.test.mjs
+++ b/desktop/src/features/projects/ui/projectsOverviewContext.test.mjs
@@ -72,7 +72,7 @@ test("activity pod shows workspace details without a create action", () => {
},
});
- assert.equal(context.title, "Projects");
+ assert.equal(context.title, "Activity");
assert.equal(context.detailsTitle, "Details");
assert.equal(context.action, null);
assert.deepEqual(
diff --git a/desktop/src/features/projects/ui/projectsOverviewContext.ts b/desktop/src/features/projects/ui/projectsOverviewContext.ts
index 0b4c5bfc693..8d717e90eb5 100644
--- a/desktop/src/features/projects/ui/projectsOverviewContext.ts
+++ b/desktop/src/features/projects/ui/projectsOverviewContext.ts
@@ -405,7 +405,7 @@ export function projectsOverviewContext(
section: "prs",
},
],
- title: "Projects",
+ title: "Activity",
};
}
diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts
index c620c577648..b06b1fa627c 100644
--- a/desktop/src/testing/e2eBridge.ts
+++ b/desktop/src/testing/e2eBridge.ts
@@ -1259,6 +1259,8 @@ declare global {
__BUZZ_E2E_REPOSITORY_ONLY_PROJECTS__?: boolean;
/** Leaves broad project enumeration pending while scoped project queries remain available. */
__BUZZ_E2E_DEFER_FULL_PROJECT_QUERIES__?: boolean;
+ /** Omits all seeded project and repository events for empty-state tests. */
+ __BUZZ_E2E_EMPTY_PROJECTS__?: boolean;
/** Project-scoped events accepted by the mock relay. */
__BUZZ_E2E_ACCEPTED_PROJECT_EVENTS__?: Array<{
content: string;
@@ -5912,6 +5914,10 @@ function writeMockProjectBranch(
}
function buildMockProjectEvents(): RelayEvent[] {
+ if (window.__BUZZ_E2E_EMPTY_PROJECTS__) {
+ return [];
+ }
+
const events: RelayEvent[] = [];
const daySeconds = 86_400;
const now = Math.floor(Date.now() / 1000);
diff --git a/desktop/tests/e2e/project-empty-state-alignment.spec.ts b/desktop/tests/e2e/project-empty-state-alignment.spec.ts
new file mode 100644
index 00000000000..2532e32453d
--- /dev/null
+++ b/desktop/tests/e2e/project-empty-state-alignment.spec.ts
@@ -0,0 +1,69 @@
+import { expect, test } from "@playwright/test";
+
+import { waitForAnimations } from "../helpers/animations";
+import { installMockBridge } from "../helpers/bridge";
+
+const ALIGNMENT_TOLERANCE_PX = 2;
+
+async function enableProjectsFeature(page: import("@playwright/test").Page) {
+ await page.addInitScript(() => {
+ window.localStorage.setItem(
+ "buzz-feature-overrides-v1",
+ JSON.stringify({ projects: true }),
+ );
+ });
+}
+
+async function addProjectToSidebar(
+ page: import("@playwright/test").Page,
+ dtag: string,
+) {
+ await page.getByTestId("sidebar-projects-section-label").hover();
+ await page.getByTestId("sidebar-projects-create").click();
+ const browser = page.getByTestId("project-browser-dialog");
+ await browser.getByRole("searchbox", { name: "Search projects" }).fill(dtag);
+ await browser.getByTestId(`project-browser-result-${dtag}`).click();
+ await expect(browser).toBeHidden();
+ await expect(page.getByTestId(`sidebar-project-${dtag}`)).toBeVisible();
+}
+
+test("first-time project empty state opens project creation", async ({
+ page,
+}) => {
+ await enableProjectsFeature(page);
+ await page.addInitScript(() => {
+ window.__BUZZ_E2E_EMPTY_PROJECTS__ = true;
+ });
+ await installMockBridge(page);
+ await page.goto("/", { waitUntil: "domcontentloaded" });
+ await page.getByTestId("open-projects-view").click();
+
+ await expect(
+ page.getByRole("main").getByText("No projects yet"),
+ ).toBeVisible();
+ await page.getByRole("button", { name: "Create project" }).click();
+ await expect(page.getByTestId("create-project-dialog")).toBeVisible();
+});
+
+test("project home context aligns with the channel header", async ({
+ page,
+}) => {
+ await enableProjectsFeature(page);
+ await installMockBridge(page);
+ await page.goto("/", { waitUntil: "domcontentloaded" });
+ await addProjectToSidebar(page, "buzz");
+ await waitForAnimations(page);
+
+ const [headerTitleBox, tasksBox] = await Promise.all([
+ page.getByTestId("chat-title").boundingBox(),
+ page.getByTestId("project-home-context-tasks").boundingBox(),
+ ]);
+ expect(headerTitleBox).not.toBeNull();
+ expect(tasksBox).not.toBeNull();
+ const headerTitleCenter =
+ (headerTitleBox?.y ?? 0) + (headerTitleBox?.height ?? 0) / 2;
+ const tasksCenter = (tasksBox?.y ?? 0) + (tasksBox?.height ?? 0) / 2;
+ expect(Math.abs(headerTitleCenter - tasksCenter)).toBeLessThanOrEqual(
+ ALIGNMENT_TOLERANCE_PX,
+ );
+});
diff --git a/desktop/tests/e2e/project-pr-review.spec.ts b/desktop/tests/e2e/project-pr-review.spec.ts
index ace54e9fa47..16d249a6dbb 100644
--- a/desktop/tests/e2e/project-pr-review.spec.ts
+++ b/desktop/tests/e2e/project-pr-review.spec.ts
@@ -1722,7 +1722,7 @@ test("project overview presents collapsible context beside grouped activity", as
page.getByTestId("projects-overview-context-panel"),
).toBeVisible();
await expect(page.getByTestId("projects-overview-context-title")).toHaveText(
- "Projects",
+ "Activity",
);
expect(
Math.round(
@@ -1757,7 +1757,7 @@ test("project overview presents collapsible context beside grouped activity", as
await expect(
page
.getByTestId("projects-overview-context-panel")
- .getByRole("heading", { name: "Activity" }),
+ .getByRole("heading", { level: 3, name: "Activity" }),
).toHaveCount(0);
await expect(
page
@@ -1932,7 +1932,7 @@ test("project overview presents collapsible context beside grouped activity", as
page.getByTestId("projects-overview-context-panel"),
).toBeVisible();
await expect(page.getByTestId("projects-overview-context-title")).toHaveText(
- "Projects",
+ "Activity",
);
await expect(page.getByTestId("projects-overview-activity")).toHaveCount(0);