From 8b344b25cc2d27b3fc902650a1424047cd1bbabc Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 13 Jun 2026 23:23:29 +0000 Subject: [PATCH] Set comment assignee from agent mentions Co-Authored-By: Paperclip --- ui/src/components/IssueChatThread.tsx | 15 +++++++++++++++ ui/src/components/IssueChatThreadClassic.tsx | 15 +++++++++++++++ ui/src/lib/interrupt-handoff.test.ts | 17 +++++++++++++++++ ui/src/lib/interrupt-handoff.ts | 10 ++++++++++ 4 files changed, 57 insertions(+) diff --git a/ui/src/components/IssueChatThread.tsx b/ui/src/components/IssueChatThread.tsx index 8d897730e56..0c96c409a91 100644 --- a/ui/src/components/IssueChatThread.tsx +++ b/ui/src/components/IssueChatThread.tsx @@ -117,6 +117,7 @@ import { computeComposerHandoffPreview, extractAgentMentionIds, findPlainAgentNameCandidate, + resolveMentionedAgentAssigneeValue, type HandoffAgentMention, } from "../lib/interrupt-handoff"; import { restoreSubmittedCommentDraft } from "../lib/comment-submit-draft"; @@ -3367,6 +3368,10 @@ const IssueChatComposer = forwardRef(null); const draftTimer = useRef | null>(null); const canAcceptFiles = Boolean(onImageUpload || onAttachImage); + const reassignOptionIds = useMemo( + () => new Set(reassignOptions.map((option) => option.id)), + [reassignOptions], + ); function queueViewportRestore(snapshot: ReturnType) { if (!snapshot) return; @@ -3408,6 +3413,16 @@ const IssueChatComposer = forwardRef resolveMentionedAgentAssigneeValue(body, reassignOptionIds), + [body, reassignOptionIds], + ); + + useEffect(() => { + if (!enableReassign || !firstMentionedAssigneeValue) return; + setReassignTarget(firstMentionedAssigneeValue); + }, [enableReassign, firstMentionedAssigneeValue]); + useEffect(() => { setPendingWorkMode(resolvedIssueWorkMode); }, [resolvedIssueWorkMode]); diff --git a/ui/src/components/IssueChatThreadClassic.tsx b/ui/src/components/IssueChatThreadClassic.tsx index 157e8e70a94..a4e9345b128 100644 --- a/ui/src/components/IssueChatThreadClassic.tsx +++ b/ui/src/components/IssueChatThreadClassic.tsx @@ -107,6 +107,7 @@ import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySel import { IssueThreadInteractionCardClassic } from "./IssueThreadInteractionCardClassic"; import { AgentIcon } from "./AgentIconPicker"; import { restoreSubmittedCommentDraft } from "../lib/comment-submit-draft"; +import { resolveMentionedAgentAssigneeValue } from "../lib/interrupt-handoff"; import { captureComposerViewportSnapshot, restoreComposerViewportSnapshot, @@ -3311,6 +3312,10 @@ const IssueChatComposer = forwardRef(null); const draftTimer = useRef | null>(null); const canAcceptFiles = Boolean(onImageUpload || onAttachImage); + const reassignOptionIds = useMemo( + () => new Set(reassignOptions.map((option) => option.id)), + [reassignOptions], + ); function queueViewportRestore(snapshot: ReturnType) { if (!snapshot) return; @@ -3352,6 +3357,16 @@ const IssueChatComposer = forwardRef resolveMentionedAgentAssigneeValue(body, reassignOptionIds), + [body, reassignOptionIds], + ); + + useEffect(() => { + if (!enableReassign || !firstMentionedAssigneeValue) return; + setReassignTarget(firstMentionedAssigneeValue); + }, [enableReassign, firstMentionedAssigneeValue]); + useEffect(() => { setUnassignedConfirmed(false); }, [reassignTarget]); diff --git a/ui/src/lib/interrupt-handoff.test.ts b/ui/src/lib/interrupt-handoff.test.ts index 45c1ff201d8..1c62b389cef 100644 --- a/ui/src/lib/interrupt-handoff.test.ts +++ b/ui/src/lib/interrupt-handoff.test.ts @@ -9,6 +9,7 @@ import { extractAgentMentionIds, findPlainAgentNameCandidate, isOperatorInterruptedRun, + resolveMentionedAgentAssigneeValue, resolveRunStatusPresentation, type PauseAffectsIssueLike, } from "./interrupt-handoff"; @@ -61,6 +62,22 @@ describe("structured mention vs plain text", () => { expect(bodyHasAgentMention(`hey ${qaMention}`)).toBe(true); }); + it("resolves the first available structured agent mention as the pending assignee", () => { + const coderHref = buildAgentMentionHref("agent-coder-2222", null); + const available = new Set([`agent:${QA_ID}`, "agent:agent-coder-2222"]); + expect( + resolveMentionedAgentAssigneeValue( + `ask ${qaMention} and [@Coder](${coderHref})`, + available, + ), + ).toBe(`agent:${QA_ID}`); + }); + + it("does not resolve plain text or unavailable mentioned agents as pending assignees", () => { + expect(resolveMentionedAgentAssigneeValue("ask QA to confirm", new Set([`agent:${QA_ID}`]))).toBeNull(); + expect(resolveMentionedAgentAssigneeValue(`ask ${qaMention}`, new Set(["agent:other"]))).toBeNull(); + }); + it("treats a plain QA name as NOT a mention", () => { expect(bodyHasAgentMention("QA you take the screenshot")).toBe(false); expect(extractAgentMentionIds("ask QA to confirm")).toEqual([]); diff --git a/ui/src/lib/interrupt-handoff.ts b/ui/src/lib/interrupt-handoff.ts index 6d71afc6c11..96c5f9b9995 100644 --- a/ui/src/lib/interrupt-handoff.ts +++ b/ui/src/lib/interrupt-handoff.ts @@ -105,6 +105,16 @@ export function bodyHasAgentMention(body: string): boolean { return extractAgentMentionIds(body).length > 0; } +export function resolveMentionedAgentAssigneeValue( + body: string, + availableAssigneeValues: ReadonlySet, +): string | null { + const agentId = extractAgentMentionIds(body)[0]; + if (!agentId) return null; + const assigneeValue = `agent:${agentId}`; + return availableAssigneeValues.has(assigneeValue) ? assigneeValue : null; +} + /** Strip every markdown link so chip labels/hrefs are not mistaken for plain text. */ function plainTextOutsideLinks(body: string): string { return body.replace(MARKDOWN_LINK_RE, " ");