fix: suggest comment reassignment from recent commenter

This commit is contained in:
dotta
2026-03-20 06:05:05 -05:00
parent ee85028534
commit 4ffa2b15dc
4 changed files with 71 additions and 20 deletions

View File

@@ -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<string | null>(null);
const editorRef = useRef<MarkdownEditorRef>(null);
const attachInputRef = useRef<HTMLInputElement | null>(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);
}

View File

@@ -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");
});
});

View File

@@ -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<AssigneeSelection>): 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 };

View File

@@ -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<string, { runId: string; runAgentId: string | null }>();
@@ -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) {