Add issue comment interrupt support

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-03-28 10:34:36 -05:00
parent cfb7dd4818
commit 4226e15128
5 changed files with 179 additions and 35 deletions

View file

@ -37,7 +37,12 @@ interface CommentThreadProps {
linkedRuns?: LinkedRunItem[];
companyId?: string | null;
projectId?: string | null;
onAdd: (body: string, reopen?: boolean, reassignment?: CommentReassignment) => Promise<void>;
onAdd: (
body: string,
reopen?: boolean,
reassignment?: CommentReassignment,
interrupt?: boolean,
) => Promise<void>;
issueStatus?: string;
agentMap?: Map<string, Agent>;
imageUploadHandler?: (file: File) => Promise<string>;
@ -50,6 +55,7 @@ interface CommentThreadProps {
currentAssigneeValue?: string;
suggestedAssigneeValue?: string;
mentions?: MentionOption[];
interruptAvailable?: boolean;
}
const DRAFT_DEBOUNCE_MS = 800;
@ -279,9 +285,11 @@ export function CommentThread({
currentAssigneeValue = "",
suggestedAssigneeValue,
mentions: providedMentions,
interruptAvailable = false,
}: CommentThreadProps) {
const [body, setBody] = useState("");
const [reopen, setReopen] = useState(true);
const [interrupt, setInterrupt] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [attaching, setAttaching] = useState(false);
const effectiveSuggestedAssigneeValue = suggestedAssigneeValue ?? currentAssigneeValue;
@ -351,6 +359,14 @@ export function CommentThread({
setReassignTarget(effectiveSuggestedAssigneeValue);
}, [effectiveSuggestedAssigneeValue]);
const interruptVisible = interruptAvailable && body.trim().length > 0;
useEffect(() => {
if (!interruptVisible && interrupt) {
setInterrupt(false);
}
}, [interruptVisible, interrupt]);
// Scroll to comment when URL hash matches #comment-{id}
useEffect(() => {
const hash = location.hash;
@ -377,10 +393,11 @@ export function CommentThread({
setSubmitting(true);
try {
await onAdd(trimmed, reopen ? true : undefined, reassignment ?? undefined);
await onAdd(trimmed, reopen ? true : undefined, reassignment ?? undefined, interrupt ? true : undefined);
setBody("");
if (draftKey) clearDraft(draftKey);
setReopen(true);
setInterrupt(false);
setReassignTarget(effectiveSuggestedAssigneeValue);
} catch {
// Parent mutation handlers surface the failure and keep the draft intact.
@ -465,6 +482,17 @@ export function CommentThread({
/>
Re-open
</label>
{interruptVisible && (
<label className="flex items-center gap-1.5 text-xs text-red-700 dark:text-red-300 cursor-pointer select-none">
<input
type="checkbox"
checked={interrupt}
onChange={(e) => setInterrupt(e.target.checked)}
className="rounded border-red-300 accent-red-600"
/>
Interrupt
</label>
)}
{enableReassign && reassignOptions.length > 0 && (
<InlineEntitySelector
value={reassignTarget}

View file

@ -275,6 +275,8 @@ export function IssueDetail() {
});
const hasLiveRuns = (liveRuns ?? []).length > 0 || !!activeRun;
const hasRunningIssueRun =
activeRun?.status === "running" || (liveRuns ?? []).some((run) => run.status === "running");
const sourceBreadcrumb = useMemo(
() => readIssueDetailBreadcrumb(location.state, location.search) ?? { label: "Issues", href: "/issues" },
[location.state, location.search],
@ -505,8 +507,8 @@ export function IssueDetail() {
});
const addComment = useMutation({
mutationFn: ({ body, reopen }: { body: string; reopen?: boolean }) =>
issuesApi.addComment(issueId!, body, reopen),
mutationFn: ({ body, reopen, interrupt }: { body: string; reopen?: boolean; interrupt?: boolean }) =>
issuesApi.addComment(issueId!, body, reopen, interrupt),
onMutate: async ({ body, reopen }) => {
await queryClient.cancelQueries({ queryKey: queryKeys.issues.comments(issueId!) });
await queryClient.cancelQueries({ queryKey: queryKeys.issues.detail(issueId!) });
@ -572,10 +574,12 @@ export function IssueDetail() {
mutationFn: ({
body,
reopen,
interrupt,
reassignment,
}: {
body: string;
reopen?: boolean;
interrupt?: boolean;
reassignment: CommentReassignment;
}) =>
issuesApi.update(issueId!, {
@ -583,6 +587,7 @@ export function IssueDetail() {
assigneeAgentId: reassignment.assigneeAgentId,
assigneeUserId: reassignment.assigneeUserId,
...(reopen ? { status: "todo" } : {}),
...(interrupt ? { interrupt } : {}),
}),
onMutate: async ({ body, reopen, reassignment }) => {
await queryClient.cancelQueries({ queryKey: queryKeys.issues.comments(issueId!) });
@ -1171,12 +1176,13 @@ export function IssueDetail() {
currentAssigneeValue={actualAssigneeValue}
suggestedAssigneeValue={suggestedAssigneeValue}
mentions={mentionOptions}
onAdd={async (body, reopen, reassignment) => {
interruptAvailable={hasRunningIssueRun}
onAdd={async (body, reopen, reassignment, interrupt) => {
if (reassignment) {
await addCommentAndReassign.mutateAsync({ body, reopen, reassignment });
await addCommentAndReassign.mutateAsync({ body, reopen, reassignment, interrupt });
return;
}
await addComment.mutateAsync({ body, reopen });
await addComment.mutateAsync({ body, reopen, interrupt });
}}
imageUploadHandler={async (file) => {
const attachment = await uploadAttachment.mutateAsync(file);