diff --git a/ui/src/components/CommentThread.tsx b/ui/src/components/CommentThread.tsx index 8e042acf..0e97f31a 100644 --- a/ui/src/components/CommentThread.tsx +++ b/ui/src/components/CommentThread.tsx @@ -46,6 +46,7 @@ interface CommentThreadProps { enableReassign?: boolean; reassignOptions?: InlineEntityOption[]; currentAssigneeValue?: string; + suggestedAssigneeValue?: string; mentions?: MentionOption[]; } @@ -269,13 +270,15 @@ export function CommentThread({ enableReassign = false, reassignOptions = [], currentAssigneeValue = "", + suggestedAssigneeValue, mentions: providedMentions, }: CommentThreadProps) { const [body, setBody] = useState(""); const [reopen, setReopen] = useState(true); const [submitting, setSubmitting] = useState(false); const [attaching, setAttaching] = useState(false); - const [reassignTarget, setReassignTarget] = useState(currentAssigneeValue); + const effectiveSuggestedAssigneeValue = suggestedAssigneeValue ?? currentAssigneeValue; + const [reassignTarget, setReassignTarget] = useState(effectiveSuggestedAssigneeValue); const [highlightCommentId, setHighlightCommentId] = useState(null); const editorRef = useRef(null); const attachInputRef = useRef(null); @@ -337,8 +340,8 @@ export function CommentThread({ }, []); useEffect(() => { - setReassignTarget(currentAssigneeValue); - }, [currentAssigneeValue]); + setReassignTarget(effectiveSuggestedAssigneeValue); + }, [effectiveSuggestedAssigneeValue]); // Scroll to comment when URL hash matches #comment-{id} useEffect(() => { @@ -370,7 +373,7 @@ export function CommentThread({ setBody(""); if (draftKey) clearDraft(draftKey); setReopen(false); - setReassignTarget(currentAssigneeValue); + setReassignTarget(effectiveSuggestedAssigneeValue); } finally { setSubmitting(false); } diff --git a/ui/src/lib/assignees.test.ts b/ui/src/lib/assignees.test.ts index 1ce22ef7..59822ef6 100644 --- a/ui/src/lib/assignees.test.ts +++ b/ui/src/lib/assignees.test.ts @@ -4,6 +4,7 @@ import { currentUserAssigneeOption, formatAssigneeUserLabel, parseAssigneeValue, + suggestedCommentAssigneeValue, } from "./assignees"; describe("assignee selection helpers", () => { @@ -50,4 +51,27 @@ describe("assignee selection helpers", () => { expect(formatAssigneeUserLabel("local-board", "someone-else")).toBe("Board"); expect(formatAssigneeUserLabel("user-abcdef", "someone-else")).toBe("user-"); }); + + it("suggests the last non-me commenter without changing the actual assignee encoding", () => { + expect( + suggestedCommentAssigneeValue( + { assigneeUserId: "board-user" }, + [ + { authorUserId: "board-user" }, + { authorAgentId: "agent-123" }, + ], + "board-user", + ), + ).toBe("agent:agent-123"); + }); + + it("falls back to the actual assignee when there is no better commenter hint", () => { + expect( + suggestedCommentAssigneeValue( + { assigneeUserId: "board-user" }, + [{ authorUserId: "board-user" }], + "board-user", + ), + ).toBe("user:board-user"); + }); }); diff --git a/ui/src/lib/assignees.ts b/ui/src/lib/assignees.ts index 274bcd40..c4df80a3 100644 --- a/ui/src/lib/assignees.ts +++ b/ui/src/lib/assignees.ts @@ -9,12 +9,40 @@ export interface AssigneeOption { searchText?: string; } +interface CommentAssigneeSuggestionInput { + assigneeAgentId?: string | null; + assigneeUserId?: string | null; +} + +interface CommentAssigneeSuggestionComment { + authorAgentId?: string | null; + authorUserId?: string | null; +} + export function assigneeValueFromSelection(selection: Partial): string { if (selection.assigneeAgentId) return `agent:${selection.assigneeAgentId}`; if (selection.assigneeUserId) return `user:${selection.assigneeUserId}`; return ""; } +export function suggestedCommentAssigneeValue( + issue: CommentAssigneeSuggestionInput, + comments: CommentAssigneeSuggestionComment[] | null | undefined, + currentUserId: string | null | undefined, +): string { + if (comments && comments.length > 0 && currentUserId) { + for (let i = comments.length - 1; i >= 0; i--) { + const comment = comments[i]; + if (comment.authorAgentId) return assigneeValueFromSelection({ assigneeAgentId: comment.authorAgentId }); + if (comment.authorUserId && comment.authorUserId !== currentUserId) { + return assigneeValueFromSelection({ assigneeUserId: comment.authorUserId }); + } + } + } + + return assigneeValueFromSelection(issue); +} + export function parseAssigneeValue(value: string): AssigneeSelection { if (!value) { return { assigneeAgentId: null, assigneeUserId: null }; diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index e90965f6..fbed1e9c 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -11,6 +11,7 @@ import { useCompany } from "../context/CompanyContext"; import { usePanel } from "../context/PanelContext"; import { useToast } from "../context/ToastContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; +import { assigneeValueFromSelection, suggestedCommentAssigneeValue } from "../lib/assignees"; import { queryKeys } from "../lib/queryKeys"; import { readIssueDetailBreadcrumb } from "../lib/issueDetailBreadcrumb"; import { useProjectOrder } from "../hooks/useProjectOrder"; @@ -374,21 +375,15 @@ export function IssueDetail() { return options; }, [agents, currentUserId]); - const currentAssigneeValue = useMemo(() => { - // Default to the last commenter who is not "me" so the user doesn't - // accidentally reassign to themselves when commenting on their own issue. - if (comments && comments.length > 0 && currentUserId) { - for (let i = comments.length - 1; i >= 0; i--) { - const c = comments[i]; - if (c.authorAgentId) return `agent:${c.authorAgentId}`; - if (c.authorUserId && c.authorUserId !== currentUserId) - return `user:${c.authorUserId}`; - } - } - if (issue?.assigneeAgentId) return `agent:${issue.assigneeAgentId}`; - if (issue?.assigneeUserId) return `user:${issue.assigneeUserId}`; - return ""; - }, [issue?.assigneeAgentId, issue?.assigneeUserId, comments, currentUserId]); + const actualAssigneeValue = useMemo( + () => assigneeValueFromSelection(issue ?? {}), + [issue], + ); + + const suggestedAssigneeValue = useMemo( + () => suggestedCommentAssigneeValue(issue ?? {}, comments, currentUserId), + [issue, comments, currentUserId], + ); const commentsWithRunMeta = useMemo(() => { const runMetaByCommentId = new Map(); @@ -1011,7 +1006,8 @@ export function IssueDetail() { draftKey={`paperclip:issue-comment-draft:${issue.id}`} enableReassign reassignOptions={commentReassignOptions} - currentAssigneeValue={currentAssigneeValue} + currentAssigneeValue={actualAssigneeValue} + suggestedAssigneeValue={suggestedAssigneeValue} mentions={mentionOptions} onAdd={async (body, reopen, reassignment) => { if (reassignment) {