fix(desktop): refine Projects overview and list presentation

Separate overview context and agent rails from the content pod, simplify project and repository rows, and align the surrounding navigation chrome and metadata.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
This commit is contained in:
Thomas Petersen
2026-08-17 22:07:36 -04:00
parent d1d54c93b2
commit 04ca3eb2cf
11 changed files with 311 additions and 173 deletions

View File

@@ -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 (
<Tooltip>
<TooltipTrigger asChild>
<span className="whitespace-nowrap text-xs leading-4 text-muted-foreground/70">
{relativeTime(updatedAt)}
</span>
</TooltipTrigger>
<TooltipContent className="max-w-96 break-words">
{latestCommit
? `${latestCommit.title || latestCommit.commit.slice(0, 7)}${
authorLabel ? ` · ${authorLabel}` : ""
} · ${formatExactTimestamp(latestCommit.createdAt)}`
: `Created ${formatExactTimestamp(project.createdAt)}`}
</TooltipContent>
</Tooltip>
);
}
export function ProjectPeopleStack({
pubkeys,
profiles,
@@ -540,12 +505,7 @@ export function ProjectGridCard({
</div>
</div>
</div>
<div className="pointer-events-auto relative z-10 flex shrink-0 items-center gap-1">
<ProjectUpdatedLabel
profiles={profiles}
project={project}
summary={summary}
/>
<div className="pointer-events-auto relative z-10 shrink-0">
<ProjectActionsMenu
canDelete={canDelete}
disabled={deleteDisabled}
@@ -613,6 +573,7 @@ export function ProjectListRow({
<span>{repositoryCount}</span>
</span>
}
affiliationClassName="w-auto"
affiliationTestId="projects-row-context"
affiliationTitle={`${repositoryCount} ${
repositoryCount === 1 ? "repository" : "repositories"

View File

@@ -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;
@@ -292,7 +294,10 @@ export function ProjectEntityListRow({
) : null}
{affiliation ? (
<span
className="hidden w-36 shrink-0 truncate text-right text-xs text-muted-foreground/65 md:block"
className={cn(
"hidden w-36 shrink-0 truncate text-right text-xs text-muted-foreground/65 md:block",
affiliationClassName,
)}
data-projects-text-priority="secondary"
data-testid={affiliationTestId}
title={

View File

@@ -884,7 +884,10 @@ export function RepositoryFilesPanel({
>
<th className="px-4 py-3 text-left font-normal" colSpan={3}>
{latestCommit ? (
<div className="flex min-w-0 items-center justify-between gap-3 text-sm">
<div
className="flex min-w-0 items-center justify-between gap-3 text-xs"
data-testid="project-repository-latest-commit-summary"
>
<div className="flex min-w-0 items-center gap-2">
<UserAvatar
accent={latestCommitProfile?.isAgent === true}
@@ -950,7 +953,7 @@ export function RepositoryFilesPanel({
}
tabIndex={0}
>
<td className="min-w-52 px-3 py-2 align-middle transition-colors group-hover/repository-entry:bg-muted/35 group-focus-visible/repository-entry:bg-muted/35">
<td className="min-w-52 px-3 py-2 align-middle transition-colors group-hover/repository-entry:rounded-l-md group-hover/repository-entry:bg-muted/35 group-focus-visible/repository-entry:rounded-l-md group-focus-visible/repository-entry:bg-muted/35">
<div className="flex min-w-0 items-center gap-2">
<RepositoryEntryIcon entry={entry} />
<span className="truncate font-medium text-foreground">
@@ -964,7 +967,7 @@ export function RepositoryFilesPanel({
profiles={profiles}
/>
</td>
<td className="w-36 whitespace-nowrap p-2 text-right align-middle text-muted-foreground transition-colors group-hover/repository-entry:bg-muted/35 group-focus-visible/repository-entry:bg-muted/35">
<td className="w-36 whitespace-nowrap p-2 text-right align-middle text-muted-foreground transition-colors group-hover/repository-entry:rounded-r-md group-hover/repository-entry:bg-muted/35 group-focus-visible/repository-entry:rounded-r-md group-focus-visible/repository-entry:bg-muted/35">
{latestCommit ? (
<time
dateTime={new Date(

View File

@@ -118,9 +118,7 @@ export function ProjectsListHeaderBar({
<div
className={cn(
"flex flex-wrap items-center justify-between gap-2 bg-muted/20 px-3 py-1.5",
variant === "bar"
? "rounded-xl border border-border/60"
: "border-b border-border/60",
variant === "bar" ? "rounded-xl" : "border-b border-border/60",
)}
data-testid="projects-list-header"
>

View File

@@ -0,0 +1,62 @@
import { Info, MessageCircle } from "lucide-react";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
export function ProjectsOverviewChromeActions({
chatOpen,
contextOpen,
onToggleChat,
onToggleContext,
sectionTitle,
}: {
chatOpen: boolean;
contextOpen: boolean;
onToggleChat: () => void;
onToggleContext: () => void;
sectionTitle: string;
}) {
return (
<>
<Button
aria-label={`Chat with an agent about ${sectionTitle}`}
aria-pressed={chatOpen}
className="h-7 w-7 text-sidebar-foreground hover:bg-sidebar-accent"
data-testid="projects-overview-chat-toggle"
onClick={onToggleChat}
size="icon"
title={`Chat with an agent about ${sectionTitle}`}
type="button"
variant="ghost"
>
<MessageCircle
className={cn(
"h-4 w-4 transition-opacity duration-200 ease-linear",
chatOpen ? "opacity-100" : "opacity-60",
)}
/>
</Button>
<Button
aria-label={
contextOpen ? "Hide project context" : "Show project context"
}
aria-pressed={contextOpen}
className="h-7 w-7 text-sidebar-foreground hover:bg-sidebar-accent"
data-testid="projects-overview-context-toggle"
onClick={onToggleContext}
size="icon"
title={contextOpen ? "Hide project context" : "Show project context"}
type="button"
variant="ghost"
>
<Info
className={cn(
"h-4 w-4 transition-opacity duration-200 ease-linear",
contextOpen ? "opacity-100" : "opacity-60",
)}
data-testid="projects-overview-context-icon"
/>
</Button>
</>
);
}

View File

@@ -31,7 +31,6 @@ import {
} from "@/features/projects/lib/projectRepoHost";
import { ProjectsActivityFeed } from "@/features/projects/ui/ProjectsActivityFeed";
import { ProjectsChannelsList } from "@/features/projects/ui/ProjectsChannelsList";
import { ProjectsOverviewChatToggle } from "@/features/projects/ui/ProjectsOverviewChatToggle";
import {
ProjectsOverviewContextSheet,
ProjectsOverviewNarrowContextToggle,
@@ -41,6 +40,7 @@ import {
ProjectsOverviewContextPanel,
ProjectsOverviewPanel,
} from "@/features/projects/ui/ProjectsOverviewPanel";
import { ProjectsOverviewChromeActions } from "@/features/projects/ui/ProjectsOverviewChromeActions";
import { ProjectContextRail } from "@/features/projects/ui/ProjectContextRail";
import {
openAppSearch,
@@ -705,22 +705,31 @@ export function ProjectsView() {
) ?? [],
summaries: activitySummariesQuery.data,
};
const overviewDetached = overviewPanelOpen && !isNarrowProjectsLayout;
const contextOpen = isNarrowProjectsLayout
? narrowContextOpen
: overviewPanelOpen;
const chromeActions = (
<>
<ProjectsOverviewNarrowContextToggle
onToggle={() =>
isNarrowProjectsLayout
? setNarrowContextOpen((open) => !open)
: setOverviewPanelOpen((open) => !open)
}
open={contextOpen}
ref={contextToggleRef}
/>
</>
const overviewChatOpen =
selectionAgentContext !== null && !isNarrowProjectsLayout;
const overviewContextOpen = overviewPanelOpen && !isNarrowProjectsLayout;
const overviewDetached = overviewContextOpen || overviewChatOpen;
const chromeActions = isNarrowProjectsLayout ? (
<ProjectsOverviewNarrowContextToggle
onToggle={() => setNarrowContextOpen((open) => !open)}
open={contextOpen}
ref={contextToggleRef}
/>
) : (
<ProjectsOverviewChromeActions
chatOpen={overviewChatOpen}
contextOpen={overviewPanelOpen}
onToggleChat={() =>
setSelectionAgentContext((context) =>
context ? null : overviewAgentContext,
)
}
onToggleContext={() => setOverviewPanelOpen((open) => !open)}
sectionTitle={projectsSectionTitle(filter)}
/>
);
return (
@@ -842,15 +851,6 @@ export function ProjectsView() {
onFilterChange={handleFilterChange}
/>
</div>
<ProjectsOverviewChatToggle
active={selectionAgentContext !== null}
onToggle={() =>
setSelectionAgentContext((context) =>
context ? null : overviewAgentContext,
)
}
sectionTitle={projectsSectionTitle(filter)}
/>
</div>
<div
className={
@@ -933,20 +933,29 @@ export function ProjectsView() {
</div>
</div>
</div>
{selectionAgentContext && !isNarrowProjectsLayout ? (
<ProjectAgentChatPanel
canResetWidth={overviewAgentPanelWidth.canReset}
context={selectionAgentContext}
onClose={() => setSelectionAgentContext(null)}
onResetWidth={overviewAgentPanelWidth.onResetWidth}
onResizeStart={overviewAgentPanelWidth.onResizeStart}
widthPx={overviewAgentPanelWidth.widthPx}
/>
) : null}
</div>
</div>
<ProjectContextRail
open={overviewDetached}
open={overviewChatOpen}
panelWidthPx={overviewAgentPanelWidth.widthPx}
resizing={overviewAgentPanelWidth.isResizing}
testId="projects-overview-agent-rail"
>
{selectionAgentContext ? (
<ProjectAgentChatPanel
canResetWidth={overviewAgentPanelWidth.canReset}
constrainToAvailableSpace={false}
context={selectionAgentContext}
detached
onClose={() => setSelectionAgentContext(null)}
onResetWidth={overviewAgentPanelWidth.onResetWidth}
onResizeStart={overviewAgentPanelWidth.onResizeStart}
widthPx={overviewAgentPanelWidth.widthPx}
/>
) : null}
</ProjectContextRail>
<ProjectContextRail
open={overviewContextOpen}
panelWidthPx={PROJECT_CONTEXT_PANEL_DEFAULT_WIDTH_PX}
testId="projects-overview-context-rail"
>

View File

@@ -289,58 +289,83 @@ export function PullRequestReviewersRow({
return (
<ProjectDetailMetaRow icon={Users} label="Reviewers">
<div className="grid min-w-0 gap-1">
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
<div
className="flex min-w-0 items-center gap-2"
data-testid="project-reviewers-content"
>
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden whitespace-nowrap">
{showSummary && displayedDecisionActors.length === 0 ? (
<span className="font-medium" data-testid={summaryTestId}>
<span className="truncate font-medium" data-testid={summaryTestId}>
{reviewSummary}
</span>
) : 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 (
<React.Fragment key={pubkey}>
{index > 0 ? (
<span
aria-hidden="true"
className="shrink-0 text-muted-foreground/50"
>
·
</span>
) : null}
<span
className={cn(
"flex min-w-0 shrink items-center gap-1 text-sm",
hasApproved && "text-green-600 dark:text-green-400",
hasRequestedChanges && "text-amber-600 dark:text-amber-400",
!hasApproved &&
!hasRequestedChanges &&
"text-muted-foreground",
)}
data-testid="project-reviewer-decision"
title={decisionLabel}
>
<span className="sr-only">{decisionLabel}</span>
{DecisionIcon ? (
<DecisionIcon
aria-hidden="true"
className="h-3.5 w-3.5 shrink-0"
/>
) : null}
<span
aria-hidden="true"
className="truncate"
data-testid="project-reviewer-name"
>
{label}
</span>
</span>
</React.Fragment>
);
})}
{hasHistoricalDecision ? (
<span className="flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400">
<span className="flex min-w-0 items-center gap-1 truncate text-xs text-amber-600 dark:text-amber-400">
<History className="h-3.5 w-3.5 shrink-0" />
Earlier decision applies to another commit
</span>
) : null}
{requestAction}
</div>
{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 (
<span
className={cn(
"flex min-w-0 items-center gap-1 text-sm",
hasApproved && "text-green-600 dark:text-green-400",
hasRequestedChanges && "text-amber-600 dark:text-amber-400",
!hasApproved && !hasRequestedChanges && "text-muted-foreground",
)}
data-testid="project-reviewer-decision"
key={pubkey}
>
{DecisionIcon ? (
<DecisionIcon className="h-3.5 w-3.5 shrink-0" />
) : null}
<span className="truncate">{decisionLabel}</span>
</span>
);
})}
{requestAction}
</div>
</ProjectDetailMetaRow>
);

View File

@@ -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<RepositoryItemProps, "repository" | "summary">) {
const updatedAt = summary?.updatedAt || repository.createdAt;
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="whitespace-nowrap text-xs leading-4 text-muted-foreground/70">
{relativeTime(updatedAt)}
</span>
</TooltipTrigger>
<TooltipContent>
{summary?.latestCommit
? `${summary.latestCommit.title || summary.latestCommit.commit.slice(0, 7)} · ${formatExactTimestamp(summary.latestCommit.createdAt)}`
: `Created ${formatExactTimestamp(repository.createdAt)}`}
</TooltipContent>
</Tooltip>
);
}
function repositoryPeople(
repository: Repository,
summary: ProjectActivitySummary | undefined,
@@ -261,13 +236,12 @@ export function RepositoryGridCard(props: RepositoryItemProps) {
>
{repository.description || "A repository in this project."}
</p>
<div className="pointer-events-auto mt-auto flex items-center justify-between gap-3">
<div className="pointer-events-auto mt-auto">
<ProjectPeopleStack
profiles={profiles}
pubkeys={repositoryPeople(repository, summary)}
workOwnerPubkey={repository.owner}
/>
<RepositoryUpdatedLabel repository={repository} summary={summary} />
</div>
<div className="mt-2">
<ProjectStatsRow summary={summary} />

View File

@@ -605,6 +605,26 @@ test("latest files commit opens its detail without a divider", async ({
const latestCommit = page.getByTestId("project-repository-latest-commit");
await expect(latestCommit).toBeVisible();
await expect(latestCommit).toHaveCSS("border-bottom-width", "0px");
await expect(
page.getByTestId("project-repository-latest-commit-summary"),
).toHaveCSS("font-size", "12px");
await expect(
page.getByTestId("project-repository-entry-row").first(),
).toHaveCSS("font-size", "12px");
const repositoryEntryRow = page
.getByTestId("project-repository-entry-row")
.first();
const repositoryEntryCells = repositoryEntryRow.locator("td");
await expect(repositoryEntryCells.first()).toHaveCSS("border-radius", "0px");
await repositoryEntryRow.hover();
await expect(repositoryEntryCells.first()).toHaveCSS(
"border-top-left-radius",
"8px",
);
await expect(repositoryEntryCells.last()).toHaveCSS(
"border-top-right-radius",
"8px",
);
await latestCommit.click();
await expect(page.getByTestId("project-commit-detail")).toBeVisible();
});

View File

@@ -266,11 +266,20 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({
timeout: 10_000,
});
await expect(page.getByTestId("project-review-summary")).toHaveCount(0);
const reviewerDecision = page
.getByTestId("project-reviewer-decision")
.filter({ hasText: "bob" });
await expect(
page
.getByTestId("project-reviewer-decision")
.filter({ hasText: "Awaiting review from bob" }),
).toHaveCount(1);
reviewerDecision.getByTestId("project-reviewer-name"),
).toHaveText("bob");
await expect(reviewerDecision).toHaveAttribute(
"title",
"Awaiting review from bob",
);
await expect(page.getByTestId("project-reviewers-content")).toHaveCSS(
"flex-wrap",
"nowrap",
);
await waitForAnimations(page);
await page.screenshot({
@@ -296,9 +305,9 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({
).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("project-review-summary")).toHaveCount(0);
await expect(
page
.getByTestId("project-reviewer-decision")
.filter({ hasText: "Changes requested" }),
page.locator(
'[data-testid="project-reviewer-decision"][title^="Changes requested by "]',
),
).toHaveCount(1);
const changeRequestEvent = await page.evaluate(() =>
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__
@@ -1108,6 +1117,14 @@ 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", "true");
@@ -1605,7 +1622,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" });
@@ -1622,25 +1641,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(() =>
@@ -1649,11 +1679,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 = () =>
@@ -1683,6 +1748,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();

View File

@@ -1122,9 +1122,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(