Move reviewer/approver pickers to inline header pills

Extract execution participant pickers from sidebar PropertyPicker rows into
compact pill-style Popover triggers in the issue header row, next to labels.
Creates a reusable ExecutionParticipantPicker component with matching
text-[10px] sizing. Removes the old sidebar rows and unused code.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-04-06 16:18:28 -05:00
parent 2e31fb7c91
commit be518529b7
3 changed files with 184 additions and 111 deletions

View file

@ -12,7 +12,6 @@ import { queryKeys } from "../lib/queryKeys";
import { useProjectOrder } from "../hooks/useProjectOrder";
import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees";
import { formatAssigneeUserLabel } from "../lib/assignees";
import { buildExecutionPolicy, stageParticipantValues } from "../lib/issue-execution-policy";
import { StatusIcon } from "./StatusIcon";
import { PriorityIcon } from "./PriorityIcon";
import { Identity } from "./Identity";
@ -270,45 +269,9 @@ export function IssueProperties({
const assignee = issue.assigneeAgentId
? agents?.find((a) => a.id === issue.assigneeAgentId)
: null;
const reviewerValues = stageParticipantValues(issue.executionPolicy, "review");
const approverValues = stageParticipantValues(issue.executionPolicy, "approval");
const userLabel = (userId: string | null | undefined) => formatAssigneeUserLabel(userId, currentUserId);
const assigneeUserLabel = userLabel(issue.assigneeUserId);
const creatorUserLabel = userLabel(issue.createdByUserId);
const updateExecutionPolicy = (nextReviewers: string[], nextApprovers: string[]) => {
onUpdate({
executionPolicy: buildExecutionPolicy({
existingPolicy: issue.executionPolicy ?? null,
reviewerValues: nextReviewers,
approverValues: nextApprovers,
}),
});
};
const toggleExecutionParticipant = (stageType: "review" | "approval", value: string) => {
const currentValues = stageType === "review" ? reviewerValues : approverValues;
const nextValues = currentValues.includes(value)
? currentValues.filter((candidate) => candidate !== value)
: [...currentValues, value];
updateExecutionPolicy(
stageType === "review" ? nextValues : reviewerValues,
stageType === "approval" ? nextValues : approverValues,
);
};
const executionParticipantLabel = (value: string) => {
if (value.startsWith("agent:")) {
return agentName(value.slice("agent:".length)) ?? value.slice("agent:".length, "agent:".length + 8);
}
if (value.startsWith("user:")) {
return userLabel(value.slice("user:".length)) ?? "User";
}
return value;
};
const reviewerTrigger = reviewerValues.length > 0
? <span className="text-sm truncate">{reviewerValues.map((value) => executionParticipantLabel(value)).join(", ")}</span>
: <span className="text-sm text-muted-foreground">None</span>;
const approverTrigger = approverValues.length > 0
? <span className="text-sm truncate">{approverValues.map((value) => executionParticipantLabel(value)).join(", ")}</span>
: <span className="text-sm text-muted-foreground">None</span>;
const currentExecutionLabel = (() => {
if (!issue.executionState?.currentStageType) return null;
const stageLabel = issue.executionState.currentStageType === "review" ? "Review" : "Approval";
@ -509,80 +472,6 @@ export function IssueProperties({
</>
);
const executionParticipantsContent = (
stageType: "review" | "approval",
values: string[],
search: string,
setSearch: (value: string) => void,
onClear: () => void,
) => (
<>
<input
className="w-full px-2 py-1.5 text-xs bg-transparent outline-none border-b border-border mb-1 placeholder:text-muted-foreground/50"
placeholder={`Search ${stageType === "review" ? "reviewers" : "approvers"}...`}
value={search}
onChange={(e) => setSearch(e.target.value)}
autoFocus={!inline}
/>
<div className="max-h-48 overflow-y-auto overscroll-contain">
<button
className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
values.length === 0 && "bg-accent",
)}
onClick={onClear}
>
No {stageType === "review" ? "reviewers" : "approvers"}
</button>
{currentUserId && (
<button
className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
values.includes(`user:${currentUserId}`) && "bg-accent",
)}
onClick={() => toggleExecutionParticipant(stageType, `user:${currentUserId}`)}
>
<User className="h-3 w-3 shrink-0 text-muted-foreground" />
Assign to me
</button>
)}
{issue.createdByUserId && issue.createdByUserId !== currentUserId && (
<button
className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
values.includes(`user:${issue.createdByUserId}`) && "bg-accent",
)}
onClick={() => toggleExecutionParticipant(stageType, `user:${issue.createdByUserId}`)}
>
<User className="h-3 w-3 shrink-0 text-muted-foreground" />
{creatorUserLabel ? creatorUserLabel : "Requester"}
</button>
)}
{sortedAgents
.filter((agent) => {
if (!search.trim()) return true;
return agent.name.toLowerCase().includes(search.toLowerCase());
})
.map((agent) => {
const encoded = `agent:${agent.id}`;
return (
<button
key={`${stageType}:${agent.id}`}
className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
values.includes(encoded) && "bg-accent",
)}
onClick={() => toggleExecutionParticipant(stageType, encoded)}
>
<AgentIcon icon={agent.icon} className="shrink-0 h-3 w-3 text-muted-foreground" />
{agent.name}
</button>
);
})}
</div>
</>
);
const projectTrigger = issue.projectId ? (
<>
<span