Set comment assignee from agent mentions

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-16 10:02:04 +00:00
1 parent 3bf8cfd127
commit 8b344b25cc
4 files changed
+57

No files matched your search

+15
View File
@@ -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<IssueChatComposerHandle, IssueChatComposerP
const composerContainerRef = useRef<HTMLDivElement | null>(null);
const draftTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const canAcceptFiles = Boolean(onImageUpload || onAttachImage);
const reassignOptionIds = useMemo(
() => new Set(reassignOptions.map((option) => option.id)),
[reassignOptions],
);
function queueViewportRestore(snapshot: ReturnType<typeof captureComposerViewportSnapshot>) {
if (!snapshot) return;
@@ -3408,6 +3413,16 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
setReassignTarget(effectiveSuggestedAssigneeValue);
}, [effectiveSuggestedAssigneeValue]);
const firstMentionedAssigneeValue = useMemo(
() => resolveMentionedAgentAssigneeValue(body, reassignOptionIds),
[body, reassignOptionIds],
);
useEffect(() => {
if (!enableReassign || !firstMentionedAssigneeValue) return;
setReassignTarget(firstMentionedAssigneeValue);
}, [enableReassign, firstMentionedAssigneeValue]);
useEffect(() => {
setPendingWorkMode(resolvedIssueWorkMode);
}, [resolvedIssueWorkMode]);
@@ -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<IssueChatComposerHandle, IssueChatComposerP
const composerContainerRef = useRef<HTMLDivElement | null>(null);
const draftTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const canAcceptFiles = Boolean(onImageUpload || onAttachImage);
const reassignOptionIds = useMemo(
() => new Set(reassignOptions.map((option) => option.id)),
[reassignOptions],
);
function queueViewportRestore(snapshot: ReturnType<typeof captureComposerViewportSnapshot>) {
if (!snapshot) return;
@@ -3352,6 +3357,16 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
setReassignTarget(effectiveSuggestedAssigneeValue);
}, [effectiveSuggestedAssigneeValue]);
const firstMentionedAssigneeValue = useMemo(
() => resolveMentionedAgentAssigneeValue(body, reassignOptionIds),
[body, reassignOptionIds],
);
useEffect(() => {
if (!enableReassign || !firstMentionedAssigneeValue) return;
setReassignTarget(firstMentionedAssigneeValue);
}, [enableReassign, firstMentionedAssigneeValue]);
useEffect(() => {
setUnassignedConfirmed(false);
}, [reassignTarget]);
+17
View File
@@ -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([]);
+10
View File
@@ -105,6 +105,16 @@ export function bodyHasAgentMention(body: string): boolean {
return extractAgentMentionIds(body).length > 0;
}
export function resolveMentionedAgentAssigneeValue(
body: string,
availableAssigneeValues: ReadonlySet<string>,
): 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, " ");