feat(desktop): polish selectable Projects workspaces

Unify grouped selection, contextual actions, agent chat, and local repository controls so Projects workflows stay focused across overview and detail views.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
This commit is contained in:
Thomas Petersen
2026-08-17 08:37:31 -04:00
parent b6894d42ea
commit ba185ba4f5
72 changed files with 4763 additions and 1456 deletions

View File

@@ -28,6 +28,11 @@ import type {
Repository,
} from "@/features/projects/hooks";
import { projectExternalRefUrl } from "@/features/projects/lib/projectExternalUrl";
import {
type ProjectSelectionItem,
projectSelectionPresentation,
} from "@/features/projects/lib/projectSelection";
import { useProjectSelection } from "@/features/projects/lib/useProjectSelection";
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import { RightAuxiliaryPane } from "@/features/channels/ui/RightAuxiliaryPane";
import { normalizePubkey } from "@/shared/lib/pubkey";
@@ -40,11 +45,15 @@ import {
RepositoryBranchDropdown,
} from "./ProjectRepositorySource";
import { ProjectRepositoryManagement } from "./ProjectRepositoryManagement";
import { ProjectWorkItemContextActions } from "./ProjectWorkItemContextActions";
import { ProjectWorkItemContextDetails } from "./ProjectWorkItemContextDetails";
import { ProjectsSelectionCountMenu } from "./ProjectsSelectionCountMenu";
import {
projectReviewActivity,
projectRightPanelScope,
projectTaskActivity,
} from "./projectRightPanelContext";
import { PROJECT_CONTEXT_ACTION_BUTTON_CLASS } from "./projectContextActionStyles";
type ProjectRepositoryActionsPanelProps = {
activeTab: string;
@@ -55,7 +64,9 @@ type ProjectRepositoryActionsPanelProps = {
files: ProjectRepoFile[];
identityPubkey?: string;
issues: ProjectIssue[];
onChatWithAgent: (items: ProjectSelectionItem[]) => void;
onCreateTask: () => void;
onCreatePullRequest?: () => void;
onOpenLocalRepository: () => void;
onOpenTerminal: () => void;
onRepositoryChange: (repositoryId: string) => void;
@@ -66,6 +77,8 @@ type ProjectRepositoryActionsPanelProps = {
project: Project;
projects: Project[];
repository: Repository;
selectedIssue?: ProjectIssue | null;
selectedPullRequest?: ProjectPullRequest | null;
snapshot: ProjectRepoSnapshot | null | undefined;
sourceControls: RepoSourceHeaderControls;
terminalTitle?: string;
@@ -82,13 +95,7 @@ function RepositoryPanelSection({
title?: string;
}) {
return (
<section
className={
divided
? "!mt-2 space-y-0.5 border-border/50 border-t pt-2"
: "space-y-0.5"
}
>
<section className={divided ? "!mt-2 space-y-0.5 pt-2" : "space-y-0.5"}>
{title ? (
<h3 className="flex h-7 min-w-0 items-center truncate text-sm font-normal text-muted-foreground/70">
{title}
@@ -155,7 +162,7 @@ function RepositoryActionButton({
}) {
return (
<Button
className="-mx-2 h-7 w-[calc(100%+1rem)] justify-start gap-3 rounded-md px-2 text-left text-sm font-normal hover:bg-muted/70 disabled:text-muted-foreground [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground"
className={PROJECT_CONTEXT_ACTION_BUTTON_CLASS}
disabled={disabled}
onClick={onClick}
size="sm"
@@ -177,7 +184,9 @@ export function ProjectRepositoryActionsPanel({
files,
identityPubkey,
issues,
onChatWithAgent,
onCreateTask,
onCreatePullRequest,
onOpenLocalRepository,
onOpenTerminal,
onRepositoryChange,
@@ -188,18 +197,30 @@ export function ProjectRepositoryActionsPanel({
project,
projects,
repository,
selectedIssue,
selectedPullRequest,
snapshot,
sourceControls,
terminalTitle,
widthPx,
}: ProjectRepositoryActionsPanelProps) {
const contextPeople = selectedIssue
? [selectedIssue.author, ...selectedIssue.assignees]
: selectedPullRequest
? [
selectedPullRequest.author,
...selectedPullRequest.reviewers,
...selectedPullRequest.approvals.map(({ author }) => author),
...selectedPullRequest.changeRequests.map(({ author }) => author),
]
: [repository.owner, ...repository.contributors];
const people = [
...new Set(
[repository.owner, ...repository.contributors]
.filter(Boolean)
.map(normalizePubkey),
),
...new Set(contextPeople.filter(Boolean).map(normalizePubkey)),
];
const selection = useProjectSelection();
const selectionPresentation = projectSelectionPresentation(
selection?.items ?? [],
);
const latestCommit = snapshot?.latestCommit ?? null;
const scope = projectRightPanelScope(activeTab);
const branchScoped = scope === "branch";
@@ -226,6 +247,7 @@ export function ProjectRepositoryActionsPanel({
return (
<RightAuxiliaryPane
canResetWidth={canResetWidth}
constrainToAvailableSpace={!detached}
detached={detached}
onResetWidth={onResetWidth}
onResizeStart={onResizeStart}
@@ -243,27 +265,54 @@ export function ProjectRepositoryActionsPanel({
className={`rounded-2xl ${detached ? "bg-background" : "border border-border/60 bg-muted/30"}`}
data-testid="project-context-card"
>
<div className="flex min-w-0 items-center justify-between gap-2 px-4 pt-3">
<h2 className="min-w-0 truncate text-sm font-normal text-muted-foreground/70">
{repository.name}
</h2>
{branchScoped ? (
<div
className="flex shrink-0 items-center"
data-testid="project-repository-management-actions"
>
<ProjectRepositoryManagement
compact
identityPubkey={identityPubkey}
onChange={onRepositoryChange}
project={project}
projects={projects}
repository={repository}
/>
</div>
) : null}
<div
className={`flex min-w-0 items-center justify-between gap-2 px-4 pt-3 ${
selectionPresentation ? "pb-3" : ""
}`}
>
{selectionPresentation && selection ? (
<ProjectsSelectionCountMenu
onChatWithAgent={onChatWithAgent}
onCreatePullRequest={onCreatePullRequest}
presentation={selectionPresentation}
selectionItems={selection.items}
/>
) : (
<>
<h2 className="min-w-0 truncate text-sm font-normal text-muted-foreground/70">
{selectedIssue?.title ??
selectedPullRequest?.title ??
repository.name}
</h2>
{branchScoped ? (
<div
className="flex shrink-0 items-center"
data-testid="project-repository-management-actions"
>
<ProjectRepositoryManagement
compact
identityPubkey={identityPubkey}
onChange={onRepositoryChange}
project={project}
projects={projects}
repository={repository}
/>
</div>
) : null}
</>
)}
</div>
<div className="space-y-0.5 px-4 pb-3 pt-2">
<div
className={`space-y-0.5 px-4 pb-3 pt-2 ${
selectionPresentation ? "hidden" : ""
}`}
>
<ProjectWorkItemContextActions
issue={selectedIssue}
profiles={profiles}
pullRequest={selectedPullRequest}
repository={repository}
/>
{branchScoped ? (
<RepositoryPanelSection>
<div className="grid gap-0.5 [&_button]:-mx-2 [&_button]:h-7 [&_button]:w-[calc(100%+1rem)] [&_button]:max-w-none [&_button]:justify-start [&_button]:gap-3 [&_button]:rounded-md [&_button]:border-0 [&_button]:bg-transparent [&_button]:px-2 [&_button]:text-left [&_button]:text-sm [&_button]:font-normal [&_button]:shadow-none [&_button]:hover:bg-muted/70 [&_button>svg:last-child]:ml-auto">
@@ -284,7 +333,7 @@ export function ProjectRepositoryActionsPanel({
/>
</div>
</RepositoryPanelSection>
) : activeTab === "issues" ? (
) : activeTab === "issues" && !selectionPresentation ? (
<RepositoryPanelSection>
<RepositoryActionButton
disabled={createIssuePending}
@@ -446,21 +495,37 @@ export function ProjectRepositoryActionsPanel({
<>
<RepositoryPanelSection
divided={activeTab === "issues"}
title={activeTab === "issues" ? "Details" : undefined}
title={
selectedIssue || selectedPullRequest
? "People"
: activeTab === "issues"
? "Details"
: undefined
}
>
<RepositoryPeople people={people} profiles={profiles} />
</RepositoryPanelSection>
<RepositoryPanelSection
title={
activeTab === "issues"
? undefined
: activeTab === "prs"
? "Review activity"
: "Repository activity"
selectedIssue
? "Task details"
: selectedPullRequest
? "Review details"
: activeTab === "issues"
? undefined
: activeTab === "prs"
? "Review activity"
: "Repository activity"
}
>
<dl className="text-sm [&>div]:h-7 [&_dd]:ml-auto [&_dd]:tabular-nums [&_dt]:gap-3 [&_dt]:text-foreground [&_dt_svg]:h-4 [&_dt_svg]:w-4 [&_dt_svg]:shrink-0 [&_dt_svg]:text-muted-foreground">
{activeTab !== "prs" ? (
<ProjectWorkItemContextDetails
issue={selectedIssue}
pullRequest={selectedPullRequest}
/>
{!selectedIssue &&
!selectedPullRequest &&
activeTab !== "prs" ? (
<>
<div className="flex items-center justify-between gap-3">
<dt className="flex items-center gap-3 text-muted-foreground">
@@ -497,7 +562,9 @@ export function ProjectRepositoryActionsPanel({
) : null}
</>
) : null}
{activeTab !== "issues" ? (
{!selectedIssue &&
!selectedPullRequest &&
activeTab !== "issues" ? (
<>
<div className="flex items-center justify-between gap-3">
<dt className="flex items-center gap-3 text-muted-foreground">