diff --git a/desktop/src/features/projects/ui/ProjectAgentChatPanel.tsx b/desktop/src/features/projects/ui/ProjectAgentChatPanel.tsx index 1e75e1b5c..60270f659 100644 --- a/desktop/src/features/projects/ui/ProjectAgentChatPanel.tsx +++ b/desktop/src/features/projects/ui/ProjectAgentChatPanel.tsx @@ -22,6 +22,7 @@ import { useProfileQuery, useUsersBatchQuery } from "@/features/profile/hooks"; import { useIdentityQuery } from "@/shared/api/hooks"; import { sendChannelMessage } from "@/shared/api/tauri"; import type { Channel } from "@/shared/api/types"; +import { cn } from "@/shared/lib/cn"; import { normalizePubkey } from "@/shared/lib/pubkey"; import { Button } from "@/shared/ui/button"; import { @@ -42,6 +43,7 @@ export function ProjectAgentChatPanel({ canResetWidth, constrainToAvailableSpace = true, context, + detached = false, onClose, onResetWidth, onResizeStart, @@ -51,6 +53,7 @@ export function ProjectAgentChatPanel({ canResetWidth: boolean; constrainToAvailableSpace?: boolean; context: ProjectDetailAgentContext; + detached?: boolean; onClose?: () => void; onResetWidth: () => void; onResizeStart: (event: React.PointerEvent) => void; @@ -166,79 +169,87 @@ export function ProjectAgentChatPanel({ - -
-
- {conversation ? ( - - ) : ( -
-

- Ask about this page -

-

- Start a conversation with the project agent. -

-
- )} -
- {context.selection?.length ? ( - - ) : null} - - - - ) : null - } +
+ +
+
+ {conversation ? ( + + ) : ( +
+

+ Ask about this page +

+

+ Start a conversation with the project agent. +

+
+ )} +
+ {context.selection?.length ? ( + + ) : null} + + + + ) : null + } + /> +
); diff --git a/desktop/src/features/projects/ui/ProjectCards.tsx b/desktop/src/features/projects/ui/ProjectCards.tsx index 45168744c..2a057fad7 100644 --- a/desktop/src/features/projects/ui/ProjectCards.tsx +++ b/desktop/src/features/projects/ui/ProjectCards.tsx @@ -20,10 +20,8 @@ import type { ProjectActivitySummary, } from "@/features/projects/hooks"; import { - formatExactTimestamp, getProjectUpdatedAt, listRowDescription, - relativeTime, } from "@/features/projects/lib/projectsViewHelpers"; import type { ProjectRepoUnavailableReason } from "@/features/projects/lib/projectRepoAvailability"; import { projectShareLink } from "@/features/projects/lib/projectShareLinks"; @@ -55,39 +53,6 @@ import { ProjectEntityListRow } from "./ProjectEntityListRow"; import { PROJECT_GRID_CARD_BODY_CLASS } from "./projectGridCardStyles"; import { ProjectListRowMenu } from "./ProjectListRowMenu"; -function ProjectUpdatedLabel({ - profiles, - project, - summary, -}: { - profiles?: UserProfileLookup; - project: Project; - summary: ProjectActivitySummary | undefined; -}) { - const updatedAt = getProjectUpdatedAt(project, summary); - const latestCommit = summary?.latestCommit; - const authorLabel = latestCommit?.author - ? resolveUserLabel({ profiles, pubkey: latestCommit.author }) - : null; - - return ( - - - - {relativeTime(updatedAt)} - - - - {latestCommit - ? `${latestCommit.title || latestCommit.commit.slice(0, 7)}${ - authorLabel ? ` · ${authorLabel}` : "" - } · ${formatExactTimestamp(latestCommit.createdAt)}` - : `Created ${formatExactTimestamp(project.createdAt)}`} - - - ); -} - export function ProjectPeopleStack({ pubkeys, profiles, @@ -540,12 +505,7 @@ export function ProjectGridCard({
-
- +
{repositoryCount} } + affiliationClassName="w-auto" affiliationTestId="projects-row-context" affiliationTitle={`${repositoryCount} ${ repositoryCount === 1 ? "repository" : "repositories" diff --git a/desktop/src/features/projects/ui/ProjectEntityListRow.tsx b/desktop/src/features/projects/ui/ProjectEntityListRow.tsx index 71aaefadc..4f9d4e1c7 100644 --- a/desktop/src/features/projects/ui/ProjectEntityListRow.tsx +++ b/desktop/src/features/projects/ui/ProjectEntityListRow.tsx @@ -131,6 +131,7 @@ export function ProjectEntitySelectControl({ export function ProjectEntityListRow({ affiliation, + affiliationClassName, affiliationTestId, affiliationTitle, beforeDate, @@ -158,6 +159,7 @@ export function ProjectEntityListRow({ trailing, }: { affiliation?: React.ReactNode; + affiliationClassName?: string; affiliationTestId?: string; affiliationTitle?: string; beforeDate?: React.ReactNode; @@ -291,7 +293,10 @@ export function ProjectEntityListRow({ ) : null} {affiliation ? ( {latestCommit ? ( -
+
- +
@@ -952,7 +955,7 @@ export function RepositoryFilesPanel({ profiles={profiles} /> - + {latestCommit ? (
-
- {selectionAgentContext && !isNarrowProjectsLayout ? ( - setSelectionAgentContext(null)} - onResetWidth={overviewAgentPanelWidth.onResetWidth} - onResizeStart={overviewAgentPanelWidth.onResizeStart} - widthPx={overviewAgentPanelWidth.widthPx} - /> - ) : null}
+ {selectionAgentContext ? ( + setSelectionAgentContext(null)} + onResetWidth={overviewAgentPanelWidth.onResetWidth} + onResizeStart={overviewAgentPanelWidth.onResizeStart} + widthPx={overviewAgentPanelWidth.widthPx} + /> + ) : null} + + diff --git a/desktop/src/features/projects/ui/PullRequestReviewersRow.tsx b/desktop/src/features/projects/ui/PullRequestReviewersRow.tsx index da0e7003a..a79a705a5 100644 --- a/desktop/src/features/projects/ui/PullRequestReviewersRow.tsx +++ b/desktop/src/features/projects/ui/PullRequestReviewersRow.tsx @@ -289,58 +289,83 @@ export function PullRequestReviewersRow({ return ( -
-
+
+
{showSummary && displayedDecisionActors.length === 0 ? ( - + {reviewSummary} ) : null} + {displayedDecisionActors.map((pubkey, index) => { + const label = labelForPubkey(pubkey, profiles); + const hasApproved = approvedBy.has(pubkey); + const hasRequestedChanges = changesRequestedBy.has(pubkey); + const needsRereview = staleDecisionActors.has(pubkey); + const DecisionIcon = hasApproved + ? Check + : hasRequestedChanges + ? TriangleAlert + : needsRereview + ? History + : null; + const decisionLabel = hasApproved + ? `Approved by ${label}` + : hasRequestedChanges + ? `Changes requested by ${label}` + : needsRereview + ? `Re-review needed from ${label}` + : `Awaiting review from ${label}`; + return ( + + {index > 0 ? ( + + ) : null} + + {decisionLabel} + {DecisionIcon ? ( + + + ); + })} {hasHistoricalDecision ? ( - + Earlier decision applies to another commit ) : null} - {requestAction}
- {displayedDecisionActors.map((pubkey) => { - const label = labelForPubkey(pubkey, profiles); - const hasApproved = approvedBy.has(pubkey); - const hasRequestedChanges = changesRequestedBy.has(pubkey); - const needsRereview = staleDecisionActors.has(pubkey); - const DecisionIcon = hasApproved - ? Check - : hasRequestedChanges - ? TriangleAlert - : needsRereview - ? History - : null; - const decisionLabel = hasApproved - ? `Approved by ${label}` - : hasRequestedChanges - ? `Changes requested by ${label}` - : needsRereview - ? `Re-review needed from ${label}` - : `Awaiting review from ${label}`; - return ( - - {DecisionIcon ? ( - - ) : null} - {decisionLabel} - - ); - })} + {requestAction}
); diff --git a/desktop/src/features/projects/ui/RepositoryCards.tsx b/desktop/src/features/projects/ui/RepositoryCards.tsx index 75d878431..fcb41bd0f 100644 --- a/desktop/src/features/projects/ui/RepositoryCards.tsx +++ b/desktop/src/features/projects/ui/RepositoryCards.tsx @@ -18,10 +18,6 @@ import { selectionItemFromRepository, type ProjectSelectionItem, } from "@/features/projects/lib/projectSelection"; -import { - formatExactTimestamp, - relativeTime, -} from "@/features/projects/lib/projectsViewHelpers"; import { cn } from "@/shared/lib/cn"; import { normalizePubkey } from "@/shared/lib/pubkey"; import { useRelayOrigin } from "@/shared/lib/useRelayOrigin"; @@ -155,27 +151,6 @@ function RepositoryIdentity({ ); } -function RepositoryUpdatedLabel({ - repository, - summary, -}: Pick) { - const updatedAt = summary?.updatedAt || repository.createdAt; - return ( - - - - {relativeTime(updatedAt)} - - - - {summary?.latestCommit - ? `${summary.latestCommit.title || summary.latestCommit.commit.slice(0, 7)} · ${formatExactTimestamp(summary.latestCommit.createdAt)}` - : `Created ${formatExactTimestamp(repository.createdAt)}`} - - - ); -} - function repositoryPeople( repository: Repository, summary: ProjectActivitySummary | undefined, @@ -261,13 +236,12 @@ export function RepositoryGridCard(props: RepositoryItemProps) { > {repository.description || "A repository in this project."}

-
+
-
diff --git a/desktop/src/features/sidebar/ui/SidebarProjectsSection.tsx b/desktop/src/features/sidebar/ui/SidebarProjectsSection.tsx index 553cbe302..a61962586 100644 --- a/desktop/src/features/sidebar/ui/SidebarProjectsSection.tsx +++ b/desktop/src/features/sidebar/ui/SidebarProjectsSection.tsx @@ -589,8 +589,10 @@ function SidebarProjectRow({ tooltip={project.name} type="button" > - - {project.name} + + + {project.name} + {canDelete ? ( window.__BUZZ_E2E_SIGNED_EVENTS__ @@ -380,9 +389,9 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({ ).toHaveCount(0); await expect(page.getByTestId("project-review-summary")).toHaveCount(0); await expect( - page - .getByTestId("project-reviewer-decision") - .filter({ hasText: "Approved by" }), + page.locator( + '[data-testid="project-reviewer-decision"][title^="Approved by "]', + ), ).toHaveCount(1); const approvalEvent = await page.evaluate(() => window.__BUZZ_E2E_SIGNED_EVENTS__ @@ -1104,10 +1113,19 @@ test("sidebar distinguishes the Projects overview from an open project", async ( await projectsOverview.click(); await expect(projectsOverview).toHaveAttribute("data-active", "true"); await expect(sidebarProject).toHaveAttribute("data-active", "false"); + await expect(sidebarProject.locator("svg").first()).toHaveCSS( + "opacity", + "0.8", + ); + await expect(sidebarProject.locator('[data-sidebar="menu-label"]')).toHaveCSS( + "opacity", + "0.8", + ); await sidebarProject.click(); - await expect(projectsOverview).toHaveAttribute("data-active", "false"); - await expect(sidebarProject).toHaveAttribute("data-active", "true"); + await expect(projectsOverview).toHaveAttribute("data-active", "true"); + await expect(sidebarProject).toHaveAttribute("data-active", "false"); + await expect(sidebarProject).toHaveAttribute("aria-expanded", "false"); }); test("collapsed sidebar leaves a balanced Projects surface gutter", async ({ @@ -1596,7 +1614,9 @@ test("project overview presents collapsible context beside grouped activity", as ).toHaveCount(0); }); -test("project overview content header toggles agent chat", async ({ page }) => { +test("project overview chrome toggles a detached resizable agent chat", async ({ + page, +}) => { await enableProjectsFeature(page); await installMockBridge(page); await page.goto("/", { waitUntil: "domcontentloaded" }); @@ -1613,25 +1633,36 @@ test("project overview content header toggles agent chat", async ({ page }) => { "Chat with an agent about Reviews", ); await expect(overviewChat).toHaveAttribute("aria-pressed", "false"); - const [contentBox, chatBox] = await Promise.all([ - page.getByTestId("projects-overview-content-pod").boundingBox(), + const overviewInfo = page.getByTestId("projects-overview-context-toggle"); + const [chatBox, infoBox] = await Promise.all([ overviewChat.boundingBox(), + overviewInfo.boundingBox(), ]); - expect(contentBox).not.toBeNull(); expect(chatBox).not.toBeNull(); + expect(infoBox).not.toBeNull(); expect( - (contentBox?.x ?? 0) + - (contentBox?.width ?? 0) - - ((chatBox?.x ?? 0) + (chatBox?.width ?? 0)), - ).toBeLessThanOrEqual(20); + (infoBox?.x ?? 0) - ((chatBox?.x ?? 0) + (chatBox?.width ?? 0)), + ).toBeLessThanOrEqual(4); + const contextRail = page.getByTestId("projects-overview-context-rail"); + await expect(overviewInfo).toHaveAttribute("aria-pressed", "true"); + await overviewInfo.click(); + await expect(overviewInfo).toHaveAttribute("aria-pressed", "false"); + await expect(contextRail).toHaveCSS("width", "0px"); await overviewChat.click(); await expect(overviewChat).toHaveAttribute("aria-pressed", "true"); + await expect(overviewInfo).toHaveAttribute("aria-pressed", "false"); + await expect(contextRail).toHaveCSS("width", "0px"); await expect(overviewChat).toBeVisible(); + const chatRail = page.getByTestId("projects-overview-agent-rail"); + await expect(chatRail.getByTestId("project-agent-chat-panel")).toBeVisible(); await expect( page .getByTestId("projects-overview-content-pod") .getByTestId("project-agent-chat-panel"), - ).toBeVisible(); + ).toHaveCount(0); + await expect( + chatRail.getByTestId("projects-overview-agent-rail-panel"), + ).toHaveCSS("border-radius", "16px"); const agentHeader = page.getByTestId("project-agent-context"); await expect .poll(() => @@ -1640,11 +1671,46 @@ test("project overview content header toggles agent chat", async ({ page }) => { ), ) .not.toBe("none"); - await expect(page.getByTestId("projects-overview-agent-rail")).toHaveCount(0); + await expect(contextRail).toHaveAttribute("aria-hidden", "true"); + await overviewInfo.click(); + await expect(overviewInfo).toHaveAttribute("aria-pressed", "true"); + await expect(contextRail).toHaveAttribute("aria-hidden", "false"); await expect( page.getByTestId("projects-overview-context-panel"), ).toBeVisible(); const chatPanel = page.getByTestId("project-agent-chat-panel"); + const resizeHandle = chatPanel.getByTestId( + "right-auxiliary-pane-resize-handle", + ); + const [initialPanelBox, resizeHandleBox] = await Promise.all([ + chatPanel.boundingBox(), + resizeHandle.boundingBox(), + ]); + expect(initialPanelBox).not.toBeNull(); + expect(resizeHandleBox).not.toBeNull(); + const resizeStartX = + (resizeHandleBox?.x ?? 0) + (resizeHandleBox?.width ?? 0) / 2; + const resizeStartY = + (resizeHandleBox?.y ?? 0) + (resizeHandleBox?.height ?? 0) / 2; + await resizeHandle.dispatchEvent("pointerdown", { + button: 0, + buttons: 1, + clientX: resizeStartX, + clientY: resizeStartY, + pointerId: 1, + pointerType: "mouse", + }); + await expect(chatRail).toHaveAttribute("data-resizing", "true"); + await page.mouse.move(resizeStartX - 32, resizeStartY); + await expect + .poll(() => + chatPanel.evaluate((element) => + Math.round(element.getBoundingClientRect().width), + ), + ) + .toBe(Math.round(initialPanelBox?.width ?? 0) + 32); + await page.mouse.up(); + await expect(chatRail).toHaveAttribute("data-resizing", "false"); await chatPanel.getByTestId("message-input").fill("Summarize these reviews"); await chatPanel.getByTestId("message-input").press("Enter"); const readSentContent = () => @@ -1674,6 +1740,7 @@ test("project overview content header toggles agent chat", async ({ page }) => { await overviewChat.click(); await expect(overviewChat).toHaveAttribute("aria-pressed", "false"); await expect(page.getByTestId("project-agent-chat-panel")).not.toBeVisible(); + await expect(chatRail).toHaveCSS("width", "0px"); await expect( page.getByTestId("projects-overview-context-panel"), ).toBeVisible(); diff --git a/desktop/tests/e2e/projects-v3-screenshots.spec.ts b/desktop/tests/e2e/projects-v3-screenshots.spec.ts index 5eddc2f0a..3d3c902e1 100644 --- a/desktop/tests/e2e/projects-v3-screenshots.spec.ts +++ b/desktop/tests/e2e/projects-v3-screenshots.spec.ts @@ -1105,9 +1105,24 @@ test("projects v3 work-item list metadata", async ({ page }) => { ); await page.getByTestId("projects-section-projects").click(); - await expectSinglePrimaryTextColumn( - page.getByTestId(/^project-row-/).first(), + await expect(page.getByTestId("projects-list-header")).toHaveCSS( + "border-left-width", + "0px", ); + const projectRow = page.getByTestId(/^project-row-/).first(); + await expectSinglePrimaryTextColumn(projectRow); + const [projectTitleBox, repositoryCountBox] = await Promise.all([ + projectRow.locator('[data-projects-text-priority="primary"]').boundingBox(), + projectRow.getByTestId("projects-row-context").boundingBox(), + ]); + expect(projectTitleBox).not.toBeNull(); + expect(repositoryCountBox).not.toBeNull(); + expect( + Math.round( + (repositoryCountBox?.x ?? 0) - + ((projectTitleBox?.x ?? 0) + (projectTitleBox?.width ?? 0)), + ), + ).toBe(12); await page.getByTestId("projects-section-repositories").click(); await expectSinglePrimaryTextColumn(