diff --git a/ui/src/components/CommentThread.tsx b/ui/src/components/CommentThread.tsx index bf2ef6bb..7c9f0e6a 100644 --- a/ui/src/components/CommentThread.tsx +++ b/ui/src/components/CommentThread.tsx @@ -4,6 +4,7 @@ import type { IssueComment, Agent } from "@paperclip/shared"; import { Button } from "@/components/ui/button"; import { Paperclip } from "lucide-react"; import { Identity } from "./Identity"; +import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySelector"; import { MarkdownBody } from "./MarkdownBody"; import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./MarkdownEditor"; import { StatusBadge } from "./StatusBadge"; @@ -27,11 +28,6 @@ interface CommentReassignment { assigneeUserId: string | null; } -interface ReassignOption { - value: string; - label: string; -} - interface CommentThreadProps { comments: CommentWithRunMeta[]; linkedRuns?: LinkedRunItem[]; @@ -44,7 +40,8 @@ interface CommentThreadProps { draftKey?: string; liveRunSlot?: React.ReactNode; enableReassign?: boolean; - reassignOptions?: ReassignOption[]; + reassignOptions?: InlineEntityOption[]; + currentAssigneeValue?: string; mentions?: MentionOption[]; } @@ -80,8 +77,7 @@ function clearDraft(draftKey: string) { } function parseReassignment(target: string): CommentReassignment | null { - if (!target) return null; - if (target === "__none__") { + if (!target || target === "__none__") { return { assigneeAgentId: null, assigneeUserId: null }; } if (target.startsWith("agent:")) { @@ -111,14 +107,14 @@ export function CommentThread({ liveRunSlot, enableReassign = false, reassignOptions = [], + currentAssigneeValue = "", mentions: providedMentions, }: CommentThreadProps) { const [body, setBody] = useState(""); const [reopen, setReopen] = useState(true); const [submitting, setSubmitting] = useState(false); const [attaching, setAttaching] = useState(false); - const [reassign, setReassign] = useState(false); - const [reassignTarget, setReassignTarget] = useState(""); + const [reassignTarget, setReassignTarget] = useState(currentAssigneeValue); const editorRef = useRef(null); const attachInputRef = useRef(null); const draftTimer = useRef | null>(null); @@ -177,16 +173,14 @@ export function CommentThread({ }, []); useEffect(() => { - if (enableReassign) return; - setReassign(false); - setReassignTarget(""); - }, [enableReassign]); + setReassignTarget(currentAssigneeValue); + }, [currentAssigneeValue]); async function handleSubmit() { const trimmed = body.trim(); if (!trimmed) return; - const reassignment = reassign ? parseReassignment(reassignTarget) : null; - if (reassign && !reassignment) return; + const hasReassignment = enableReassign && reassignTarget !== currentAssigneeValue; + const reassignment = hasReassignment ? parseReassignment(reassignTarget) : null; setSubmitting(true); try { @@ -194,8 +188,7 @@ export function CommentThread({ setBody(""); if (draftKey) clearDraft(draftKey); setReopen(false); - setReassign(false); - setReassignTarget(""); + setReassignTarget(currentAssigneeValue); } finally { setSubmitting(false); } @@ -213,7 +206,7 @@ export function CommentThread({ } } - const canSubmit = !submitting && !!body.trim() && (!reassign || !!parseReassignment(reassignTarget)); + const canSubmit = !submitting && !!body.trim(); return (
@@ -308,61 +301,24 @@ export function CommentThread({ contentClassName="min-h-[60px] text-sm" />
- {(onAttachImage || enableReassign) && ( + {onAttachImage && (
- {onAttachImage && ( - <> - - - - )} - {enableReassign && ( -
- - -
- )} + +
)} {isClosed && ( @@ -376,6 +332,18 @@ export function CommentThread({ Re-open )} + {enableReassign && reassignOptions.length > 0 && ( + + )} diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 3d50dc38..6583dd99 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -273,30 +273,26 @@ export function IssueDetail() { .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); }, [allIssues, issue]); - const canReassignFromComment = Boolean( - issue?.assigneeUserId && - (issue.assigneeUserId === "local-board" || (currentUserId && issue.assigneeUserId === currentUserId)), - ); - const commentReassignOptions = useMemo(() => { - const options: Array<{ value: string; label: string }> = [{ value: "__none__", label: "No assignee" }]; + const options: Array<{ id: string; label: string; searchText?: string }> = []; const activeAgents = [...(agents ?? [])] .filter((agent) => agent.status !== "terminated") .sort((a, b) => a.name.localeCompare(b.name)); for (const agent of activeAgents) { - options.push({ value: `agent:${agent.id}`, label: agent.name }); + options.push({ id: `agent:${agent.id}`, label: agent.name }); } - if (issue?.createdByUserId && issue.createdByUserId !== issue.assigneeUserId) { - const requesterLabel = - issue.createdByUserId === "local-board" - ? "Board" - : currentUserId && issue.createdByUserId === currentUserId - ? "Me" - : issue.createdByUserId.slice(0, 8); - options.push({ value: `user:${issue.createdByUserId}`, label: `Requester (${requesterLabel})` }); + if (currentUserId) { + const label = currentUserId === "local-board" ? "Board" : "Me (Board)"; + options.push({ id: `user:${currentUserId}`, label }); } return options; - }, [agents, currentUserId, issue?.assigneeUserId, issue?.createdByUserId]); + }, [agents, currentUserId]); + + const currentAssigneeValue = useMemo(() => { + if (issue?.assigneeAgentId) return `agent:${issue.assigneeAgentId}`; + if (issue?.assigneeUserId) return `user:${issue.assigneeUserId}`; + return ""; + }, [issue?.assigneeAgentId, issue?.assigneeUserId]); const commentsWithRunMeta = useMemo(() => { const runMetaByCommentId = new Map(); @@ -744,8 +740,9 @@ export function IssueDetail() { issueStatus={issue.status} agentMap={agentMap} draftKey={`paperclip:issue-comment-draft:${issue.id}`} - enableReassign={canReassignFromComment} + enableReassign reassignOptions={commentReassignOptions} + currentAssigneeValue={currentAssigneeValue} mentions={mentionOptions} onAdd={async (body, reopen, reassignment) => { if (reassignment) {