diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts
index 04ec0b35e54..90e97db13d5 100644
--- a/packages/shared/src/constants.ts
+++ b/packages/shared/src/constants.ts
@@ -157,6 +157,7 @@ export type IssueThreadInteractionStatus = (typeof ISSUE_THREAD_INTERACTION_STAT
export const ISSUE_THREAD_INTERACTION_CONTINUATION_POLICIES = [
"none",
"wake_assignee",
+ "wake_assignee_on_accept",
] as const;
export type IssueThreadInteractionContinuationPolicy =
(typeof ISSUE_THREAD_INTERACTION_CONTINUATION_POLICIES)[number];
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index 0514d182738..3fcf5e4a51f 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -317,6 +317,7 @@ export type {
AskUserQuestionsAnswer,
AskUserQuestionsResult,
RequestConfirmationIssueDocumentTarget,
+ RequestConfirmationCustomTarget,
RequestConfirmationTarget,
RequestConfirmationPayload,
RequestConfirmationResult,
@@ -595,6 +596,7 @@ export {
askUserQuestionsAnswerSchema,
askUserQuestionsResultSchema,
requestConfirmationIssueDocumentTargetSchema,
+ requestConfirmationCustomTargetSchema,
requestConfirmationTargetSchema,
requestConfirmationPayloadSchema,
requestConfirmationResultSchema,
diff --git a/packages/shared/src/issue-thread-interactions.test.ts b/packages/shared/src/issue-thread-interactions.test.ts
index 98cfc6a8fd1..2419353b39f 100644
--- a/packages/shared/src/issue-thread-interactions.test.ts
+++ b/packages/shared/src/issue-thread-interactions.test.ts
@@ -12,6 +12,7 @@ describe("issue thread interaction schemas", () => {
rejectLabel: "Revise",
rejectRequiresReason: true,
rejectReasonLabel: "What needs to change?",
+ declineReasonPlaceholder: "Optional: tell the agent what you'd change.",
detailsMarkdown: "The current plan document will be accepted as-is.",
supersedeOnUserComment: true,
},
@@ -26,6 +27,8 @@ describe("issue thread interaction schemas", () => {
rejectLabel: "Revise",
rejectRequiresReason: true,
rejectReasonLabel: "What needs to change?",
+ allowDeclineReason: true,
+ declineReasonPlaceholder: "Optional: tell the agent what you'd change.",
supersedeOnUserComment: true,
},
});
@@ -34,10 +37,11 @@ describe("issue thread interaction schemas", () => {
it("accepts issue document targets for request_confirmation interactions", () => {
const parsed = createIssueThreadInteractionSchema.parse({
kind: "request_confirmation",
- continuationPolicy: "wake_assignee",
+ continuationPolicy: "wake_assignee_on_accept",
payload: {
version: 1,
prompt: "Accept the latest plan revision?",
+ allowDeclineReason: false,
target: {
type: "issue_document",
issueId: "11111111-1111-4111-8111-111111111111",
@@ -45,6 +49,8 @@ describe("issue thread interaction schemas", () => {
key: "plan",
revisionId: "33333333-3333-4333-8333-333333333333",
revisionNumber: 2,
+ label: "Plan v2",
+ href: "/issues/PAP-123#document-plan",
},
},
});
@@ -55,6 +61,34 @@ describe("issue thread interaction schemas", () => {
type: "issue_document",
key: "plan",
revisionNumber: 2,
+ label: "Plan v2",
+ href: "/issues/PAP-123#document-plan",
+ });
+ });
+
+ it("accepts custom targets for request_confirmation interactions", () => {
+ const parsed = createIssueThreadInteractionSchema.parse({
+ kind: "request_confirmation",
+ payload: {
+ version: 1,
+ prompt: "Proceed with the external checklist?",
+ target: {
+ type: "custom",
+ key: "external-checklist",
+ revisionId: "checklist-v1",
+ revisionNumber: 1,
+ label: "Checklist v1",
+ href: "https://example.com/checklist",
+ },
+ },
+ });
+
+ expect(parsed.kind).toBe("request_confirmation");
+ if (parsed.kind !== "request_confirmation") return;
+ expect(parsed.payload.target).toMatchObject({
+ type: "custom",
+ key: "external-checklist",
+ label: "Checklist v1",
});
});
});
diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts
index 187ecf5d472..7a6433751ee 100644
--- a/packages/shared/src/types/index.ts
+++ b/packages/shared/src/types/index.ts
@@ -125,6 +125,7 @@ export type {
AskUserQuestionsAnswer,
AskUserQuestionsResult,
RequestConfirmationIssueDocumentTarget,
+ RequestConfirmationCustomTarget,
RequestConfirmationTarget,
RequestConfirmationPayload,
RequestConfirmationResult,
diff --git a/packages/shared/src/types/issue.ts b/packages/shared/src/types/issue.ts
index 08cacfc9b56..f105ab5c8ed 100644
--- a/packages/shared/src/types/issue.ts
+++ b/packages/shared/src/types/issue.ts
@@ -351,9 +351,22 @@ export interface RequestConfirmationIssueDocumentTarget {
key: string;
revisionId: string;
revisionNumber?: number | null;
+ label?: string | null;
+ href?: string | null;
}
-export type RequestConfirmationTarget = RequestConfirmationIssueDocumentTarget;
+export interface RequestConfirmationCustomTarget {
+ type: "custom";
+ key: string;
+ revisionId?: string | null;
+ revisionNumber?: number | null;
+ label?: string | null;
+ href?: string | null;
+}
+
+export type RequestConfirmationTarget =
+ | RequestConfirmationIssueDocumentTarget
+ | RequestConfirmationCustomTarget;
export interface RequestConfirmationPayload {
version: 1;
@@ -362,6 +375,8 @@ export interface RequestConfirmationPayload {
rejectLabel?: string | null;
rejectRequiresReason?: boolean;
rejectReasonLabel?: string | null;
+ allowDeclineReason?: boolean;
+ declineReasonPlaceholder?: string | null;
detailsMarkdown?: string | null;
supersedeOnUserComment?: boolean;
target?: RequestConfirmationTarget | null;
diff --git a/packages/shared/src/validators/index.ts b/packages/shared/src/validators/index.ts
index f2af85ea983..b7ae92fd058 100644
--- a/packages/shared/src/validators/index.ts
+++ b/packages/shared/src/validators/index.ts
@@ -155,6 +155,7 @@ export {
askUserQuestionsAnswerSchema,
askUserQuestionsResultSchema,
requestConfirmationIssueDocumentTargetSchema,
+ requestConfirmationCustomTargetSchema,
requestConfirmationTargetSchema,
requestConfirmationPayloadSchema,
requestConfirmationResultSchema,
diff --git a/packages/shared/src/validators/issue.ts b/packages/shared/src/validators/issue.ts
index 904c282d083..e47c02a7dbf 100644
--- a/packages/shared/src/validators/issue.ts
+++ b/packages/shared/src/validators/issue.ts
@@ -315,7 +315,17 @@ export const askUserQuestionsResultSchema = z.object({
summaryMarkdown: z.string().max(20000).nullable().optional(),
});
-export const requestConfirmationIssueDocumentTargetSchema = z.object({
+const requestConfirmationHrefSchema = z.string().trim().min(1).max(2000).refine((value) => {
+ const lower = value.toLowerCase();
+ return !lower.startsWith("javascript:") && !lower.startsWith("data:");
+}, "href must not use javascript: or data: URLs");
+
+const requestConfirmationTargetBaseSchema = z.object({
+ label: z.string().trim().min(1).max(120).nullable().optional(),
+ href: requestConfirmationHrefSchema.nullable().optional(),
+});
+
+export const requestConfirmationIssueDocumentTargetSchema = requestConfirmationTargetBaseSchema.extend({
type: z.literal("issue_document"),
issueId: z.string().uuid().nullable().optional(),
documentId: z.string().uuid().nullable().optional(),
@@ -324,8 +334,16 @@ export const requestConfirmationIssueDocumentTargetSchema = z.object({
revisionNumber: z.number().int().positive().nullable().optional(),
});
+export const requestConfirmationCustomTargetSchema = requestConfirmationTargetBaseSchema.extend({
+ type: z.literal("custom"),
+ key: z.string().trim().min(1).max(120),
+ revisionId: z.string().trim().min(1).max(255).nullable().optional(),
+ revisionNumber: z.number().int().positive().nullable().optional(),
+});
+
export const requestConfirmationTargetSchema = z.discriminatedUnion("type", [
requestConfirmationIssueDocumentTargetSchema,
+ requestConfirmationCustomTargetSchema,
]);
export const requestConfirmationPayloadSchema = z.object({
@@ -335,6 +353,8 @@ export const requestConfirmationPayloadSchema = z.object({
rejectLabel: z.string().trim().min(1).max(80).nullable().optional(),
rejectRequiresReason: z.boolean().optional(),
rejectReasonLabel: z.string().trim().min(1).max(160).nullable().optional(),
+ allowDeclineReason: z.boolean().optional().default(true),
+ declineReasonPlaceholder: z.string().trim().min(1).max(240).nullable().optional(),
detailsMarkdown: z.string().max(20000).nullable().optional(),
supersedeOnUserComment: z.boolean().optional(),
target: requestConfirmationTargetSchema.nullable().optional(),
diff --git a/server/src/__tests__/issue-thread-interaction-routes.test.ts b/server/src/__tests__/issue-thread-interaction-routes.test.ts
index 075e89b8560..8db09e587da 100644
--- a/server/src/__tests__/issue-thread-interaction-routes.test.ts
+++ b/server/src/__tests__/issue-thread-interaction-routes.test.ts
@@ -360,7 +360,7 @@ describe("issue thread interaction routes", () => {
);
});
- it("accepts request confirmations and wakes the current assignee when configured", async () => {
+ it("accepts request confirmations and wakes the current assignee when configured for accept-only wakeups", async () => {
mockInteractionService.acceptInteraction.mockResolvedValueOnce({
interaction: {
id: "interaction-3",
@@ -368,7 +368,7 @@ describe("issue thread interaction routes", () => {
issueId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
kind: "request_confirmation",
status: "accepted",
- continuationPolicy: "wake_assignee",
+ continuationPolicy: "wake_assignee_on_accept",
idempotencyKey: null,
sourceCommentId: null,
sourceRunId: "run-3",
@@ -414,7 +414,7 @@ describe("issue thread interaction routes", () => {
issueId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
kind: "request_confirmation",
status: "rejected",
- continuationPolicy: "wake_assignee",
+ continuationPolicy: "wake_assignee_on_accept",
idempotencyKey: null,
sourceCommentId: null,
sourceRunId: "run-3",
diff --git a/server/src/__tests__/issue-thread-interactions-service.test.ts b/server/src/__tests__/issue-thread-interactions-service.test.ts
index 960c605830e..a3290b6a7ae 100644
--- a/server/src/__tests__/issue-thread-interactions-service.test.ts
+++ b/server/src/__tests__/issue-thread-interactions-service.test.ts
@@ -754,6 +754,14 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
expect(accepted.interaction).toMatchObject({
id: created.id,
status: "expired",
+ payload: {
+ target: {
+ type: "issue_document",
+ key: "plan",
+ revisionId: nextRevisionId,
+ revisionNumber: 2,
+ },
+ },
result: {
version: 1,
outcome: "stale_target",
diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts
index 65fc74d8c57..dbd7c8b69c9 100644
--- a/server/src/routes/issues.ts
+++ b/server/src/routes/issues.ts
@@ -204,7 +204,10 @@ function queueResolvedInteractionContinuationWakeup(input: {
actor: { actorType: "user" | "agent"; actorId: string };
source: string;
}) {
- if (input.interaction.continuationPolicy !== "wake_assignee") return;
+ if (
+ input.interaction.continuationPolicy !== "wake_assignee"
+ && input.interaction.continuationPolicy !== "wake_assignee_on_accept"
+ ) return;
if (input.interaction.kind === "request_confirmation" && input.interaction.status !== "accepted") return;
if (input.interaction.status === "expired") return;
if (!input.issue.assigneeAgentId || isClosedIssueStatus(input.issue.status)) return;
diff --git a/server/src/services/issue-thread-interactions.ts b/server/src/services/issue-thread-interactions.ts
index 51a41fd1d42..86a05a671e8 100644
--- a/server/src/services/issue-thread-interactions.ts
+++ b/server/src/services/issue-thread-interactions.ts
@@ -250,9 +250,7 @@ async function getIssueDocumentTargetSnapshot(db: Db | any, args: {
issueId: string;
target: RequestConfirmationTarget;
}) {
- if (args.target.type !== "issue_document") {
- throw unprocessable(`Unsupported request confirmation target: ${(args.target as { type: string }).type}`);
- }
+ if (args.target.type !== "issue_document") return null;
const targetIssueId = args.target.issueId ?? args.issueId;
const row = await db
.select({
@@ -282,12 +280,49 @@ async function getIssueDocumentTargetSnapshot(db: Db | any, args: {
return row;
}
+function buildIssueDocumentTargetFromSnapshot(args: {
+ issueId: string;
+ snapshot: {
+ issueId: string;
+ documentId: string;
+ key: string;
+ latestRevisionId: string | null;
+ latestRevisionNumber: number;
+ } | null;
+}): RequestConfirmationTarget | null {
+ if (!args.snapshot?.latestRevisionId) return null;
+ return {
+ type: "issue_document",
+ issueId: args.snapshot.issueId ?? args.issueId,
+ documentId: args.snapshot.documentId,
+ key: args.snapshot.key,
+ revisionId: args.snapshot.latestRevisionId,
+ revisionNumber: args.snapshot.latestRevisionNumber,
+ };
+}
+
+function buildIssueDocumentTargetFromDocument(args: {
+ issueId: string;
+ document: { id: string; key: string; latestRevisionId?: string | null; latestRevisionNumber?: number | null } | null;
+}): RequestConfirmationTarget | null {
+ if (!args.document?.latestRevisionId) return null;
+ return {
+ type: "issue_document",
+ issueId: args.issueId,
+ documentId: args.document.id,
+ key: args.document.key,
+ revisionId: args.document.latestRevisionId,
+ revisionNumber: args.document.latestRevisionNumber ?? null,
+ };
+}
+
async function assertRequestConfirmationTargetIsCurrent(db: Db | any, args: {
companyId: string;
issueId: string;
target?: RequestConfirmationTarget | null;
}) {
if (!args.target) return;
+ if (args.target.type !== "issue_document") return;
const snapshot = await getIssueDocumentTargetSnapshot(db, {
companyId: args.companyId,
issueId: args.issueId,
@@ -309,6 +344,7 @@ async function expireStaleRequestConfirmationTarget(db: Db | any, args: {
const interaction = hydrateInteraction(args.row) as RequestConfirmationInteraction;
const target = interaction.payload.target ?? null;
if (!target) return null;
+ if (target.type !== "issue_document") return null;
const snapshot = await getIssueDocumentTargetSnapshot(db, {
companyId: args.row.companyId,
@@ -322,10 +358,20 @@ async function expireStaleRequestConfirmationTarget(db: Db | any, args: {
if (isCurrent) return null;
const now = new Date();
+ const currentTarget = buildIssueDocumentTargetFromSnapshot({
+ issueId: args.row.issueId,
+ snapshot,
+ });
const [updated] = await db
.update(issueThreadInteractions)
.set({
status: "expired",
+ payload: currentTarget
+ ? {
+ ...interaction.payload,
+ target: currentTarget,
+ }
+ : interaction.payload,
result: {
version: 1,
outcome: "stale_target",
@@ -907,11 +953,22 @@ export function issueThreadInteractionService(db: Db) {
const now = new Date();
const expired: IssueThreadInteraction[] = [];
for (const row of staleRows) {
- const target = (hydrateInteraction(row) as RequestConfirmationInteraction).payload.target ?? null;
+ const interaction = hydrateInteraction(row) as RequestConfirmationInteraction;
+ const target = interaction.payload.target ?? null;
+ const currentTarget = buildIssueDocumentTargetFromDocument({
+ issueId: issue.id,
+ document,
+ });
const [updated] = await db
.update(issueThreadInteractions)
.set({
status: "expired",
+ payload: currentTarget
+ ? {
+ ...interaction.payload,
+ target: currentTarget,
+ }
+ : interaction.payload,
result: {
version: 1,
outcome: "stale_target",
diff --git a/ui/src/components/IssueThreadInteractionCard.test.tsx b/ui/src/components/IssueThreadInteractionCard.test.tsx
index 8d7482f07f3..fe9d14d7183 100644
--- a/ui/src/components/IssueThreadInteractionCard.test.tsx
+++ b/ui/src/components/IssueThreadInteractionCard.test.tsx
@@ -9,6 +9,9 @@ import { ThemeProvider } from "../context/ThemeContext";
import { TooltipProvider } from "./ui/tooltip";
import {
pendingAskUserQuestionsInteraction,
+ commentExpiredRequestConfirmationInteraction,
+ disabledDeclineReasonRequestConfirmationInteraction,
+ failedRequestConfirmationInteraction,
pendingRequestConfirmationInteraction,
pendingSuggestedTasksInteraction,
staleTargetRequestConfirmationInteraction,
@@ -126,11 +129,17 @@ describe("IssueThreadInteractionCard", () => {
const saveButton = Array.from(host.querySelectorAll("button")).filter((button) =>
button.textContent?.includes("Request revisions"),
).at(-1);
- expect(saveButton?.hasAttribute("disabled")).toBe(true);
+ expect(saveButton?.hasAttribute("disabled")).toBe(false);
+
+ await act(async () => {
+ saveButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ });
+
expect(host.textContent).toContain("A decline reason is required.");
const textarea = host.querySelector("textarea") as HTMLTextAreaElement | null;
expect(textarea).toBeTruthy();
+ expect(textarea?.getAttribute("aria-invalid")).toBe("true");
await act(async () => {
const valueSetter = Object.getOwnPropertyDescriptor(
@@ -175,15 +184,75 @@ describe("IssueThreadInteractionCard", () => {
);
});
+ it("labels accept-only continuation policies in the card header", () => {
+ const host = renderCard({
+ interaction: {
+ ...pendingRequestConfirmationInteraction,
+ continuationPolicy: "wake_assignee_on_accept",
+ },
+ });
+
+ expect(host.textContent).toContain("Wakes on confirm");
+ });
+
it("renders request confirmation target links and stale-target expiry", () => {
const host = renderCard({
interaction: staleTargetRequestConfirmationInteraction,
});
- const targetLink = host.querySelector("a");
+ const targetLinks = host.querySelectorAll("a");
expect(host.textContent).toContain("Expired by target change");
- expect(host.textContent).toContain("Document: plan rev 3");
- expect(targetLink?.getAttribute("href")).toContain("#document-plan");
+ expect(host.textContent).toContain("Plan v3");
+ expect(host.textContent).toContain("Plan v4");
+ expect(targetLinks[0]?.getAttribute("href")).toContain("#document-plan");
+ expect(targetLinks[1]?.getAttribute("href")).toContain("#document-plan");
expect(host.textContent).not.toContain("Approve plan");
});
+
+ it("renders a jump link for confirmations expired by comment", () => {
+ const host = renderCard({
+ interaction: commentExpiredRequestConfirmationInteraction,
+ });
+
+ const jumpLink = Array.from(host.querySelectorAll("a")).find((link) =>
+ link.textContent?.includes("Jump to comment"),
+ );
+
+ expect(jumpLink?.getAttribute("href")).toBe(
+ "#comment-22222222-2222-4222-8222-222222222222",
+ );
+ });
+
+ it("declines immediately when decline reasons are disabled", async () => {
+ const onRejectInteraction = vi.fn(async () => undefined);
+ const host = renderCard({
+ interaction: disabledDeclineReasonRequestConfirmationInteraction,
+ onRejectInteraction,
+ });
+
+ const declineButton = Array.from(host.querySelectorAll("button")).find((button) =>
+ button.textContent?.includes("Keep it"),
+ );
+ expect(declineButton).toBeTruthy();
+
+ await act(async () => {
+ declineButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ });
+
+ expect(host.querySelector("textarea")).toBeNull();
+ expect(onRejectInteraction).toHaveBeenCalledWith(
+ expect.objectContaining({ kind: "request_confirmation" }),
+ undefined,
+ );
+ });
+
+ it("renders explicit copy for failed request confirmations", () => {
+ const host = renderCard({
+ interaction: failedRequestConfirmationInteraction,
+ });
+
+ expect(host.textContent).toContain(
+ "This request could not be resolved. Try again or create a new request.",
+ );
+ });
});
diff --git a/ui/src/components/IssueThreadInteractionCard.tsx b/ui/src/components/IssueThreadInteractionCard.tsx
index ee5686b97f7..c65f8b4f2f9 100644
--- a/ui/src/components/IssueThreadInteractionCard.tsx
+++ b/ui/src/components/IssueThreadInteractionCard.tsx
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import type { Agent } from "@paperclipai/shared";
-import { AlertTriangle, CheckCircle2, ChevronRight, CircleDashed, FileText, GitBranch, ListChecks, Loader2, MessageSquareQuote, XCircle } from "lucide-react";
+import { AlertTriangle, CheckCircle2, ChevronRight, CircleDashed, GitBranch, ListChecks, Loader2, MessageSquareQuote, XCircle } from "lucide-react";
import { Link } from "@/lib/router";
import { formatAssigneeUserLabel } from "../lib/assignees";
import {
@@ -12,6 +12,7 @@ import {
type AskUserQuestionsInteraction,
type IssueThreadInteraction,
type RequestConfirmationInteraction,
+ type RequestConfirmationTarget,
type SuggestTasksInteraction,
type SuggestTasksResultCreatedTask,
type SuggestedTaskDraft,
@@ -87,7 +88,7 @@ function interactionKindLabel(kind: IssueThreadInteraction["kind"]) {
case "ask_user_questions":
return "Ask user questions";
case "request_confirmation":
- return "Request confirmation";
+ return "Confirmation";
default:
return kind;
}
@@ -826,32 +827,69 @@ function AskUserQuestionsCard({
);
}
-function RequestConfirmationTargetLink({
+function requestConfirmationTargetLabel(target: RequestConfirmationTarget) {
+ if (target.label) return target.label;
+ const revision = target.revisionNumber ? ` v${target.revisionNumber}` : "";
+ if (target.type === "issue_document" && target.key === "plan") {
+ return `Plan${revision}`;
+ }
+ return `${target.key}${revision}`;
+}
+
+function requestConfirmationTargetHref({
interaction,
+ target,
}: {
interaction: RequestConfirmationInteraction;
+ target: RequestConfirmationTarget;
}) {
- const target = interaction.payload.target ?? interaction.result?.staleTarget ?? null;
- if (!target) return null;
-
+ if (target.href) return target.href;
if (target.type === "issue_document") {
const issueId = target.issueId ?? interaction.issueId;
+ return `/issues/${issueId}#document-${encodeURIComponent(target.key)}`;
+ }
+ return null;
+}
+
+function RequestConfirmationTargetChip({
+ interaction,
+ target,
+ tone = "default",
+}: {
+ interaction: RequestConfirmationInteraction;
+ target: RequestConfirmationTarget | null | undefined;
+ tone?: "default" | "subtle";
+}) {
+ if (!target) return null;
+
+ const href = requestConfirmationTargetHref({ interaction, target });
+ const className = cn(
+ "inline-flex max-w-full items-center gap-1.5 rounded-sm border px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.16em]",
+ tone === "default"
+ ? "border-border/70 bg-transparent text-foreground"
+ : "border-border/60 bg-transparent text-muted-foreground",
+ href && "transition-colors hover:border-sky-500/70 hover:bg-sky-500/10",
+ );
+ const content = (
+ <>
+
The request was confirmed and is now disabled in the thread.
+- {interaction.result?.reason || "No reason provided."} -
+ {interaction.result?.reason ? ( ++ {interaction.result.reason} ++ ) : null}
+
{expiredByComment ? "A board comment superseded this confirmation before it was resolved." : "The requested target changed before this confirmation was resolved."}
+ {expiredByComment && interaction.result?.commentId ? ( + + ) : null} + {expiredByTargetChange ? ( ++ This request could not be resolved. Try again or create a new request. +
+ ); + } + return null; } @@ -924,13 +989,22 @@ function RequestConfirmationCard({ const [rejecting, setRejecting] = useState(false); const [working, setWorking] = useState<"accept" | "reject" | null>(null); const [rejectReason, setRejectReason] = useState(interaction.result?.reason ?? ""); + const [rejectAttempted, setRejectAttempted] = useState(false); const [actionError, setActionError] = useState