diff --git a/ui/src/components/IssueProperties.tsx b/ui/src/components/IssueProperties.tsx
index 9f882ef8..937e4628 100644
--- a/ui/src/components/IssueProperties.tsx
+++ b/ui/src/components/IssueProperties.tsx
@@ -12,6 +12,7 @@ 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";
@@ -269,9 +270,45 @@ 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
+ ? {reviewerValues.map((value) => executionParticipantLabel(value)).join(", ")}
+ : None;
+ const approverTrigger = approverValues.length > 0
+ ? {approverValues.map((value) => executionParticipantLabel(value)).join(", ")}
+ : None;
const currentExecutionLabel = (() => {
if (!issue.executionState?.currentStageType) return null;
const stageLabel = issue.executionState.currentStageType === "review" ? "Review" : "Approval";
@@ -472,6 +509,80 @@ export function IssueProperties({
>
);
+ const executionParticipantsContent = (
+ stageType: "review" | "approval",
+ values: string[],
+ search: string,
+ setSearch: (value: string) => void,
+ onClear: () => void,
+ ) => (
+ <>
+ setSearch(e.target.value)}
+ autoFocus={!inline}
+ />
+
+
+ {currentUserId && (
+
+ )}
+ {issue.createdByUserId && issue.createdByUserId !== currentUserId && (
+
+ )}
+ {sortedAgents
+ .filter((agent) => {
+ if (!search.trim()) return true;
+ return agent.name.toLowerCase().includes(search.toLowerCase());
+ })
+ .map((agent) => {
+ const encoded = `agent:${agent.id}`;
+ return (
+
+ );
+ })}
+
+ >
+ );
+
const projectTrigger = issue.projectId ? (
<>
-
- option ? (
- {`Reviewer: ${option.label}`}
- ) : (
- Reviewer
- )
- }
- renderOption={(option) => {
- if (!option.id) return {option.label};
- const reviewer = parseAssigneeValue(option.id).assigneeAgentId
- ? (agents ?? []).find((agent) => agent.id === parseAssigneeValue(option.id).assigneeAgentId)
- : null;
- return (
- <>
- {reviewer ? : null}
- {option.label}
- >
- );
- }}
- />
-
- option ? (
- {`Approver: ${option.label}`}
- ) : (
- Approver
- )
- }
- renderOption={(option) => {
- if (!option.id) return {option.label};
- const approver = parseAssigneeValue(option.id).assigneeAgentId
- ? (agents ?? []).find((agent) => agent.id === parseAssigneeValue(option.id).assigneeAgentId)
- : null;
- return (
- <>
- {approver ? : null}
- {option.label}
- >
- );
- }}
- />
@@ -1538,7 +1482,7 @@ export function NewIssueDialog() {
multiple
/>
+
+ option ? (
+ <>
+
+ {option.label}
+ >
+ ) : (
+ <>
+
+ Reviewer
+ >
+ )
+ }
+ renderOption={(option) => {
+ if (!option.id) return {option.label};
+ const reviewer = parseAssigneeValue(option.id).assigneeAgentId
+ ? (agents ?? []).find((agent) => agent.id === parseAssigneeValue(option.id).assigneeAgentId)
+ : null;
+ return (
+ <>
+ {reviewer ? : null}
+ {option.label}
+ >
+ );
+ }}
+ />
+
+
+ option ? (
+ <>
+
+ {option.label}
+ >
+ ) : (
+ <>
+
+ Approver
+ >
+ )
+ }
+ renderOption={(option) => {
+ if (!option.id) return {option.label};
+ const approver = parseAssigneeValue(option.id).assigneeAgentId
+ ? (agents ?? []).find((agent) => agent.id === parseAssigneeValue(option.id).assigneeAgentId)
+ : null;
+ return (
+ <>
+ {approver ? : null}
+ {option.label}
+ >
+ );
+ }}
+ />
+
{/* More (dates) */}
diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx
index 8b11d5de..05fb5af8 100644
--- a/ui/src/pages/IssueDetail.tsx
+++ b/ui/src/pages/IssueDetail.tsx
@@ -43,7 +43,6 @@ import { InlineEditor } from "../components/InlineEditor";
import { CommentThread } from "../components/CommentThread";
import { IssueDocumentsSection } from "../components/IssueDocumentsSection";
import { IssueProperties } from "../components/IssueProperties";
-import { ExecutionParticipantPicker } from "../components/ExecutionParticipantPicker";
import { IssueWorkspaceCard } from "../components/IssueWorkspaceCard";
import { LiveRunWidget } from "../components/LiveRunWidget";
import type { MentionOption } from "../components/MarkdownEditor";
@@ -1356,23 +1355,6 @@ export function IssueDetail() {
)}
-
- updateIssue.mutate(data)}
- />
- updateIssue.mutate(data)}
- />
-
-