desktop: test layout — single workspace box for project detail

Restructure the project detail view so the repository / source / branch
selection row sits at the top of one panel box, with the tab strip and
the active panel inside it. The readme header rows move up: source
controls into the selection row, the last-changed timestamp into the
tab strip. The files panel drops its duplicated source controls and
keeps only its path breadcrumb. All selection dropdowns (repository,
source, branch) now share one trigger style.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
This commit is contained in:
Thomas Petersen
2026-08-11 07:03:57 -04:00
parent 764d6183a1
commit 20fce153ae
9 changed files with 349 additions and 269 deletions
@@ -843,36 +843,6 @@ export function ProjectDetailScreen(props: ProjectDetailScreenProps) {
<div className="flex min-h-0 min-w-0 flex-1 flex-row overflow-hidden">
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<ProjectDetailChrome
actions={
<>
<ProjectRepositoryManagement
identityPubkey={identityQuery.data?.pubkey}
onChange={handleRepositoryChange}
project={project}
projects={projectsQuery.data ?? []}
repository={repository}
/>
{repoRemote.webUrl &&
(repoRemote.host.kind !== "external" ||
repoSource === "local") ? (
<Button
asChild
aria-label="Open project web page"
className="h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground"
size="icon"
variant="ghost"
>
<a
href={repoRemote.webUrl}
rel="noopener noreferrer"
target="_blank"
>
<ExternalLink className="h-4 w-4" />
</a>
</Button>
) : null}
</>
}
activeTabCrumb={activeTabCrumb}
activeWorkItemCrumb={activeWorkItemCrumb}
chromeRef={projectDetailHeaderChromeRef}
@@ -932,6 +902,36 @@ export function ProjectDetailScreen(props: ProjectDetailScreenProps) {
onSelectedTabChange={setActiveTab}
profiles={profiles}
project={repository}
repositoryControls={
<>
<ProjectRepositoryManagement
identityPubkey={identityQuery.data?.pubkey}
onChange={handleRepositoryChange}
project={project}
projects={projectsQuery.data ?? []}
repository={repository}
/>
{repoRemote.webUrl &&
(repoRemote.host.kind !== "external" ||
repoSource === "local") ? (
<Button
asChild
aria-label="Open project web page"
className="h-7 w-7 shrink-0 text-muted-foreground hover:text-foreground"
size="icon"
variant="ghost"
>
<a
href={repoRemote.webUrl}
rel="noopener noreferrer"
target="_blank"
>
<ExternalLink className="h-4 w-4" />
</a>
</Button>
) : null}
</>
}
projectId={project.id}
repoDiff={displayedRepoDiff}
repoDiffError={displayedRepoDiffError}
@@ -36,6 +36,8 @@ type ProjectOverviewPanelProps = {
externalUrl?: string | null;
files: ProjectRepoFile[];
gitDataState: GitDataState;
/** Hide the readme header rows when the workspace renders them itself. */
hideReadmeHeader?: boolean;
project: Project;
onViewContributors: () => void;
profiles?: UserProfileLookup;
@@ -149,6 +151,7 @@ export function ProjectOverviewPanel({
externalUrl,
files,
gitDataState,
hideReadmeHeader,
onViewContributors,
project,
profiles,
@@ -182,6 +185,7 @@ export function ProjectOverviewPanel({
externalUrl={externalUrl}
file={readmeFile}
gitDataState={gitDataState}
hideHeader={hideReadmeHeader}
sourceControls={sourceControls}
unavailableReason={unavailableReason}
/>
@@ -140,6 +140,7 @@ export function ReadmePanel({
gitDataState,
externalHost,
externalUrl,
hideHeader,
sourceControls,
unavailableReason,
}: {
@@ -149,13 +150,18 @@ export function ReadmePanel({
gitDataState: "checking" | "available" | "empty" | "unavailable";
externalHost?: string;
externalUrl?: string | null;
/**
* Skip the header rows entirely — the workspace layout renders the source
* controls and last-changed timestamp itself.
*/
hideHeader?: boolean;
unavailableReason?: ProjectRepoUnavailableReason;
/** Branch picker + remote/local toggle rendered in the panel header. */
sourceControls?: RepoSourceHeaderControls;
}) {
// Two header rows, mirroring the files panel: controls on top, then the
// file identity row.
const header = (
const header = hideHeader ? null : (
<>
{sourceControls ? (
<div className="flex min-h-14 min-w-0 items-center gap-1 border-border/50 border-b px-3 py-3">
@@ -163,7 +169,6 @@ export function ReadmePanel({
<RepositoryBranchDropdown
branch={sourceControls.branch}
branchOptions={sourceControls.branchOptions}
compact
createBranchDisabled={sourceControls.createBranchDisabled}
createBranchTitle={sourceControls.createBranchTitle}
deleteBranchDisabled={sourceControls.deleteBranchDisabled}
@@ -118,7 +118,7 @@ export function ProjectRepositoryManagement({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="h-8 shrink-0 gap-1.5"
className="h-7 shrink-0 gap-1.5 rounded-md"
disabled={repairMutation.isPending}
size="sm"
type="button"
@@ -710,7 +710,6 @@ export function RepositoryFilesPanel({
<RepositoryBranchDropdown
branch={sourceControls.branch}
branchOptions={sourceControls.branchOptions}
compact
createBranchDisabled={sourceControls.createBranchDisabled}
createBranchTitle={sourceControls.createBranchTitle}
deleteBranchDisabled={sourceControls.deleteBranchDisabled}
@@ -752,7 +751,6 @@ export function RepositoryFilesPanel({
<RepositoryBranchDropdown
branch={sourceControls.branch}
branchOptions={sourceControls.branchOptions}
compact
createBranchDisabled={sourceControls.createBranchDisabled}
createBranchTitle={sourceControls.createBranchTitle}
deleteBranchDisabled={sourceControls.deleteBranchDisabled}
@@ -1,13 +1,14 @@
import {
Check,
ChevronDown,
FolderGit2,
FolderPlus,
GitBranch,
Link,
Plus,
} from "lucide-react";
import type { Project, Repository } from "@/features/projects/hooks";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import {
DropdownMenu,
@@ -16,6 +17,7 @@ import {
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import { PROJECT_PICKER_TRIGGER_CLASS } from "./projectPanelStyles";
export function ProjectRepositoryPicker({
onAttach,
@@ -32,7 +34,7 @@ export function ProjectRepositoryPicker({
}) {
const repositoryLabel = (
<>
<GitBranch className="h-3.5 w-3.5" />
<FolderGit2 className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{repository.name}</span>
</>
);
@@ -42,7 +44,10 @@ export function ProjectRepositoryPicker({
<div className="flex items-center gap-1.5">
{project.repositoryAddresses.length === 1 ? (
<div
className="flex h-8 max-w-64 shrink-0 items-center gap-1.5 rounded-md border border-input bg-background px-3 text-sm font-medium"
className={cn(
"flex max-w-64 items-center rounded-md border border-input bg-background",
PROJECT_PICKER_TRIGGER_CLASS,
)}
data-testid="project-repository-picker"
>
{repositoryLabel}
@@ -52,13 +57,13 @@ export function ProjectRepositoryPicker({
<DropdownMenuTrigger asChild>
<Button
aria-label="Select repository"
className="h-8 max-w-64 shrink-0 gap-1.5"
className={cn("max-w-64", PROJECT_PICKER_TRIGGER_CLASS)}
data-testid="project-repository-picker"
size="sm"
variant="outline"
>
{repositoryLabel}
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-56">
@@ -98,7 +103,7 @@ export function ProjectRepositoryPicker({
<DropdownMenuTrigger asChild>
<Button
aria-label="Add repository"
className="h-8 w-8 shrink-0"
className="h-7 w-7 shrink-0 rounded-md"
data-testid="add-project-repository"
size="icon"
type="button"
@@ -26,7 +26,10 @@ import {
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import { GitHubMark } from "./GitHubMark";
import { PROJECT_PANEL_ACTION_BUTTON_CLASS } from "./projectPanelStyles";
import {
PROJECT_PANEL_ACTION_BUTTON_CLASS,
PROJECT_PICKER_TRIGGER_CLASS,
} from "./projectPanelStyles";
/** Branch picker shared by the readme and files panel headers. */
export function RepositoryBranchDropdown({
@@ -34,7 +37,6 @@ export function RepositoryBranchDropdown({
branchOptions,
selectedTag,
tagOptions = [],
compact,
createBranchDisabled,
createBranchTitle,
deleteBranchDisabled,
@@ -48,8 +50,6 @@ export function RepositoryBranchDropdown({
branchOptions: string[];
selectedTag?: string | null;
tagOptions?: Array<{ name: string; commit: string }>;
/** Smaller trigger for inline headers. */
compact?: boolean;
createBranchDisabled?: boolean;
createBranchTitle?: string;
deleteBranchDisabled?: boolean;
@@ -74,17 +74,13 @@ export function RepositoryBranchDropdown({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className={
compact
? "h-7 max-w-full gap-1.5 rounded-md px-3 font-mono text-sm font-medium hover:border-input"
: "h-6 max-w-full gap-1.5 px-2 font-mono text-sm font-semibold hover:border-input"
}
className={PROJECT_PICKER_TRIGGER_CLASS}
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="truncate font-mono">{selectedTag ?? branch}</span>
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
@@ -229,7 +225,7 @@ export function RepoSourceDropdown({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="h-7 max-w-full shrink-0 gap-1.5 rounded-md px-3 text-sm font-medium hover:border-input"
className={PROJECT_PICKER_TRIGGER_CLASS}
size="sm"
type="button"
variant="outline"
@@ -29,11 +29,20 @@ import {
} from "@/features/projects/lib/projectRepoAvailability";
import { useMemberChannelIds } from "@/features/projects/useRepositoryAccess";
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import { Tabs, TabsContent } from "@/shared/ui/tabs";
import { findReadmeFile } from "./ProjectReadmePanel";
import { RepositoryFilesPanel } from "./ProjectRepositoryPanel";
import type { RepoSourceHeaderControls } from "./ProjectRepositorySource";
import {
formatLastChangedAt,
RepositoryFilesPanel,
} from "./ProjectRepositoryPanel";
import {
type RepoSourceHeaderControls,
RepoSourceDropdown,
RepoSyncActionButton,
RepositoryBranchDropdown,
} from "./ProjectRepositorySource";
import { ProjectCommitDetailPanel } from "./ProjectCommitDetailPanel";
import { ActivityPanel, ContributorsPanel } from "./ProjectDetailFeedPanels";
import { ProjectIssuesPanel } from "./ProjectIssuesPanel";
@@ -154,6 +163,7 @@ export function WorkspaceTabs({
repoContributors,
repoSource,
repoHost,
repositoryControls,
sourceControls,
terminalTitle,
viewerGitIdentity,
@@ -193,7 +203,9 @@ export function WorkspaceTabs({
repoContributors: ProjectRepoContributor[];
repoSource: "remote" | "local";
repoHost: ProjectRepoHost;
/** Branch picker + remote/local toggle for the Code tab header. */
/** Repository picker (and related actions) for the selection header row. */
repositoryControls?: React.ReactNode;
/** Branch picker + remote/local toggle for the selection header row. */
sourceControls?: RepoSourceHeaderControls;
terminalTitle?: string;
viewerGitIdentity?: ViewerGitIdentity | null;
@@ -326,235 +338,288 @@ export function WorkspaceTabs({
return (
<Tabs
className="space-y-3"
className="min-w-0"
onValueChange={handleTabChange}
value={selectedTab}
>
{repositoryLoaded ? (
<div className="flex h-10 min-w-0 items-center gap-1">
<ProjectTabsList prsActive={isPullRequestSelected} />
{onOpenTerminal ? (
<Button
aria-label="Open terminal"
className="h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground"
onClick={() => onOpenTerminal()}
size="icon"
title={terminalTitle ?? "Open terminal"}
variant="ghost"
>
<SquareTerminal className="h-[1.125rem] w-[1.125rem]" />
</Button>
) : null}
{updatePullRequestAction ? (
<Button
className="h-8 shrink-0 gap-1.5"
disabled={updatePullRequestAction.pending}
onClick={updatePullRequestAction.onUpdate}
size="sm"
title="Publish the pushed commit to this pull request"
variant="outline"
>
<RefreshCw className="h-4 w-4" />
{updatePullRequestAction.pending ? "Updating…" : "Update PR"}
</Button>
) : null}
</div>
) : null}
{selectedPullRequest ? (
<div className={PROJECT_DETAIL_PANEL_CLASS} data-project-detail-panel>
{/* Two full-height columns: the meta rail runs all the way to the
{/* Single workspace box: selection header (repository / source /
branch), then the tab strip, then the active panel. Inner panels
keep their own PROJECT_DETAIL_PANEL_CLASS for standalone use, so
their border + radius are neutralized here via the data attribute
they all carry. */}
<div
className={cn(
PROJECT_DETAIL_PANEL_CLASS,
"[&_[data-project-detail-panel]]:rounded-none [&_[data-project-detail-panel]]:border-0",
)}
data-project-detail-panel
>
{repositoryControls || sourceControls ? (
<div className="flex min-h-12 min-w-0 flex-wrap items-center gap-1.5 border-border/50 border-b px-3 py-2">
{repositoryControls}
{sourceControls ? (
<>
<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>
</>
) : null}
</div>
) : null}
{repositoryLoaded ? (
<div className="flex h-10 min-w-0 items-center gap-1 border-border/50 border-b px-2">
<ProjectTabsList prsActive={isPullRequestSelected} />
{onOpenTerminal ? (
<Button
aria-label="Open terminal"
className="h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground"
onClick={() => onOpenTerminal()}
size="icon"
title={terminalTitle ?? "Open terminal"}
variant="ghost"
>
<SquareTerminal className="h-[1.125rem] w-[1.125rem]" />
</Button>
) : null}
{updatePullRequestAction ? (
<Button
className="h-8 shrink-0 gap-1.5"
disabled={updatePullRequestAction.pending}
onClick={updatePullRequestAction.onUpdate}
size="sm"
title="Publish the pushed commit to this pull request"
variant="outline"
>
<RefreshCw className="h-4 w-4" />
{updatePullRequestAction.pending ? "Updating…" : "Update PR"}
</Button>
) : null}
{selectedTab === "overview" && readmeFile ? (
<span className="ml-auto hidden shrink-0 pr-2 text-2xs text-muted-foreground sm:block">
Last changed {formatLastChangedAt(readmeFile.lastChangedAt)}
</span>
) : null}
</div>
) : null}
{selectedPullRequest ? (
<div className={PROJECT_DETAIL_PANEL_CLASS} data-project-detail-panel>
{/* Two full-height columns: the meta rail runs all the way to the
top of the card, alongside the header and tabs. */}
<div className="grid xl:grid-cols-[minmax(0,1fr)_18rem]">
<div className="min-w-0">
<PullRequestDetailHeader
profiles={profiles}
pullRequest={selectedPullRequest}
/>
<div className="border-b border-border/60 px-4">
<PullRequestTabsList
filesCount={repoDiff?.files.length ?? files.length}
<div className="grid xl:grid-cols-[minmax(0,1fr)_18rem]">
<div className="min-w-0">
<PullRequestDetailHeader
profiles={profiles}
pullRequest={selectedPullRequest}
/>
</div>
{(["conversation", "commits", "checks"] as const).map((mode) => (
<TabsContent className="m-0" key={mode} value={`pr-${mode}`}>
<PullRequestsPanel
error={pullRequestsError}
isLoading={pullRequestsLoading}
mode={mode}
onOpenInlineComment={handleOpenPullRequestComment}
onOpenCommit={onSelectedCommitHashChange}
onOpenTerminal={onOpenMergeRecoveryTerminal}
onSelectedPullRequestIdChange={
onSelectedPullRequestIdChange
<div className="border-b border-border/60 px-4">
<PullRequestTabsList
filesCount={repoDiff?.files.length ?? files.length}
pullRequest={selectedPullRequest}
/>
</div>
{(["conversation", "commits", "checks"] as const).map(
(mode) => (
<TabsContent
className="m-0"
key={mode}
value={`pr-${mode}`}
>
<PullRequestsPanel
error={pullRequestsError}
isLoading={pullRequestsLoading}
mode={mode}
onOpenInlineComment={handleOpenPullRequestComment}
onOpenCommit={onSelectedCommitHashChange}
onOpenTerminal={onOpenMergeRecoveryTerminal}
onSelectedPullRequestIdChange={
onSelectedPullRequestIdChange
}
profiles={profiles}
project={project}
pullRequests={pullRequests}
selectedPullRequestId={selectedPullRequestId}
/>
</TabsContent>
),
)}
<TabsContent className="m-0" value="pr-files">
<ProjectPullRequestFilesChangedPanel
diff={repoDiff}
error={repoDiffError}
focusedAnchor={
pullRequestCommentTarget?.pullRequestId ===
selectedPullRequestId
? pullRequestCommentTarget.anchor
: null
}
isLoading={repoDiffLoading}
profiles={profiles}
project={project}
pullRequests={pullRequests}
selectedPullRequestId={selectedPullRequestId}
pullRequest={selectedPullRequest}
/>
</TabsContent>
))}
<TabsContent className="m-0" value="pr-files">
<ProjectPullRequestFilesChangedPanel
diff={repoDiff}
error={repoDiffError}
focusedAnchor={
pullRequestCommentTarget?.pullRequestId ===
selectedPullRequestId
? pullRequestCommentTarget.anchor
: null
}
isLoading={repoDiffLoading}
profiles={profiles}
project={project}
pullRequest={selectedPullRequest}
/>
</TabsContent>
</div>
<PullRequestMetaRail
profiles={profiles}
project={project}
pullRequest={selectedPullRequest}
/>
</div>
</div>
) : null}
<TabsContent className="m-0" value="overview">
<ProjectOverviewPanel
accessChannelId={project.channelId}
contributors={displayedContributors}
externalHost={externalHost}
externalUrl={externalHost ? sourceControls?.externalUrl : null}
files={files}
gitDataState={gitDataState}
onViewContributors={() => setSelectedTab("contributors")}
profiles={profiles}
project={project}
pullRequests={pullRequests}
readmeFile={readmeFile}
snapshot={displayedSnapshot}
sourceControls={sourceControls}
unavailableReason={unavailableReason}
/>
</TabsContent>
<TabsContent className="m-0" value="activity">
{selectedCommitHash ? (
<ProjectCommitDetailPanel
commit={
displayedSnapshot?.commits.find(
(commit) => commit.hash === selectedCommitHash,
) ?? null
}
commitAuthorPubkeys={commitAuthorPubkeys}
commitHash={selectedCommitHash}
viewerGitIdentity={viewerGitIdentity}
diff={commitDiff}
diffError={commitDiffError}
diffLoading={commitDiffLoading}
originAgentName={selectedCommitPullRequest?.originAgentName}
originChannelId={selectedCommitPullRequest?.channelId}
profiles={profiles}
/>
) : (
<ActivityPanel
branch={sourceControls?.branch}
error={displayedSnapshotError}
isLoading={displayedSnapshotLoading}
onSelectCommit={(commit) => onSelectedCommitHashChange(commit.hash)}
profiles={profiles}
pullRequests={pullRequests}
repoContributors={displayedContributors}
snapshot={displayedSnapshot}
viewerGitIdentity={viewerGitIdentity}
/>
)}
</TabsContent>
<TabsContent
className={`m-0 ${PROJECT_DETAIL_PANEL_CLASS}`}
data-project-detail-panel
value="prs"
>
<WorkItemListHeader
actionDisabled={
!createPullRequestAction ||
createPullRequestAction.projects.length === 0
}
actionLabel="Pull Request"
actionTitle="Choose a repository and branches to compare."
icon={GitPullRequest}
onAction={() => setCreatePullRequestOpen(true)}
title="Pull Requests"
/>
<PullRequestsPanel
error={pullRequestsError}
isLoading={pullRequestsLoading}
onOpenCommit={onSelectedCommitHashChange}
onOpenTerminal={onOpenMergeRecoveryTerminal}
onSelectedPullRequestIdChange={onSelectedPullRequestIdChange}
profiles={profiles}
project={project}
pullRequests={pullRequests}
selectedPullRequestId={selectedPullRequestId}
/>
</TabsContent>
<TabsContent
className={`m-0 ${PROJECT_DETAIL_PANEL_CLASS}`}
data-project-detail-panel
value="issues"
>
<WorkItemListHeader
actionDisabled={createIssueAction.pending}
actionLabel="Issues"
icon={CircleDot}
onAction={() => setCreateIssueOpen(true)}
title="Issues"
/>
<ProjectIssuesPanel
onSelectedIssueIdChange={onSelectedIssueIdChange}
profiles={profiles}
project={project}
selectedIssueId={selectedIssueId}
/>
</TabsContent>
<TabsContent className="m-0" value="files">
{repoSource === "local" && !localSnapshot && !localSnapshotLoading ? (
<div className="mb-3">
<div
className={PROJECT_DETAIL_PANEL_MESSAGE_CLASS}
data-project-detail-panel
>
No local checkout found.
</div>
<PullRequestMetaRail
profiles={profiles}
project={project}
pullRequest={selectedPullRequest}
/>
</div>
</div>
) : null}
<RepositoryFilesPanel
error={displayedSnapshotError}
fallbackAuthorPubkey={project.owner}
files={files}
isLoading={displayedSnapshotLoading}
profiles={profiles}
snapshot={displayedSnapshot}
sourceControls={sourceControls}
unavailableMessage={
externalHost
? `Not mirrored on Buzz. Repository files are hosted on ${externalHost}.`
: undefined
}
/>
</TabsContent>
<TabsContent className="m-0" value="contributors">
<ContributorsPanel
profiles={profiles}
repoContributors={displayedContributors}
/>
</TabsContent>
<TabsContent className="m-0" value="overview">
<ProjectOverviewPanel
accessChannelId={project.channelId}
contributors={displayedContributors}
externalHost={externalHost}
externalUrl={externalHost ? sourceControls?.externalUrl : null}
files={files}
gitDataState={gitDataState}
hideReadmeHeader
onViewContributors={() => setSelectedTab("contributors")}
profiles={profiles}
project={project}
pullRequests={pullRequests}
readmeFile={readmeFile}
snapshot={displayedSnapshot}
sourceControls={sourceControls}
unavailableReason={unavailableReason}
/>
</TabsContent>
<TabsContent className="m-0" value="activity">
{selectedCommitHash ? (
<ProjectCommitDetailPanel
commit={
displayedSnapshot?.commits.find(
(commit) => commit.hash === selectedCommitHash,
) ?? null
}
commitAuthorPubkeys={commitAuthorPubkeys}
commitHash={selectedCommitHash}
viewerGitIdentity={viewerGitIdentity}
diff={commitDiff}
diffError={commitDiffError}
diffLoading={commitDiffLoading}
originAgentName={selectedCommitPullRequest?.originAgentName}
originChannelId={selectedCommitPullRequest?.channelId}
profiles={profiles}
/>
) : (
<ActivityPanel
branch={sourceControls?.branch}
error={displayedSnapshotError}
isLoading={displayedSnapshotLoading}
onSelectCommit={(commit) =>
onSelectedCommitHashChange(commit.hash)
}
profiles={profiles}
pullRequests={pullRequests}
repoContributors={displayedContributors}
snapshot={displayedSnapshot}
viewerGitIdentity={viewerGitIdentity}
/>
)}
</TabsContent>
<TabsContent
className={`m-0 ${PROJECT_DETAIL_PANEL_CLASS}`}
data-project-detail-panel
value="prs"
>
<WorkItemListHeader
actionDisabled={
!createPullRequestAction ||
createPullRequestAction.projects.length === 0
}
actionLabel="Pull Request"
actionTitle="Choose a repository and branches to compare."
icon={GitPullRequest}
onAction={() => setCreatePullRequestOpen(true)}
title="Pull Requests"
/>
<PullRequestsPanel
error={pullRequestsError}
isLoading={pullRequestsLoading}
onOpenCommit={onSelectedCommitHashChange}
onOpenTerminal={onOpenMergeRecoveryTerminal}
onSelectedPullRequestIdChange={onSelectedPullRequestIdChange}
profiles={profiles}
project={project}
pullRequests={pullRequests}
selectedPullRequestId={selectedPullRequestId}
/>
</TabsContent>
<TabsContent
className={`m-0 ${PROJECT_DETAIL_PANEL_CLASS}`}
data-project-detail-panel
value="issues"
>
<WorkItemListHeader
actionDisabled={createIssueAction.pending}
actionLabel="Issues"
icon={CircleDot}
onAction={() => setCreateIssueOpen(true)}
title="Issues"
/>
<ProjectIssuesPanel
onSelectedIssueIdChange={onSelectedIssueIdChange}
profiles={profiles}
project={project}
selectedIssueId={selectedIssueId}
/>
</TabsContent>
<TabsContent className="m-0" value="files">
{repoSource === "local" && !localSnapshot && !localSnapshotLoading ? (
<div className="mb-3">
<div
className={PROJECT_DETAIL_PANEL_MESSAGE_CLASS}
data-project-detail-panel
>
No local checkout found.
</div>
</div>
) : null}
<RepositoryFilesPanel
error={displayedSnapshotError}
fallbackAuthorPubkey={project.owner}
files={files}
isLoading={displayedSnapshotLoading}
profiles={profiles}
snapshot={displayedSnapshot}
unavailableMessage={
externalHost
? `Not mirrored on Buzz. Repository files are hosted on ${externalHost}.`
: undefined
}
/>
</TabsContent>
<TabsContent className="m-0" value="contributors">
<ContributorsPanel
profiles={profiles}
repoContributors={displayedContributors}
/>
</TabsContent>
</div>
{createPullRequestAction && createPullRequestOpen ? (
<CreatePullRequestDialog
initialProjectId={projectId}
@@ -2,6 +2,13 @@
export const PROJECT_PANEL_ACTION_BUTTON_CLASS =
"h-auto shrink-0 gap-1.5 rounded-full border-transparent bg-muted px-3 py-1.5 text-sm font-medium text-foreground shadow-none hover:bg-muted/80";
/**
* Shared trigger for the selection dropdowns (repository, source, branch) so
* 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";
/** Bordered shell that lets the project page surface show through. */
export const PROJECT_DETAIL_PANEL_CLASS =
"overflow-hidden rounded-xl border border-border/60 bg-transparent";