feat(desktop): refine repository project navigation

Make project repository states, branch actions, loading feedback, split-pane chrome, and persistent sidebar expansion behave consistently across the workspace.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
This commit is contained in:
Thomas Petersen
2026-08-15 11:33:18 -04:00
parent 6ae17a454a
commit 65e24bafe1
32 changed files with 645 additions and 137 deletions
+7
View File
@@ -101,6 +101,13 @@ export function AppTopChrome({
)}
data-tauri-drag-region
data-testid="app-top-chrome"
style={
{
"--app-top-chrome-center-offset": hasCommunityRail
? "-1.75rem"
: "0rem",
} as React.CSSProperties
}
>
<div className={cn("flex items-center gap-0.5", navRowAlignmentClass)}>
<TopChromeSidebarTrigger />
@@ -0,0 +1,33 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { projectExternalRefUrl } from "./projectExternalUrl.ts";
test("opens the selected GitHub branch", () => {
assert.equal(
projectExternalRefUrl(
"https://github.com/block/buzz",
"fix/agent-profile-about-preserve",
),
"https://github.com/block/buzz/tree/fix%2Fagent-profile-about-preserve",
);
});
test("normalizes clone URLs before adding the selected ref", () => {
assert.equal(
projectExternalRefUrl("https://github.com/block/buzz.git/", "main"),
"https://github.com/block/buzz/tree/main",
);
});
test("keeps unsupported and unscoped URLs unchanged", () => {
assert.equal(
projectExternalRefUrl("https://gitlab.com/block/buzz", "main"),
"https://gitlab.com/block/buzz",
);
assert.equal(
projectExternalRefUrl("https://github.com/block/buzz", null),
"https://github.com/block/buzz",
);
assert.equal(projectExternalRefUrl("not a URL", "main"), "not a URL");
});
@@ -0,0 +1,26 @@
/** Builds a GitHub repository URL scoped to the selected branch or tag. */
export function projectExternalRefUrl(
externalUrl: string | null | undefined,
ref: string | null | undefined,
): string | null {
if (!externalUrl) return null;
const selectedRef = ref?.trim();
if (!selectedRef) return externalUrl;
try {
const url = new URL(externalUrl);
if (
url.protocol !== "https:" ||
url.hostname.toLowerCase() !== "github.com"
) {
return externalUrl;
}
const segments = url.pathname.split("/").filter(Boolean);
if (segments.length !== 2) return externalUrl;
const repository = segments[1]?.replace(/\.git$/i, "");
if (!segments[0] || !repository) return externalUrl;
return `${url.origin}/${segments[0]}/${repository}/tree/${encodeURIComponent(selectedRef)}`;
} catch {
return externalUrl;
}
}
@@ -34,6 +34,7 @@ import {
getMentionTagPubkey,
resolveMentionProps,
} from "@/shared/lib/resolveMentionNames";
import { BuzzLoadingState } from "@/shared/ui/BuzzLoadingState";
import { Markdown } from "@/shared/ui/markdown";
import { UserAvatar } from "@/shared/ui/UserAvatar";
import { useProjectConversationPanel } from "./ProjectConversationPanelContext";
@@ -491,11 +492,7 @@ export function DiscussionChannelsPanel({
const profiles = profilesQuery.data?.profiles;
if (isLoading) {
return (
<p className="px-4 py-6 text-sm text-muted-foreground">
Searching channel discussions
</p>
);
return <BuzzLoadingState label="Loading channel discussions" />;
}
if (channels.length === 0) {
return (
@@ -40,6 +40,7 @@ export function ProjectConversationPanel({
onClose,
onResetWidth,
onResizeStart,
sharedHeaderBackdrop,
widthPx,
}: {
canResetWidth: boolean;
@@ -47,6 +48,7 @@ export function ProjectConversationPanel({
onClose: () => void;
onResetWidth: () => void;
onResizeStart: (event: React.PointerEvent<HTMLButtonElement>) => void;
sharedHeaderBackdrop?: boolean;
widthPx: number;
}) {
const { goChannel } = useAppNavigation();
@@ -204,7 +206,7 @@ export function ProjectConversationPanel({
showBackButton: false,
splitPaneClamp: false,
testId: isOverlay ? "project-conversation-panel" : "message-thread-panel",
transparentChrome: false,
transparentChrome: sharedHeaderBackdrop,
};
const handleSend = React.useCallback(
@@ -86,6 +86,7 @@ export function ProjectConversationPanelController({
onClose={() => setHit(null)}
onResetWidth={onResetWidth}
onResizeStart={onResizeStart}
sharedHeaderBackdrop={sharedHeaderBackdrop}
widthPx={widthPx}
/>
) : (
@@ -42,7 +42,10 @@ export function ProjectDetailChrome({
>
<nav
aria-label="Project breadcrumb"
className="absolute left-1/2 flex max-w-[50%] min-w-0 -translate-x-1/2 -translate-y-px items-center gap-0.5 text-xs text-sidebar-foreground/65"
className="absolute flex max-w-[50%] min-w-0 -translate-x-1/2 -translate-y-px items-center gap-0.5 text-xs text-sidebar-foreground/65"
style={{
left: "calc(50% + var(--app-top-chrome-center-offset, 0rem))",
}}
>
<button
className="flex shrink-0 items-center gap-1.5 rounded-md px-1 py-1 font-medium transition-colors hover:text-sidebar-accent-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
@@ -14,6 +14,7 @@ import type {
import { relativeTime } from "@/features/projects/lib/projectsViewHelpers";
import type { ProjectRepoCommit } from "@/shared/api/types";
import { truncatePubkey } from "@/shared/lib/pubkey";
import { BuzzLoadingState } from "@/shared/ui/BuzzLoadingState";
import {
resolveUserLabel,
type UserProfileLookup,
@@ -241,14 +242,7 @@ export function ActivityPanel({
);
if (isLoading) {
return (
<p
className={PROJECT_DETAIL_PANEL_MESSAGE_CLASS}
data-project-detail-panel
>
Loading activity
</p>
);
return <BuzzLoadingState label="Loading activity" />;
}
if (commits.length === 0) {
@@ -479,19 +479,18 @@ export function ProjectDetailScreen(props: ProjectDetailScreenProps) {
}
}, [projectPending, repository]);
React.useEffect(() => {
setRepoSource((currentSource) => {
if (selectedTag) return "remote";
if (currentSource === "local" && !hasLocalCheckout) return "remote";
if (
currentSource === "remote" &&
!hasRemoteSnapshot &&
hasLocalCheckout
) {
return "local";
}
return currentSource;
});
}, [hasLocalCheckout, hasRemoteSnapshot, selectedTag]);
if (selectedTag) {
if (repoSource !== "remote") setRepoSource("remote");
return;
}
if (repoSource === "local" && !hasLocalCheckout) {
setRepoSource("remote");
return;
}
if (repoSource === "remote" && !hasRemoteSnapshot && hasLocalCheckout) {
setRepoSource("local");
}
}, [hasLocalCheckout, hasRemoteSnapshot, repoSource, selectedTag]);
const {
contributorActivityCounts,
contributorPubkeys,
@@ -809,12 +808,7 @@ export function ProjectDetailScreen(props: ProjectDetailScreenProps) {
/>
);
const sharedHeaderBackdrop =
repositoryPanel.mode === "chat" &&
!repositoryPanel.collapsed &&
!profilePanelPubkey &&
!selectedPullRequestId &&
!selectedIssueId &&
!selectedCommitHash;
!selectedPullRequestId && !selectedIssueId && !selectedCommitHash;
const handleRepositoryChange = (nextRepositoryId: string) => {
applyRepositorySearch({
repositoryId: nextRepositoryId,
@@ -985,6 +979,7 @@ export function ProjectDetailScreen(props: ProjectDetailScreenProps) {
onViewChange={handleProfilePanelViewChange}
pubkey={profilePanelPubkey}
tab={profilePanelTab}
transparentChrome={sharedHeaderBackdrop}
view={profilePanelView}
widthPx={threadPanelWidth.widthPx}
/>
@@ -33,6 +33,7 @@ import {
} from "@/features/projects/projectTaskCategories";
import { useIdentityQuery } from "@/shared/api/hooks";
import type { ChannelMember } from "@/shared/api/types";
import { BuzzLoadingState } from "@/shared/ui/BuzzLoadingState";
import { normalizePubkey } from "@/shared/lib/pubkey";
import { IssueAssigneeFacepile, IssueAssigneesRow } from "./IssueAssigneesRow";
import { DiscussedInChannels } from "./DiscussionChannels";
@@ -416,7 +417,7 @@ export function ProjectIssuesPanel({
issues.find((issue) => issue.id === selectedIssueId) ?? null;
if (issuesQuery.isLoading) {
return <p className="p-4 text-sm text-muted-foreground">Loading tasks</p>;
return <BuzzLoadingState label="Loading tasks" />;
}
if (issues.length === 0) {
@@ -39,6 +39,7 @@ import type { UserProfileLookup } from "@/features/profile/lib/identity";
import { useIdentityQuery } from "@/shared/api/hooks";
import { cn } from "@/shared/lib/cn";
import type { ProjectRepoDiff, ProjectRepoDiffFile } from "@/shared/api/types";
import { BuzzLoadingState } from "@/shared/ui/BuzzLoadingState";
import { PROJECT_DETAIL_PANEL_CLASS } from "./projectPanelStyles";
import { ProjectPullRequestInlineCommentThread } from "./ProjectPullRequestInlineComments";
@@ -814,14 +815,7 @@ export function ProjectDiffFilesPanel({
}, [filteredFiles, selectedPath]);
if (isLoading) {
return (
<div
className={cn("p-4 text-sm text-muted-foreground", outerBorderClass)}
data-project-detail-panel={embedded ? undefined : true}
>
Loading changed files
</div>
);
return <BuzzLoadingState label="Loading changed files" />;
}
if (error) {
@@ -33,6 +33,7 @@ import type { UserProfileLookup } from "@/features/profile/lib/identity";
import { useIdentityQuery } from "@/shared/api/hooks";
import type { ChannelMember } from "@/shared/api/types";
import { normalizePubkey, truncatePubkey } from "@/shared/lib/pubkey";
import { BuzzLoadingState } from "@/shared/ui/BuzzLoadingState";
import {
ProjectFeedRow,
ProjectFeedRowCluster,
@@ -874,9 +875,7 @@ export function PullRequestsPanel({
}, [onSelectedPullRequestIdChange, pullRequests, selectedPullRequestId]);
if (isLoading) {
return (
<p className="p-4 text-sm text-muted-foreground">Loading reviews</p>
);
return <BuzzLoadingState label="Loading reviews" />;
}
if (pullRequests.length === 0) {
@@ -7,8 +7,10 @@ import {
} from "lucide-react";
import type { ProjectRepoFile } from "@/features/projects/hooks";
import { projectExternalRefUrl } from "@/features/projects/lib/projectExternalUrl";
import type { ProjectRepoUnavailableReason } from "@/features/projects/lib/projectRepoAvailability";
import { Button } from "@/shared/ui/button";
import { BuzzLoadingState } from "@/shared/ui/BuzzLoadingState";
import { Markdown, SyntaxHighlightedCode } from "@/shared/ui/markdown";
import {
baseName,
@@ -117,6 +119,10 @@ export function ReadmePanel({
/** Branch picker + remote/local toggle rendered in the panel header. */
sourceControls?: RepoSourceHeaderControls;
}) {
const externalOpenUrl = projectExternalRefUrl(
externalUrl,
sourceControls?.selectedTag ?? sourceControls?.branch,
);
// Two header rows, mirroring the files panel: controls on top, then the
// file identity row.
const header = hideHeader ? null : (
@@ -161,10 +167,7 @@ export function ReadmePanel({
return (
<section className="overflow-hidden">
{header}
<div className="flex items-center gap-2 p-6 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
Loading repository
</div>
<BuzzLoadingState label="Loading repository" />
</section>
);
}
@@ -204,14 +207,14 @@ export function ReadmePanel({
Clone this repository locally to explore its files, commits, and
contributors in Buzz.
</p>
{externalUrl ? (
{externalOpenUrl ? (
<a
className="mt-2 max-w-lg truncate font-mono text-xs text-primary hover:underline focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
href={externalUrl}
href={externalOpenUrl}
rel="noreferrer"
target="_blank"
>
{externalUrl}
{externalOpenUrl}
</a>
) : null}
<div className="mt-4 flex flex-wrap items-center justify-center gap-2">
@@ -229,9 +232,9 @@ export function ReadmePanel({
{sourceControls.clonePending ? "Cloning…" : "Clone locally"}
</Button>
) : null}
{externalUrl ? (
{externalOpenUrl ? (
<Button asChild size="sm" variant="outline">
<a href={externalUrl} rel="noreferrer" target="_blank">
<a href={externalOpenUrl} rel="noreferrer" target="_blank">
<ExternalLink className="h-4 w-4" />
Open on {externalHost}
</a>
@@ -26,6 +26,7 @@ import {
languageForPath,
topLanguagesFromCounts,
} from "@/features/projects/lib/projectLanguages";
import { projectExternalRefUrl } from "@/features/projects/lib/projectExternalUrl";
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import { RightAuxiliaryPane } from "@/features/channels/ui/RightAuxiliaryPane";
import { normalizePubkey } from "@/shared/lib/pubkey";
@@ -148,6 +149,10 @@ export function ProjectRepositoryActionsPanel({
: undefined;
const pullAction = sourceControls.canPull ? sourceControls.onPull : undefined;
const pushAction = sourceControls.canPush ? sourceControls.onPush : undefined;
const externalOpenUrl = projectExternalRefUrl(
sourceControls.externalUrl,
sourceControls.selectedTag ?? sourceControls.branch,
);
return (
<RightAuxiliaryPane
@@ -157,7 +162,7 @@ export function ProjectRepositoryActionsPanel({
testId="project-repository-actions-panel"
widthPx={widthPx}
>
<div className="min-h-0 flex-1 space-y-6 overflow-y-auto p-5">
<div className="relative z-30 min-h-0 flex-1 space-y-6 overflow-y-auto p-5">
<RepositoryPanelSection title="Working copy">
<div className="grid gap-2 [&_button]:w-full [&_button]:justify-between [&_button]:text-sm">
<RepoSourceDropdown controls={sourceControls} />
@@ -258,18 +263,14 @@ export function ProjectRepositoryActionsPanel({
<SquareTerminal className="h-3.5 w-3.5" />
Terminal
</RepositoryActionButton>
{sourceControls.externalUrl ? (
{externalOpenUrl ? (
<Button
asChild
className="h-8 justify-start gap-2 text-sm"
size="sm"
variant="outline"
>
<a
href={sourceControls.externalUrl}
rel="noreferrer"
target="_blank"
>
<a href={externalOpenUrl} rel="noreferrer" target="_blank">
<ExternalLink className="h-3.5 w-3.5" />
Open
</a>
@@ -32,6 +32,7 @@ import type { UserProfileLookup } from "@/features/profile/lib/identity";
import type { UserSearchResult } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
import { normalizePubkey } from "@/shared/lib/pubkey";
import { BuzzLoadingState } from "@/shared/ui/BuzzLoadingState";
import { SyntaxHighlightedCode } from "@/shared/ui/markdown";
import { UserAvatar } from "@/shared/ui/UserAvatar";
import {
@@ -694,15 +695,43 @@ export function RepositoryFilesPanel({
// Loading/error/empty states keep the header controls visible — the
// remote/local toggle must stay reachable when one source fails to load.
const stateMessage = isLoading
? "Loading repository files…"
: unavailableMessage
? unavailableMessage
: error
? "Could not load the repository file tree."
: files.length === 0
? "No files have been pushed yet."
: null;
if (isLoading) {
if (!sourceControls) {
return <BuzzLoadingState label="Loading repository files" />;
}
return (
<div className={PROJECT_DETAIL_PANEL_CLASS} data-project-detail-panel>
<div className="flex min-h-14 min-w-0 items-center gap-1 border-border/50 border-b px-4 py-3">
<RepoSourceDropdown controls={sourceControls} />
<RepositoryBranchDropdown
branch={sourceControls.branch}
branchOptions={sourceControls.branchOptions}
createBranchDisabled={sourceControls.createBranchDisabled}
createBranchTitle={sourceControls.createBranchTitle}
deleteBranchDisabled={sourceControls.deleteBranchDisabled}
deleteBranchTitle={sourceControls.deleteBranchTitle}
onBranchChange={sourceControls.onBranchChange}
onCreateBranch={sourceControls.onCreateBranch}
onDeleteBranch={sourceControls.onDeleteBranch}
onTagChange={sourceControls.onTagChange}
selectedTag={sourceControls.selectedTag}
tagOptions={sourceControls.tagOptions}
/>
<div className="ml-auto flex shrink-0 items-center">
<RepoSyncActionButton controls={sourceControls} />
</div>
</div>
<BuzzLoadingState label="Loading repository files" />
</div>
);
}
const stateMessage = unavailableMessage
? unavailableMessage
: error
? "Could not load the repository file tree."
: files.length === 0
? "No files have been pushed yet."
: null;
if (stateMessage) {
if (!sourceControls) {
return (
@@ -15,6 +15,7 @@ import {
} from "lucide-react";
import { Button } from "@/shared/ui/button";
import { projectExternalRefUrl } from "@/features/projects/lib/projectExternalUrl";
import type { ProjectRepoUnavailableReason } from "@/features/projects/lib/projectRepoAvailability";
import {
DropdownMenu,
@@ -74,12 +75,15 @@ export function RepositoryBranchDropdown({
<DropdownMenuTrigger asChild>
<Button
className={PROJECT_PICKER_TRIGGER_CLASS}
data-testid="project-repository-branch-trigger"
size="sm"
type="button"
variant="outline"
>
<RefIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{selectedTag ?? branch}</span>
<span className="min-w-0 flex-1 truncate text-left">
{selectedTag ?? branch}
</span>
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
@@ -98,7 +102,7 @@ export function RepositoryBranchDropdown({
{selectableBranches.map((option) => (
<DropdownMenuRadioItem key={option} value={`branch:${option}`}>
<GitBranch className="mr-1.5 h-3.5 w-3.5 text-muted-foreground" />
<span className="truncate">{option}</span>
<span className="min-w-0 flex-1 truncate">{option}</span>
</DropdownMenuRadioItem>
))}
{tagOptions.length > 0 ? (
@@ -111,7 +115,7 @@ export function RepositoryBranchDropdown({
value={`tag:${option.name}`}
>
<Tag className="mr-1.5 h-3.5 w-3.5 text-muted-foreground" />
<span className="truncate">{option.name}</span>
<span className="min-w-0 flex-1 truncate">{option.name}</span>
<span className="ml-auto font-mono text-xs text-muted-foreground">
{option.commit.slice(0, 7)}
</span>
@@ -233,7 +237,7 @@ export function RepoSourceDropdown({
variant="outline"
>
<SourceIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">
<span className="min-w-0 flex-1 truncate text-left">
{isLocal ? controls.localLabel : controls.remoteLabel}
</span>
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
@@ -289,8 +293,12 @@ export function RepoSyncActionButton({
}: {
controls: RepoSourceHeaderControls;
}) {
const externalOpenUrl = projectExternalRefUrl(
controls.externalUrl,
controls.selectedTag ?? controls.branch,
);
if (controls.remoteKind === "external") {
return controls.externalUrl ? (
return externalOpenUrl ? (
<Button
asChild
className={PROJECT_PANEL_ACTION_BUTTON_CLASS}
@@ -298,7 +306,7 @@ export function RepoSyncActionButton({
title={`Open repository on ${controls.remoteLabel}`}
variant="ghost"
>
<a href={controls.externalUrl} rel="noreferrer" target="_blank">
<a href={externalOpenUrl} rel="noreferrer" target="_blank">
<ExternalLink className="h-4 w-4" />
Open
</a>
@@ -32,6 +32,7 @@ import { projectRepoUnavailableReason } from "@/features/projects/lib/projectRep
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import { normalizePubkey } from "@/shared/lib/pubkey";
import { Button } from "@/shared/ui/button";
import { BuzzLoadingState } from "@/shared/ui/BuzzLoadingState";
import { Tabs, TabsContent } from "@/shared/ui/tabs";
import { findReadmeFile } from "./ProjectReadmePanel";
import { RepositoryFilesPanel } from "./ProjectRepositoryPanel";
@@ -551,13 +552,17 @@ export function WorkspaceTabs({
</TabsContent>
<TabsContent className="m-0" value="contributors">
<ContributorsPanel
activityCounts={contributorActivityCounts}
contributorPubkeys={contributorPubkeys}
contributorPubkeysByGitIdentity={contributorPubkeysByGitIdentity}
profiles={profiles}
repoContributors={displayedContributors}
/>
{displayedSnapshotLoading ? (
<BuzzLoadingState label="Loading contributors" />
) : (
<ContributorsPanel
activityCounts={contributorActivityCounts}
contributorPubkeys={contributorPubkeys}
contributorPubkeysByGitIdentity={contributorPubkeysByGitIdentity}
profiles={profiles}
repoContributors={displayedContributors}
/>
)}
</TabsContent>
</div>
{createPullRequestAction && createPullRequestOpen ? (
@@ -23,6 +23,7 @@ import {
} from "@/features/projects/projectPullRequests.mjs";
import { cn } from "@/shared/lib/cn";
import { normalizePubkey } from "@/shared/lib/pubkey";
import { BuzzLoadingState } from "@/shared/ui/BuzzLoadingState";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
import { UserAvatar } from "@/shared/ui/UserAvatar";
import {
@@ -431,19 +432,7 @@ export function ProjectsActivityFeed(props: ProjectsActivityFeedProps) {
const items = buildActivityItems(props);
if (props.isLoading && items.length === 0) {
return (
<div className={cn(props.compact ? "space-y-2.5" : "space-y-3")}>
{["first", "second", "third"].map((key) => (
<div
className={cn(
"animate-pulse rounded-xl border border-border/60 bg-muted/20",
props.compact ? "h-24" : "h-28",
)}
key={key}
/>
))}
</div>
);
return <BuzzLoadingState label="Loading project activity" />;
}
if (items.length === 0) {
@@ -19,6 +19,7 @@ import {
} from "@/features/profile/lib/identity";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import { BuzzLoadingState } from "@/shared/ui/BuzzLoadingState";
import { Card } from "@/shared/ui/card";
import { DropdownMenuItem } from "@/shared/ui/dropdown-menu";
import { CopyShareLinkMenuItem } from "./CopyShareLinkMenuItem";
@@ -318,16 +319,7 @@ export function ProjectsIssuesList({
viewMode,
}: ProjectsIssuesListProps) {
if (isLoading) {
return (
<div
className={cn(
"px-4 py-12 text-center text-sm text-muted-foreground",
!embedded && "border border-border/60",
)}
>
Loading tasks...
</div>
);
return <BuzzLoadingState label="Loading tasks" />;
}
const loadNotice = (
@@ -15,6 +15,7 @@ import {
type UserProfileLookup,
} from "@/features/profile/lib/identity";
import { Button } from "@/shared/ui/button";
import { BuzzLoadingState } from "@/shared/ui/BuzzLoadingState";
import { Card } from "@/shared/ui/card";
import { DropdownMenuItem } from "@/shared/ui/dropdown-menu";
import { CopyShareLinkMenuItem } from "./CopyShareLinkMenuItem";
@@ -317,16 +318,7 @@ export function ProjectsPullRequestsList({
viewMode,
}: ProjectsPullRequestsListProps) {
if (isLoading) {
return (
<div
className={cn(
"px-4 py-12 text-center text-sm text-muted-foreground",
!embedded && "border border-border/60",
)}
>
Loading reviews...
</div>
);
return <BuzzLoadingState label="Loading reviews" />;
}
const loadNotice = (
@@ -7,7 +7,7 @@ export const PROJECT_PANEL_ACTION_BUTTON_CLASS =
* they read as one consistent control family in the workspace header.
*/
export const PROJECT_PICKER_TRIGGER_CLASS =
"h-7 max-w-full shrink-0 gap-1.5 rounded-md px-3 text-sm font-medium hover:border-input";
"h-7 min-w-0 max-w-full gap-1.5 rounded-md px-3 text-sm font-medium hover:border-input";
/** Bordered shell that lets the project page surface show through. */
export const PROJECT_DETAIL_PANEL_CLASS =
@@ -597,7 +597,7 @@ export function AppSidebar({
) : null}
<SidebarContent
className="buzz-sidebar-scrollbar overscroll-none"
className="buzz-sidebar-scrollbar overscroll-none [overflow-anchor:none]"
data-sidebar-background
ref={scrollRef}
>
@@ -3,12 +3,54 @@ import { test } from "node:test";
import {
listSidebarProjects,
readSidebarProjectExpansion,
selectedProjectRouteId,
writeSidebarProjectExpansion,
} from "./listSidebarProjects.ts";
const OWNER = "a".repeat(64);
const VIEWER = "b".repeat(64);
test("project expansion persists independently per relay and viewer", () => {
const values = new Map();
const previousLocalStorage = globalThis.localStorage;
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
value: {
getItem: (key) => values.get(key) ?? null,
setItem: (key, value) => values.set(key, value),
},
});
try {
writeSidebarProjectExpansion(
{ "project:one": true, "project:two": false },
"https://relay.example",
VIEWER,
);
assert.deepEqual(
readSidebarProjectExpansion("https://relay.example", VIEWER),
{ "project:one": true, "project:two": false },
);
assert.deepEqual(
readSidebarProjectExpansion("https://other.example", VIEWER),
{},
);
assert.deepEqual(
readSidebarProjectExpansion("https://relay.example", OWNER),
{},
);
} finally {
if (previousLocalStorage === undefined) {
delete globalThis.localStorage;
} else {
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
value: previousLocalStorage,
});
}
}
});
function makeProject(overrides = {}) {
return {
createdAt: 0,
@@ -89,11 +89,14 @@ import {
} from "@/features/sidebar/ui/sidebarSectionStyles";
import {
listSidebarProjects,
readSidebarProjectExpansion,
readSidebarProjectsFilter,
readSidebarProjectsSort,
selectedProjectRouteId,
type SidebarProjectExpansionState,
type SidebarProjectsFilter,
type SidebarProjectsSort,
writeSidebarProjectExpansion,
writeSidebarProjectsFilter,
writeSidebarProjectsSort,
} from "@/features/sidebar/ui/listSidebarProjects";
@@ -144,6 +147,10 @@ function SidebarProjectsSectionContent() {
const [sort, setSort] = React.useState<SidebarProjectsSort>(
readSidebarProjectsSort,
);
const [projectExpansion, setProjectExpansion] =
React.useState<SidebarProjectExpansionState>(() =>
readSidebarProjectExpansion(relayOrigin, currentPubkey),
);
const [addedProjectAddresses, setAddedProjectAddresses] = React.useState<
string[]
>(() => readProjectSidebarMembership(relayOrigin, currentPubkey));
@@ -160,6 +167,11 @@ function SidebarProjectsSectionContent() {
return () =>
globalThis.removeEventListener(PROJECT_SIDEBAR_MEMBERSHIP_EVENT, refresh);
}, [currentPubkey, relayOrigin]);
React.useEffect(() => {
setProjectExpansion(
readSidebarProjectExpansion(relayOrigin, currentPubkey),
);
}, [currentPubkey, relayOrigin]);
const addedProjectAddressSet = React.useMemo(
() => new Set(addedProjectAddresses),
[addedProjectAddresses],
@@ -175,6 +187,23 @@ function SidebarProjectsSectionContent() {
}),
[addedProjectAddressSet, currentPubkey, filter, projectsQuery.data, sort],
);
React.useEffect(() => {
if (!routeProjectId) return;
const selectedProject = projects.find((project) =>
projectMatchesRouteId(project, routeProjectId),
);
if (
!selectedProject ||
projectExpansion[selectedProject.projectAddress] !== undefined
) {
return;
}
setProjectExpansion((current) => {
const next = { ...current, [selectedProject.projectAddress]: true };
writeSidebarProjectExpansion(next, relayOrigin, currentPubkey);
return next;
});
}, [currentPubkey, projectExpansion, projects, relayOrigin, routeProjectId]);
const handleFilterChange = (next: SidebarProjectsFilter) => {
setFilter(next);
@@ -184,6 +213,13 @@ function SidebarProjectsSectionContent() {
setSort(next);
writeSidebarProjectsSort(next);
};
const setProjectExpanded = (project: Project, expanded: boolean) => {
setProjectExpansion((current) => {
const next = { ...current, [project.projectAddress]: expanded };
writeSidebarProjectExpansion(next, relayOrigin, currentPubkey);
return next;
});
};
const handleAdd = (project: Project) => {
addProjectToSidebar(project.projectAddress, relayOrigin, currentPubkey);
};
@@ -282,6 +318,8 @@ function SidebarProjectsSectionContent() {
const selectedRepository = isActive
? selectProjectRepository(project, routeRepositoryId)
: null;
const isExpanded =
projectExpansion[project.projectAddress] ?? isActive;
return (
<React.Fragment key={project.id}>
@@ -292,12 +330,20 @@ function SidebarProjectsSectionContent() {
)}
deleteDisabled={deleteProjectMutation.isPending}
isActive={isActive}
isExpanded={isExpanded}
onDelete={() => setProjectToDelete(project)}
onOpen={() => goProject(project.id)}
onOpen={() => {
if (isActive) {
setProjectExpanded(project, !isExpanded);
return;
}
setProjectExpanded(project, true);
void goProject(project.id);
}}
onRemove={() => handleRemove(project)}
project={project}
/>
{isActive
{isExpanded
? project.repositories.map((repository) => (
<SidebarMenuItem key={repository.id}>
<SidebarMenuButton
@@ -529,6 +575,7 @@ function SidebarProjectRow({
canDelete,
deleteDisabled,
isActive,
isExpanded,
onDelete,
onOpen,
onRemove,
@@ -537,19 +584,21 @@ function SidebarProjectRow({
canDelete: boolean;
deleteDisabled: boolean;
isActive: boolean;
isExpanded: boolean;
onDelete: () => void;
onOpen: () => void;
onRemove: () => void;
project: Project;
}) {
const shareLink = projectShareLink(project);
const ProjectIcon = isActive ? FolderOpen : Folders;
const ProjectIcon = isExpanded ? FolderOpen : Folders;
return (
<ContextMenu>
<ContextMenuTrigger asChild>
<SidebarMenuItem>
<SidebarMenuButton
aria-expanded={isExpanded}
className="data-[active=true]:!bg-transparent data-[active=true]:font-normal data-[active=true]:text-sidebar-foreground data-[active=true]:shadow-none data-[active=true]:hover:!bg-transparent data-[active=true]:hover:text-sidebar-foreground data-[active=true]:active:!bg-transparent"
data-testid={`sidebar-project-${project.dtag}`}
isActive={isActive}
@@ -3,9 +3,55 @@ import { isProjectOwnedByCurrentUser } from "@/features/projects/lib/projectsVie
const SIDEBAR_PROJECTS_FILTER_KEY = "buzz.sidebar.projects.filter";
const SIDEBAR_PROJECTS_SORT_KEY = "buzz.sidebar.projects.sort";
const SIDEBAR_PROJECTS_EXPANDED_KEY = "buzz.sidebar.projects.expanded";
export type SidebarProjectsFilter = "added" | "owned";
export type SidebarProjectsSort = "name" | "created";
export type SidebarProjectExpansionState = Record<string, boolean>;
function expandedProjectsStorageKey(
relayOrigin: string | null,
currentPubkey?: string,
) {
return `${SIDEBAR_PROJECTS_EXPANDED_KEY}:${encodeURIComponent(relayOrigin ?? "unknown")}:${currentPubkey ?? "anonymous"}`;
}
export function readSidebarProjectExpansion(
relayOrigin: string | null,
currentPubkey?: string,
): SidebarProjectExpansionState {
try {
const value = globalThis.localStorage?.getItem(
expandedProjectsStorageKey(relayOrigin, currentPubkey),
);
if (!value) return {};
const parsed: unknown = JSON.parse(value);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
return {};
return Object.fromEntries(
Object.entries(parsed).filter((entry): entry is [string, boolean] => {
return typeof entry[1] === "boolean";
}),
);
} catch {
return {};
}
}
export function writeSidebarProjectExpansion(
expansion: SidebarProjectExpansionState,
relayOrigin: string | null,
currentPubkey?: string,
) {
try {
globalThis.localStorage?.setItem(
expandedProjectsStorageKey(relayOrigin, currentPubkey),
JSON.stringify(expansion),
);
} catch {
// Persistence is best-effort; the in-memory toggle still works.
}
}
export function selectedProjectRouteId(pathname: string): string | undefined {
if (!pathname.startsWith("/projects/")) return undefined;
@@ -0,0 +1,34 @@
import { cn } from "@/shared/lib/cn";
import BuzzLogoAnimation from "@/shared/ui/buzz-logo/BuzzLogoAnimation";
/** Centered, low-emphasis loading state for page and panel fetches. */
export function BuzzLoadingState({
className,
fill = false,
label = "Loading",
}: {
className?: string;
fill?: boolean;
label?: string;
}) {
return (
<div
className={cn(
"flex w-full items-center justify-center text-muted-foreground/45",
fill ? "min-h-0 flex-1" : "min-h-[calc(100dvh-7rem)]",
className,
)}
data-testid="buzz-loading-state"
role="status"
>
<BuzzLogoAnimation
ariaLabel={label}
className="buzz-logo--scale-pulse"
fullScreen={false}
showBackground={false}
style={{ width: "2rem" }}
textured={false}
/>
</div>
);
}
@@ -1,4 +1,5 @@
import { Card } from "@/shared/ui/card";
import { BuzzLoadingState } from "@/shared/ui/BuzzLoadingState";
import { Skeleton } from "@/shared/ui/skeleton";
import { cn } from "@/shared/lib/cn";
import { channelChrome } from "@/shared/layout/chromeLayout";
@@ -402,7 +403,9 @@ export function ViewLoadingFallback({
{shouldShowChannelHeader ? <LoadingHeaderSkeleton /> : null}
{kind === "agents" ? <AgentsLoadingBody /> : null}
{kind === "workflows" ? <CardListLoadingBody /> : null}
{kind === "projects" ? <CardListLoadingBody /> : null}
{kind === "projects" ? (
<BuzzLoadingState fill label="Loading projects" />
) : null}
{kind === "channel" ? (
<ChannelLoadingBody hasHeader={shouldShowChannelHeader} />
) : null}
@@ -56,6 +56,21 @@
animation: buzz-logo-pulse 1.8s ease-in-out infinite;
}
@keyframes buzz-logo-scale-pulse {
0%,
100% {
transform: scale(0.94);
}
50% {
transform: scale(1.06);
}
}
.buzz-logo--scale-pulse .buzz-logo__mark {
animation: buzz-logo-scale-pulse 1.8s ease-in-out infinite;
transform-origin: center;
}
@media (prefers-reduced-motion: reduce) {
.buzz-logo animate {
display: none;
@@ -65,4 +80,9 @@
animation: none;
opacity: 0.8;
}
.buzz-logo--scale-pulse .buzz-logo__mark {
animation: none;
transform: none;
}
}
+17 -1
View File
@@ -192,6 +192,8 @@ type E2eConfig = {
projectAccessChannelId?: string;
/** Make remote project snapshots fail with this git-facing message. */
projectRepoSnapshotError?: string;
/** Delay remote repository snapshots so project loading UI is observable. */
projectRepoSnapshotDelayMs?: number;
/** Builderlab account returned by hosted-community onboarding. Null/omitted = signed out. */
builderlabAuth?: {
email?: string;
@@ -1260,6 +1262,8 @@ declare global {
__BUZZ_E2E_REJECT_PROJECT_QUERY_KINDS__?: number[];
/** Captured aggregate project-history filters for request-count assertions. */
__BUZZ_E2E_PROJECT_QUERY_FILTERS__?: MockFilter[];
/** Optional local repository snapshot returned for project branch tests. */
__BUZZ_E2E_PROJECT_LOCAL_REPO_SNAPSHOT__?: unknown;
__BUZZ_E2E_PROJECT_REPO_SYNC_STATUS__?: {
local_path: string | null;
local_branch: string | null;
@@ -5364,6 +5368,7 @@ const MOCK_PROJECT_SEEDS = [
description:
"Relay, desktop, and mobile clients for the Buzz community platform.",
cloneUrl: `${DEFAULT_RELAY_HTTP_URL}/git/${MOCK_IDENTITY_PUBKEY}/buzz`,
webUrl: null,
owner: MOCK_IDENTITY_PUBKEY,
contributors: [ALICE_PUBKEY, BOB_PUBKEY, CHARLIE_PUBKEY],
activityLevel: 4,
@@ -5373,6 +5378,7 @@ const MOCK_PROJECT_SEEDS = [
name: "relay-tools",
description: "Operator tooling and admin CLI for relay deployments.",
cloneUrl: "https://github.com/block/relay-tools.git",
webUrl: "https://github.com/block/relay-tools",
owner: ALICE_PUBKEY,
contributors: [MOCK_IDENTITY_PUBKEY, BOB_PUBKEY],
activityLevel: 2,
@@ -5382,6 +5388,7 @@ const MOCK_PROJECT_SEEDS = [
name: "design-system",
description: "Shared UI tokens, typography ramps, and component library.",
cloneUrl: `${DEFAULT_RELAY_HTTP_URL}/git/${BOB_PUBKEY}/design-system`,
webUrl: null,
owner: BOB_PUBKEY,
contributors: [ALICE_PUBKEY],
activityLevel: 1,
@@ -5485,6 +5492,7 @@ function buildMockProjectEvents(): RelayEvent[] {
"9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50",
],
["clone", seed.cloneUrl],
...(seed.webUrl ? [["web", seed.webUrl]] : []),
...seed.contributors.map((pubkey) => ["p", pubkey]),
],
owner,
@@ -11543,6 +11551,14 @@ export function maybeInstallE2eTauriMocks() {
// viewer-identity avatar attribution is exercised in e2e.
return { name: "Thomas P", email: "thomasp@example.com" };
case "get_project_repo_snapshot":
if (activeConfig?.mock?.projectRepoSnapshotDelayMs) {
await new Promise((resolve) =>
window.setTimeout(
resolve,
activeConfig.mock?.projectRepoSnapshotDelayMs,
),
);
}
if (activeConfig?.mock?.projectRepoSnapshotError) {
throw new Error(activeConfig.mock.projectRepoSnapshotError);
}
@@ -11641,7 +11657,7 @@ export function maybeInstallE2eTauriMocks() {
],
};
case "get_project_local_repo_snapshot":
return null;
return window.__BUZZ_E2E_PROJECT_LOCAL_REPO_SNAPSHOT__ ?? null;
case "get_project_repo_diff":
return {
additions: 27,
@@ -437,8 +437,74 @@ test("multi-repository projects switch the active repository", async ({
const relayToolsRepository = page.getByTestId(
"sidebar-project-repository-relay-tools",
);
const projectRow = page.getByTestId("sidebar-project-buzz");
await expect(projectRow).toHaveAttribute("aria-expanded", "true");
await expect(primaryRepository).toHaveAttribute("data-active", "true");
await expect(relayToolsRepository).toBeVisible();
await projectRow.click();
await expect(projectRow).toHaveAttribute("aria-expanded", "false");
await expect(relayToolsRepository).toBeHidden();
await page.reload({ waitUntil: "domcontentloaded" });
await addProjectToSidebar(page, "buzz");
await expect(projectRow).toHaveAttribute("aria-expanded", "false");
await expect(relayToolsRepository).toBeHidden();
await projectRow.click();
await expect(projectRow).toHaveAttribute("aria-expanded", "true");
await expect(relayToolsRepository).toBeVisible();
await page.getByTestId("channel-general").click();
await expect(projectRow).toHaveAttribute("aria-expanded", "true");
await expect(relayToolsRepository).toBeVisible();
const sidebarScrollContent = page.getByTestId("sidebar-scroll-content");
const channelSidebarMetrics = await sidebarScrollContent.evaluate(
(element) => {
const bounds = element.getBoundingClientRect();
return {
clientWidth: element.clientWidth,
left: bounds.left,
top: bounds.top,
width: bounds.width,
};
},
);
await projectRow.click();
await expect(page).toHaveURL(/\/projects\//);
await expect(relayToolsRepository).toBeVisible();
const projectSidebarMetrics = await sidebarScrollContent.evaluate(
(element) => {
const bounds = element.getBoundingClientRect();
return {
clientWidth: element.clientWidth,
left: bounds.left,
top: bounds.top,
width: bounds.width,
};
},
);
expect(projectSidebarMetrics).toEqual(channelSidebarMetrics);
await projectRow.click();
await expect(projectRow).toHaveAttribute("aria-expanded", "false");
await page.getByTestId("channel-general").click();
const sidebarScroller = page.locator('[data-sidebar="content"]');
const anchoredScrollTop = await sidebarScroller.evaluate((element) => {
element.scrollTop = Math.min(
20,
Math.max(0, element.scrollHeight - element.clientHeight),
);
return element.scrollTop;
});
await projectRow.click();
await expect(page).toHaveURL(/\/projects\//);
await expect(projectRow).toHaveAttribute("aria-expanded", "true");
await expect
.poll(() =>
sidebarScroller.evaluate((element) => Math.round(element.scrollTop)),
)
.toBe(Math.round(anchoredScrollTop));
await waitForAnimations(page);
await page.screenshot({
path: `${SHOTS}/04-multi-repository-picker.png`,
+128
View File
@@ -32,6 +32,17 @@ async function openBuzzProject(page: import("@playwright/test").Page) {
await projectEntry.click();
}
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();
}
test("same-second request changes supersedes approval", async ({ page }) => {
await enableProjectsFeature(page);
await page.addInitScript(() => {
@@ -1276,6 +1287,123 @@ test("project branches can be deleted but the default branch cannot", async ({
expect(commands).toContain("delete_project_remote_branch");
});
test("external repositories stay on local source after a branch round trip", async ({
page,
}) => {
await enableProjectsFeature(page);
await page.addInitScript(() => {
const commit = "0123456789abcdef0123456789abcdef01234567";
const localBranch =
"wintermute/entity-link-recipient-cards-with-a-long-branch-name";
window.sessionStorage.setItem(
"buzz-e2e-project-branches",
JSON.stringify({ "relay-tools": { [localBranch]: commit } }),
);
window.__BUZZ_E2E_PROJECT_REPO_SYNC_STATUS__ = {
local_path: "/tmp/buzz/REPOS/relay-tools",
local_branch: localBranch,
local_branches: ["main", localBranch],
local_head: commit,
local_short_head: commit.slice(0, 7),
remote_branch: localBranch,
remote_head: commit,
remote_short_head: commit.slice(0, 7),
merge_base: commit,
ahead_count: 0,
behind_count: 0,
has_uncommitted_changes: false,
has_untracked_files: false,
can_push: false,
push_block_reason: "Local branch is already pushed.",
can_pull: false,
pull_block_reason: "Local branch is up to date.",
};
window.__BUZZ_E2E_PROJECT_LOCAL_REPO_SNAPSHOT__ = {
path: "/tmp/buzz/REPOS/relay-tools",
snapshot: {
latest_commit: null,
commits: [],
contributors: [],
files: [
{
path: "README.md",
kind: "text",
size: 21,
preview_content: "# Local branch README",
last_changed_at: null,
latest_commit: null,
},
],
},
};
});
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await addProjectToSidebar(page, "buzz");
await page.getByTestId("sidebar-project-repository-relay-tools").click();
await expect(
page.getByRole("heading", { name: "Local branch README" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Local", exact: true }),
).toBeVisible();
await expect(
page.getByRole("link", { name: "Open", exact: true }),
).toHaveAttribute("href", "https://github.com/block/relay-tools/tree/main");
await page.getByRole("button", { name: /main/ }).click();
await page
.getByRole("menuitemradio", {
name: "wintermute/entity-link-recipient-cards-with-a-long-branch-name",
})
.click();
const branchTrigger = page.getByTestId("project-repository-branch-trigger");
await expect(
page.getByRole("button", {
name: /wintermute\/entity-link-recipient-cards-with-a-long-branch-name/,
}),
).toBeVisible();
await expect
.poll(() =>
branchTrigger.evaluate(
(element) => element.scrollWidth <= element.clientWidth,
),
)
.toBe(true);
await expect
.poll(() =>
branchTrigger
.locator("span")
.evaluate((element) => element.scrollWidth > element.clientWidth),
)
.toBe(true);
await expect(
page.getByRole("button", { name: "Local", exact: true }),
).toBeVisible();
await expect(
page.getByRole("link", { name: "Open", exact: true }),
).toHaveAttribute(
"href",
"https://github.com/block/relay-tools/tree/wintermute%2Fentity-link-recipient-cards-with-a-long-branch-name",
);
await branchTrigger.click();
await page.getByRole("menuitemradio", { name: "main" }).click();
await expect(page.getByRole("button", { name: /main/ })).toBeVisible();
await expect(
page.getByRole("button", { name: "Local", exact: true }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Local branch README" }),
).toBeVisible();
await expect(
page.getByRole("link", { name: "Open", exact: true }),
).toHaveAttribute("href", "https://github.com/block/relay-tools/tree/main");
await expect(page.getByText("Code hosted on github.com")).toHaveCount(0);
});
test("pushed local branch can open a pull request", async ({ page }) => {
await enableProjectsFeature(page);
await page.addInitScript(() => {
@@ -113,6 +113,27 @@ test("restricted repositories keep event work visible and offer access help", as
await expect(chatPanel.getByTestId("message-composer")).toBeVisible();
});
test("repository pages show a centered Buzz loader while fetching", async ({
page,
}) => {
await installMockBridge(page, { projectRepoSnapshotDelayMs: 750 });
await openBuzzProject(page);
const loader = page.getByTestId("buzz-loading-state");
await expect(loader).toBeVisible();
await expect(
loader.getByRole("img", { name: "Loading repository" }),
).toBeVisible();
const animatedMark = loader.locator(".buzz-logo__mark");
await expect(animatedMark).toHaveCSS(
"animation-name",
"buzz-logo-scale-pulse",
);
await expect(animatedMark).toHaveCSS("opacity", "1");
await expect(loader).toHaveCSS("justify-content", "center");
await expect(loader).toBeHidden({ timeout: 5_000 });
});
// Walks the Projects v3 workspace through its headline states so PR
// screenshots capture distinct pixels per feature (overview box, tab-strip
// plus, issue detail with inline copy link + avatar timeline, PR detail).
@@ -177,6 +198,31 @@ test("projects v3 workspace screenshot states", async ({ page }) => {
).toBeGreaterThan(13);
expect((await filesTab.boundingBox())?.height).toBe(28);
await expect(repositoryActionsPanel).toBeVisible();
const sharedHeaderBackdrop = page.getByTestId(
"project-shared-header-backdrop",
);
await expect(sharedHeaderBackdrop).toBeVisible();
await expect
.poll(() =>
sharedHeaderBackdrop.evaluate(
(element) => getComputedStyle(element).backdropFilter,
),
)
.not.toBe("none");
const [sharedHeaderBackdropBounds, repositoryActionsPanelBounds] =
await Promise.all([
sharedHeaderBackdrop.boundingBox(),
repositoryActionsPanel.boundingBox(),
]);
expect(sharedHeaderBackdropBounds).not.toBeNull();
expect(repositoryActionsPanelBounds).not.toBeNull();
expect(
(sharedHeaderBackdropBounds?.x ?? 0) +
(sharedHeaderBackdropBounds?.width ?? 0),
).toBeGreaterThanOrEqual(
(repositoryActionsPanelBounds?.x ?? 0) +
(repositoryActionsPanelBounds?.width ?? 0),
);
const repositoryPanelTab = page.getByTestId(
"project-right-panel-repository-tab",
);
@@ -228,19 +274,6 @@ test("projects v3 workspace screenshot states", async ({ page }) => {
(tabMenuHeaderBounds?.height ?? 0) - (agentContextBounds?.height ?? 0),
),
).toBeLessThanOrEqual(1);
const sharedHeaderBackdrop = page.getByTestId(
"project-shared-header-backdrop",
);
await expect(sharedHeaderBackdrop).toBeVisible();
await expect
.poll(() =>
sharedHeaderBackdrop.evaluate(
(element) => getComputedStyle(element).backdropFilter,
),
)
.not.toBe("none");
const sharedHeaderBackdropBounds = await sharedHeaderBackdrop.boundingBox();
expect(sharedHeaderBackdropBounds).not.toBeNull();
expect(sharedHeaderBackdropBounds?.x).toBeLessThanOrEqual(
tabMenuHeaderBounds?.x ?? 0,
);