feat: add pending edits functionality to ApprovalCard across all HITL tools

This commit is contained in:
Anish Sarkar 2026-03-20 21:38:19 +05:30
parent 4bd2071a8d
commit 9b38626723
9 changed files with 246 additions and 286 deletions

View file

@ -132,6 +132,9 @@ function ApprovalCard({
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{
subject: string; body: string; to: string; cc: string; bcc: string;
} | null>(null);
const accounts = interruptData.context?.accounts ?? [];
const validAccounts = accounts.filter((a) => !a.auth_expired);
@ -153,18 +156,26 @@ function ApprovalCard({
const handleApprove = useCallback(() => {
if (decided || isPanelOpen || !canApprove) return;
if (!allowedDecisions.includes("approve")) return;
setDecided("approve");
const isEdited = pendingEdits !== null;
setDecided(isEdited ? "edit" : "approve");
onDecision({
type: "approve",
type: isEdited ? "edit" : "approve",
edited_action: {
name: interruptData.action_requests[0].name,
args: {
...args,
...(pendingEdits && {
subject: pendingEdits.subject,
body: pendingEdits.body,
to: pendingEdits.to,
cc: pendingEdits.cc,
bcc: pendingEdits.bcc,
}),
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
},
},
});
}, [decided, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, args, selectedAccountId]);
}, [decided, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, args, selectedAccountId, pendingEdits]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -214,33 +225,24 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
const extraFields: ExtraField[] = [
{ key: "to", label: "To", type: "email", value: args.to || "" },
{ key: "cc", label: "CC", type: "email", value: args.cc || "" },
{ key: "bcc", label: "BCC", type: "email", value: args.bcc || "" },
{ key: "to", label: "To", type: "email", value: pendingEdits?.to ?? args.to ?? "" },
{ key: "cc", label: "CC", type: "email", value: pendingEdits?.cc ?? args.cc ?? "" },
{ key: "bcc", label: "BCC", type: "email", value: pendingEdits?.bcc ?? args.bcc ?? "" },
];
openHitlEditPanel({
title: args.subject ?? "",
content: args.body ?? "",
title: pendingEdits?.subject ?? (args.subject ?? ""),
content: pendingEdits?.body ?? (args.body ?? ""),
toolName: "Gmail Draft",
extraFields,
onSave: (newTitle, newContent, extraFieldValues) => {
setIsPanelOpen(false);
setDecided("edit");
const extras = extraFieldValues ?? {};
onDecision({
type: "edit",
edited_action: {
name: interruptData.action_requests[0].name,
args: {
...args,
subject: newTitle,
body: newContent,
to: extras.to ?? args.to,
cc: extras.cc ?? args.cc,
bcc: extras.bcc ?? args.bcc,
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
},
},
setPendingEdits({
subject: newTitle,
body: newContent,
to: extras.to ?? pendingEdits?.to ?? args.to ?? "",
cc: extras.cc ?? pendingEdits?.cc ?? args.cc ?? "",
bcc: extras.bcc ?? pendingEdits?.bcc ?? args.bcc ?? "",
});
},
});
@ -297,31 +299,31 @@ function ApprovalCard({
{/* Email headers + body preview */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3 pb-2 space-y-1.5 select-none">
{args.to && (
{(pendingEdits?.to ?? args.to) && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<UserIcon className="size-3 shrink-0" />
<span>To: {args.to}</span>
<span>To: {pendingEdits?.to ?? args.to}</span>
</div>
)}
{args.cc && args.cc.trim() !== "" && (
{(pendingEdits?.cc ?? args.cc) && (pendingEdits?.cc ?? args.cc)?.trim() !== "" && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<UsersIcon className="size-3 shrink-0" />
<span>CC: {args.cc}</span>
<span>CC: {pendingEdits?.cc ?? args.cc}</span>
</div>
)}
{args.bcc && args.bcc.trim() !== "" && (
{(pendingEdits?.bcc ?? args.bcc) && (pendingEdits?.bcc ?? args.bcc)?.trim() !== "" && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<UsersIcon className="size-3 shrink-0" />
<span>BCC: {args.bcc}</span>
<span>BCC: {pendingEdits?.bcc ?? args.bcc}</span>
</div>
)}
</div>
<div className="px-5 pt-1">
{args.subject != null && (
<p className="text-sm font-medium text-foreground">{args.subject}</p>
{(pendingEdits?.subject ?? args.subject) != null && (
<p className="text-sm font-medium text-foreground">{pendingEdits?.subject ?? args.subject}</p>
)}
{args.body != null && (
{(pendingEdits?.body ?? args.body) != null && (
<div
className="mt-2 max-h-[7rem] overflow-hidden text-sm"
style={{
@ -330,7 +332,7 @@ function ApprovalCard({
}}
>
<PlateEditor
markdown={String(args.body)}
markdown={String(pendingEdits?.body ?? args.body)}
readOnly
preset="readonly"
editorVariant="none"

View file

@ -133,6 +133,9 @@ function ApprovalCard({
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{
subject: string; body: string; to: string; cc: string; bcc: string;
} | null>(null);
const accounts = interruptData.context?.accounts ?? [];
const validAccounts = accounts.filter((a) => !a.auth_expired);
@ -154,18 +157,26 @@ function ApprovalCard({
const handleApprove = useCallback(() => {
if (decided || isPanelOpen || !canApprove) return;
if (!allowedDecisions.includes("approve")) return;
setDecided("approve");
const isEdited = pendingEdits !== null;
setDecided(isEdited ? "edit" : "approve");
onDecision({
type: "approve",
type: isEdited ? "edit" : "approve",
edited_action: {
name: interruptData.action_requests[0].name,
args: {
...args,
...(pendingEdits && {
subject: pendingEdits.subject,
body: pendingEdits.body,
to: pendingEdits.to,
cc: pendingEdits.cc,
bcc: pendingEdits.bcc,
}),
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
},
},
});
}, [decided, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, args, selectedAccountId]);
}, [decided, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, args, selectedAccountId, pendingEdits]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -216,33 +227,24 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
const extraFields: ExtraField[] = [
{ key: "to", label: "To", type: "email", value: args.to || "" },
{ key: "cc", label: "CC", type: "email", value: args.cc || "" },
{ key: "bcc", label: "BCC", type: "email", value: args.bcc || "" },
{ key: "to", label: "To", type: "email", value: pendingEdits?.to ?? args.to ?? "" },
{ key: "cc", label: "CC", type: "email", value: pendingEdits?.cc ?? args.cc ?? "" },
{ key: "bcc", label: "BCC", type: "email", value: pendingEdits?.bcc ?? args.bcc ?? "" },
];
openHitlEditPanel({
title: args.subject ?? "",
content: args.body ?? "",
title: pendingEdits?.subject ?? (args.subject ?? ""),
content: pendingEdits?.body ?? (args.body ?? ""),
toolName: "Send Email",
extraFields,
onSave: (newTitle, newContent, extraFieldValues) => {
setIsPanelOpen(false);
setDecided("edit");
const extras = extraFieldValues ?? {};
onDecision({
type: "edit",
edited_action: {
name: interruptData.action_requests[0].name,
args: {
...args,
subject: newTitle,
body: newContent,
to: extras.to ?? args.to,
cc: extras.cc ?? args.cc,
bcc: extras.bcc ?? args.bcc,
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
},
},
setPendingEdits({
subject: newTitle,
body: newContent,
to: extras.to ?? pendingEdits?.to ?? args.to ?? "",
cc: extras.cc ?? pendingEdits?.cc ?? args.cc ?? "",
bcc: extras.bcc ?? pendingEdits?.bcc ?? args.bcc ?? "",
});
},
});
@ -299,31 +301,31 @@ function ApprovalCard({
{/* Email headers + body preview */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3 pb-2 space-y-1.5 select-none">
{args.to && (
{(pendingEdits?.to ?? args.to) && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<UserIcon className="size-3 shrink-0" />
<span>To: {args.to}</span>
<span>To: {pendingEdits?.to ?? args.to}</span>
</div>
)}
{args.cc && args.cc.trim() !== "" && (
{(pendingEdits?.cc ?? args.cc) && (pendingEdits?.cc ?? args.cc)!.trim() !== "" && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<UsersIcon className="size-3 shrink-0" />
<span>CC: {args.cc}</span>
<span>CC: {pendingEdits?.cc ?? args.cc}</span>
</div>
)}
{args.bcc && args.bcc.trim() !== "" && (
{(pendingEdits?.bcc ?? args.bcc) && (pendingEdits?.bcc ?? args.bcc)!.trim() !== "" && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<UsersIcon className="size-3 shrink-0" />
<span>BCC: {args.bcc}</span>
<span>BCC: {pendingEdits?.bcc ?? args.bcc}</span>
</div>
)}
</div>
<div className="px-5 pt-1">
{args.subject != null && (
<p className="text-sm font-medium text-foreground">{args.subject}</p>
{(pendingEdits?.subject ?? args.subject) != null && (
<p className="text-sm font-medium text-foreground">{pendingEdits?.subject ?? args.subject}</p>
)}
{args.body != null && (
{(pendingEdits?.body ?? args.body) != null && (
<div
className="mt-2 max-h-[7rem] overflow-hidden text-sm"
style={{
@ -332,7 +334,7 @@ function ApprovalCard({
}}
>
<PlateEditor
markdown={String(args.body)}
markdown={String(pendingEdits?.body ?? args.body)}
readOnly
preset="readonly"
editorVariant="none"

View file

@ -160,6 +160,10 @@ function ApprovalCard({
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{
summary: string; description: string; start_datetime: string;
end_datetime: string; location: string; attendees: string;
} | null>(null);
const accounts = interruptData.context?.accounts ?? [];
const validAccounts = accounts.filter((a) => !a.auth_expired);
@ -189,24 +193,42 @@ function ApprovalCard({
const canApprove =
!!selectedAccountId &&
!!selectedCalendarId &&
!!args.summary?.trim();
!!(pendingEdits?.summary ?? args.summary)?.trim();
const handleApprove = useCallback(() => {
if (decided || isPanelOpen || !canApprove) return;
if (!allowedDecisions.includes("approve")) return;
setDecided("approve");
const isEdited = pendingEdits !== null;
setDecided(isEdited ? "edit" : "approve");
const finalArgs: Record<string, unknown> = {
...args,
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
calendar_id: selectedCalendarId || null,
};
if (pendingEdits) {
finalArgs.summary = pendingEdits.summary;
finalArgs.description = pendingEdits.description;
if (pendingEdits.start_datetime) finalArgs.start_datetime = pendingEdits.start_datetime;
if (pendingEdits.end_datetime) finalArgs.end_datetime = pendingEdits.end_datetime;
if (pendingEdits.location !== undefined) finalArgs.location = pendingEdits.location;
if (pendingEdits.attendees !== undefined) {
finalArgs.attendees = pendingEdits.attendees
.split(",")
.map((e) => e.trim())
.filter(Boolean);
}
}
onDecision({
type: "approve",
type: isEdited ? "edit" : "approve",
edited_action: {
name: interruptData.action_requests[0].name,
args: {
...args,
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
calendar_id: selectedCalendarId || null,
},
args: finalArgs,
},
});
}, [decided, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, args, selectedAccountId, selectedCalendarId]);
}, [decided, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, args, selectedAccountId, selectedCalendarId, pendingEdits]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -219,6 +241,9 @@ function ApprovalCard({
}, [handleApprove]);
const attendeesList = (args.attendees as string[]) ?? [];
const displayAttendees = pendingEdits?.attendees
? pendingEdits.attendees.split(",").map((e) => e.trim()).filter(Boolean)
: attendeesList;
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
@ -259,46 +284,26 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
const extraFields: ExtraField[] = [
{ key: "start_datetime", label: "Start", type: "datetime-local", value: args.start_datetime || "" },
{ key: "end_datetime", label: "End", type: "datetime-local", value: args.end_datetime || "" },
{ key: "location", label: "Location", type: "text", value: args.location || "" },
{ key: "attendees", label: "Attendees (comma-separated emails)", type: "text", value: attendeesList.join(", ") },
{ key: "start_datetime", label: "Start", type: "datetime-local", value: pendingEdits?.start_datetime ?? args.start_datetime ?? "" },
{ key: "end_datetime", label: "End", type: "datetime-local", value: pendingEdits?.end_datetime ?? args.end_datetime ?? "" },
{ key: "location", label: "Location", type: "text", value: pendingEdits?.location ?? args.location ?? "" },
{ key: "attendees", label: "Attendees (comma-separated emails)", type: "text", value: pendingEdits?.attendees ?? attendeesList.join(", ") },
];
openHitlEditPanel({
title: args.summary ?? "",
content: args.description ?? "",
title: pendingEdits?.summary ?? (args.summary ?? ""),
content: pendingEdits?.description ?? (args.description ?? ""),
toolName: "Calendar Event",
extraFields,
onSave: (newTitle, newContent, extraFieldValues) => {
setIsPanelOpen(false);
setDecided("edit");
const editedArgs: Record<string, unknown> = {
...args,
const extras = extraFieldValues ?? {};
setPendingEdits({
summary: newTitle,
description: newContent,
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
calendar_id: selectedCalendarId || null,
};
if (extraFieldValues) {
if (extraFieldValues.start_datetime) editedArgs.start_datetime = extraFieldValues.start_datetime;
if (extraFieldValues.end_datetime) editedArgs.end_datetime = extraFieldValues.end_datetime;
if (extraFieldValues.location !== undefined) editedArgs.location = extraFieldValues.location;
if (extraFieldValues.attendees !== undefined) {
editedArgs.attendees = extraFieldValues.attendees
.split(",")
.map((e) => e.trim())
.filter(Boolean);
}
}
onDecision({
type: "edit",
edited_action: {
name: interruptData.action_requests[0].name,
args: editedArgs,
},
start_datetime: extras.start_datetime ?? pendingEdits?.start_datetime ?? args.start_datetime ?? "",
end_datetime: extras.end_datetime ?? pendingEdits?.end_datetime ?? args.end_datetime ?? "",
location: extras.location ?? pendingEdits?.location ?? args.location ?? "",
attendees: extras.attendees ?? pendingEdits?.attendees ?? attendeesList.join(", "),
});
},
});
@ -385,36 +390,36 @@ function ApprovalCard({
{/* Content preview */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3 pb-3 space-y-2">
{args.summary && (
<p className="text-sm font-medium text-foreground">{args.summary}</p>
{(pendingEdits?.summary ?? args.summary) && (
<p className="text-sm font-medium text-foreground">{pendingEdits?.summary ?? args.summary}</p>
)}
{(args.start_datetime || args.end_datetime) && (
{((pendingEdits?.start_datetime ?? args.start_datetime) || (pendingEdits?.end_datetime ?? args.end_datetime)) && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<ClockIcon className="size-3.5 shrink-0" />
<span>
{args.start_datetime ? formatDateTime(args.start_datetime) : ""}
{args.start_datetime && args.end_datetime ? " — " : ""}
{args.end_datetime ? formatDateTime(args.end_datetime) : ""}
{(pendingEdits?.start_datetime ?? args.start_datetime) ? formatDateTime(pendingEdits?.start_datetime ?? args.start_datetime) : ""}
{(pendingEdits?.start_datetime ?? args.start_datetime) && (pendingEdits?.end_datetime ?? args.end_datetime) ? " — " : ""}
{(pendingEdits?.end_datetime ?? args.end_datetime) ? formatDateTime(pendingEdits?.end_datetime ?? args.end_datetime) : ""}
</span>
</div>
)}
{args.location && (
{(pendingEdits?.location ?? args.location) && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<MapPinIcon className="size-3.5 shrink-0" />
<span>{args.location}</span>
<span>{pendingEdits?.location ?? args.location}</span>
</div>
)}
{attendeesList.length > 0 && (
{displayAttendees.length > 0 && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<UsersIcon className="size-3.5 shrink-0" />
<span>{attendeesList.join(", ")}</span>
<span>{displayAttendees.join(", ")}</span>
</div>
)}
{args.description && (
{(pendingEdits?.description ?? args.description) && (
<div
className="mt-2 max-h-[7rem] overflow-hidden text-sm"
style={{
@ -423,7 +428,7 @@ function ApprovalCard({
}}
>
<PlateEditor
markdown={String(args.description)}
markdown={String(pendingEdits?.description ?? args.description)}
readOnly
preset="readonly"
editorVariant="none"

View file

@ -171,6 +171,10 @@ function ApprovalCard({
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{
summary: string; description: string; start_datetime: string;
end_datetime: string; location: string; attendees: string;
} | null>(null);
const reviewConfig = interruptData.review_configs[0];
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
@ -216,6 +220,22 @@ function ApprovalCard({
String(actionArgs.new_description ?? "") !== (event?.description ?? "");
const buildFinalArgs = useCallback(() => {
if (pendingEdits) {
const attendeesArr = pendingEdits.attendees
? pendingEdits.attendees.split(",").map((e) => e.trim()).filter(Boolean)
: null;
return {
event_id: event?.event_id,
document_id: event?.document_id,
connector_id: account?.id,
new_summary: pendingEdits.summary || null,
new_description: pendingEdits.description || null,
new_start_datetime: pendingEdits.start_datetime || null,
new_end_datetime: pendingEdits.end_datetime || null,
new_location: pendingEdits.location || null,
new_attendees: attendeesArr,
};
}
return {
event_id: event?.event_id,
document_id: event?.document_id,
@ -227,20 +247,21 @@ function ApprovalCard({
new_location: actionArgs.new_location ?? null,
new_attendees: proposedAttendees ?? null,
};
}, [event, account, actionArgs, proposedAttendees]);
}, [event, account, actionArgs, proposedAttendees, pendingEdits]);
const handleApprove = useCallback(() => {
if (decided || isPanelOpen) return;
if (!allowedDecisions.includes("approve")) return;
setDecided("approve");
const isEdited = pendingEdits !== null;
setDecided(isEdited ? "edit" : "approve");
onDecision({
type: "approve",
type: isEdited ? "edit" : "approve",
edited_action: {
name: interruptData.action_requests[0].name,
args: buildFinalArgs(),
},
});
}, [decided, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs]);
}, [decided, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs, pendingEdits]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -290,24 +311,18 @@ function ApprovalCard({
className="rounded-lg text-muted-foreground -mt-1 -mr-2"
onClick={() => {
setIsPanelOpen(true);
const proposedSummary = actionArgs.new_summary
? String(actionArgs.new_summary)
: (event?.summary ?? "");
const proposedDescription = actionArgs.new_description
? String(actionArgs.new_description)
: (event?.description ?? "");
const proposedStart = actionArgs.new_start_datetime
? String(actionArgs.new_start_datetime)
: (event?.start ?? "");
const proposedEnd = actionArgs.new_end_datetime
? String(actionArgs.new_end_datetime)
: (event?.end ?? "");
const proposedLocation = actionArgs.new_location !== undefined
? String(actionArgs.new_location ?? "")
: (event?.location ?? "");
const proposedAttendeesStr = proposedAttendees
? proposedAttendees.join(", ")
: currentAttendees.join(", ");
const proposedSummary = pendingEdits?.summary
?? (actionArgs.new_summary ? String(actionArgs.new_summary) : (event?.summary ?? ""));
const proposedDescription = pendingEdits?.description
?? (actionArgs.new_description ? String(actionArgs.new_description) : (event?.description ?? ""));
const proposedStart = pendingEdits?.start_datetime
?? (actionArgs.new_start_datetime ? String(actionArgs.new_start_datetime) : (event?.start ?? ""));
const proposedEnd = pendingEdits?.end_datetime
?? (actionArgs.new_end_datetime ? String(actionArgs.new_end_datetime) : (event?.end ?? ""));
const proposedLocation = pendingEdits?.location
?? (actionArgs.new_location !== undefined ? String(actionArgs.new_location ?? "") : (event?.location ?? ""));
const proposedAttendeesStr = pendingEdits?.attendees
?? (proposedAttendees ? proposedAttendees.join(", ") : currentAttendees.join(", "));
const extraFields: ExtraField[] = [
{ key: "start_datetime", label: "Start", type: "datetime-local", value: proposedStart },
@ -322,34 +337,14 @@ function ApprovalCard({
extraFields,
onSave: (newTitle, newContent, extraFieldValues) => {
setIsPanelOpen(false);
setDecided("edit");
const editedArgs: Record<string, unknown> = {
event_id: event?.event_id,
document_id: event?.document_id,
connector_id: account?.id,
new_summary: newTitle || null,
new_description: newContent || null,
};
if (extraFieldValues) {
editedArgs.new_start_datetime = extraFieldValues.start_datetime || null;
editedArgs.new_end_datetime = extraFieldValues.end_datetime || null;
editedArgs.new_location = extraFieldValues.location || null;
if (extraFieldValues.attendees !== undefined) {
editedArgs.new_attendees = extraFieldValues.attendees
.split(",")
.map((e) => e.trim())
.filter(Boolean);
}
}
onDecision({
type: "edit",
edited_action: {
name: interruptData.action_requests[0].name,
args: editedArgs,
},
const extras = extraFieldValues ?? {};
setPendingEdits({
summary: newTitle,
description: newContent,
start_datetime: extras.start_datetime ?? proposedStart,
end_datetime: extras.end_datetime ?? proposedEnd,
location: extras.location ?? proposedLocation,
attendees: extras.attendees ?? proposedAttendeesStr,
});
},
});

View file

@ -137,6 +137,7 @@ function ApprovalCard({
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{ name: string; content: string } | null>(null);
const accounts = interruptData.context?.accounts ?? [];
const validAccounts = accounts.filter(a => !a.auth_expired);
@ -164,10 +165,10 @@ function ApprovalCard({
const fileTypeLabel = FILE_TYPE_LABELS[selectedFileType] ?? FILE_TYPE_LABELS[args.file_type] ?? "Google Drive File";
const isNameValid = useMemo(
() => args.name && typeof args.name === "string" && args.name.trim().length > 0,
[args.name]
);
const isNameValid = useMemo(() => {
const name = pendingEdits?.name ?? args.name;
return name && typeof name === "string" && name.trim().length > 0;
}, [pendingEdits?.name, args.name]);
const canApprove = !!selectedAccountId && isNameValid;
@ -178,20 +179,22 @@ function ApprovalCard({
const handleApprove = useCallback(() => {
if (decided || isPanelOpen || !canApprove) return;
if (!allowedDecisions.includes("approve")) return;
setDecided("approve");
const isEdited = pendingEdits !== null;
setDecided(isEdited ? "edit" : "approve");
onDecision({
type: "approve",
type: isEdited ? "edit" : "approve",
edited_action: {
name: interruptData.action_requests[0].name,
args: {
...args,
...(pendingEdits && { name: pendingEdits.name, content: pendingEdits.content }),
file_type: selectedFileType,
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
parent_folder_id: parentFolderId === "__root__" ? null : parentFolderId,
},
},
});
}, [decided, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, args, selectedFileType, selectedAccountId, parentFolderId]);
}, [decided, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, args, selectedFileType, selectedAccountId, parentFolderId, pendingEdits]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -239,27 +242,13 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
openHitlEditPanel({
title: args.name ?? "",
content: args.content ?? "",
title: pendingEdits?.name ?? (args.name ?? ""),
content: pendingEdits?.content ?? (args.content ?? ""),
toolName: fileTypeLabel,
onSave: (newName, newContent) => {
setIsPanelOpen(false);
setDecided("edit");
onDecision({
type: "edit",
edited_action: {
name: interruptData.action_requests[0].name,
args: {
...args,
name: newName,
content: newContent,
file_type: selectedFileType,
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
parent_folder_id: parentFolderId === "__root__" ? null : parentFolderId,
},
},
});
},
onSave: (newName, newContent) => {
setIsPanelOpen(false);
setPendingEdits({ name: newName, content: newContent });
},
});
}}
>
@ -355,10 +344,10 @@ function ApprovalCard({
{/* Content preview */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3">
{args.name != null && (
<p className="text-sm font-medium text-foreground">{args.name}</p>
{(pendingEdits?.name ?? args.name) != null && (
<p className="text-sm font-medium text-foreground">{String(pendingEdits?.name ?? args.name)}</p>
)}
{args.content != null && (
{(pendingEdits?.content ?? args.content) != null && (
<div
className="mt-2 max-h-[7rem] overflow-hidden text-sm"
style={{
@ -367,7 +356,7 @@ function ApprovalCard({
}}
>
<PlateEditor
markdown={String(args.content)}
markdown={String(pendingEdits?.content ?? args.content)}
readOnly
preset="readonly"
editorVariant="none"

View file

@ -149,6 +149,7 @@ function ApprovalCard({
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{ title: string; description: string } | null>(null);
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState("");
const [selectedTeamId, setSelectedTeamId] = useState("");
@ -171,7 +172,7 @@ function ApprovalCard({
[selectedWorkspace, selectedTeamId]
);
const isTitleValid = (args.title ?? "").trim().length > 0;
const isTitleValid = (pendingEdits?.title ?? args.title ?? "").trim().length > 0;
const canApprove = !!selectedWorkspaceId && !!selectedTeamId && isTitleValid;
const reviewConfig = interruptData.review_configs[0];
@ -180,8 +181,8 @@ function ApprovalCard({
const buildFinalArgs = useCallback((overrides?: { title?: string; description?: string }) => {
return {
title: overrides?.title ?? args.title,
description: overrides?.description ?? args.description ?? null,
title: overrides?.title ?? pendingEdits?.title ?? args.title,
description: overrides?.description ?? pendingEdits?.description ?? args.description ?? null,
connector_id: selectedWorkspaceId ? Number(selectedWorkspaceId) : null,
team_id: selectedTeamId || null,
state_id: selectedStateId === "__none__" ? null : selectedStateId,
@ -189,20 +190,21 @@ function ApprovalCard({
priority: Number(selectedPriority),
label_ids: selectedLabelIds,
};
}, [args.title, args.description, selectedWorkspaceId, selectedTeamId, selectedStateId, selectedAssigneeId, selectedPriority, selectedLabelIds]);
}, [args.title, args.description, selectedWorkspaceId, selectedTeamId, selectedStateId, selectedAssigneeId, selectedPriority, selectedLabelIds, pendingEdits]);
const handleApprove = useCallback(() => {
if (decided || isPanelOpen || !canApprove) return;
if (!allowedDecisions.includes("approve")) return;
setDecided("approve");
const isEdited = pendingEdits !== null;
setDecided(isEdited ? "edit" : "approve");
onDecision({
type: "approve",
type: isEdited ? "edit" : "approve",
edited_action: {
name: interruptData.action_requests[0].name,
args: buildFinalArgs(),
},
});
}, [decided, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, buildFinalArgs]);
}, [decided, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, buildFinalArgs, pendingEdits]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -250,19 +252,12 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
openHitlEditPanel({
title: args.title ?? "",
content: args.description ?? "",
title: pendingEdits?.title ?? (args.title ?? ""),
content: pendingEdits?.description ?? (args.description ?? ""),
toolName: "Linear Issue",
onSave: (newTitle, newDescription) => {
setIsPanelOpen(false);
setDecided("edit");
onDecision({
type: "edit",
edited_action: {
name: interruptData.action_requests[0].name,
args: buildFinalArgs({ title: newTitle, description: newDescription }),
},
});
setPendingEdits({ title: newTitle, description: newDescription });
},
});
}}
@ -461,10 +456,10 @@ function ApprovalCard({
{/* Content preview */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3">
{args.title != null && (
<p className="text-sm font-medium text-foreground">{args.title}</p>
{(pendingEdits?.title ?? args.title) != null && (
<p className="text-sm font-medium text-foreground">{pendingEdits?.title ?? args.title}</p>
)}
{args.description != null && (
{(pendingEdits?.description ?? args.description) != null && (
<div
className="max-h-[7rem] overflow-hidden text-sm"
style={{
@ -473,7 +468,7 @@ function ApprovalCard({
}}
>
<PlateEditor
markdown={args.description}
markdown={pendingEdits?.description ?? args.description ?? ""}
readOnly
preset="readonly"
editorVariant="none"

View file

@ -189,6 +189,7 @@ function ApprovalCard({
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const [editedArgs, setEditedArgs] = useState(initialEditState);
const [hasPanelEdits, setHasPanelEdits] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const reviewConfig = interruptData.review_configs[0];
@ -255,15 +256,16 @@ function ApprovalCard({
const handleApprove = useCallback(() => {
if (decided || isPanelOpen) return;
if (!allowedDecisions.includes("approve")) return;
setDecided("approve");
const isEdited = hasPanelEdits;
setDecided(isEdited ? "edit" : "approve");
onDecision({
type: "approve",
type: isEdited ? "edit" : "approve",
edited_action: {
name: interruptData.action_requests[0].name,
args: buildFinalArgs(),
},
});
}, [decided, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs]);
}, [decided, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs, hasPanelEdits]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -321,18 +323,7 @@ function ApprovalCard({
title: newTitle,
description: newDescription,
}));
setDecided("edit");
onDecision({
type: "edit",
edited_action: {
name: interruptData.action_requests[0].name,
args: {
...buildFinalArgs(),
new_title: newTitle || null,
new_description: newDescription || null,
},
},
});
setHasPanelEdits(true);
},
});
}}
@ -536,12 +527,12 @@ function ApprovalCard({
{/* Content preview — proposed changes */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3">
{hasProposedChanges ? (
{(hasProposedChanges || hasPanelEdits) ? (
<>
{actionArgs.new_title && (
<p className="text-sm font-medium text-foreground">{String(actionArgs.new_title)}</p>
{(hasPanelEdits ? editedArgs.title : actionArgs.new_title) && (
<p className="text-sm font-medium text-foreground">{String(hasPanelEdits ? editedArgs.title : actionArgs.new_title)}</p>
)}
{actionArgs.new_description && (
{(hasPanelEdits ? editedArgs.description : actionArgs.new_description) && (
<div
className="max-h-[7rem] overflow-hidden text-sm"
style={{
@ -550,7 +541,7 @@ function ApprovalCard({
}}
>
<PlateEditor
markdown={String(actionArgs.new_description)}
markdown={String(hasPanelEdits ? editedArgs.description : actionArgs.new_description)}
readOnly
preset="readonly"
editorVariant="none"

View file

@ -121,6 +121,7 @@ function ApprovalCard({
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{ title: string; content: string } | null>(null);
const accounts = interruptData.context?.accounts ?? [];
const validAccounts = accounts.filter(a => !a.auth_expired);
@ -144,8 +145,9 @@ function ApprovalCard({
}, [selectedAccountId, parentPages]);
const isTitleValid = useMemo(() => {
return args.title && typeof args.title === "string" && (args.title as string).trim().length > 0;
}, [args.title]);
const title = pendingEdits?.title ?? args.title;
return title && typeof title === "string" && title.trim().length > 0;
}, [pendingEdits?.title, args.title]);
const reviewConfig = interruptData.review_configs[0];
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
@ -154,20 +156,22 @@ function ApprovalCard({
const handleApprove = useCallback(() => {
if (decided || isPanelOpen || !selectedAccountId || !isTitleValid) return;
if (!allowedDecisions.includes("approve")) return;
setDecided("approve");
const isEdited = pendingEdits !== null;
setDecided(isEdited ? "edit" : "approve");
onDecision({
type: "approve",
type: isEdited ? "edit" : "approve",
edited_action: {
name: interruptData.action_requests[0].name,
args: {
...args,
...(pendingEdits && { title: pendingEdits.title, content: pendingEdits.content }),
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
parent_page_id:
selectedParentPageId === "__none__" ? null : selectedParentPageId,
},
},
});
}, [decided, isPanelOpen, selectedAccountId, isTitleValid, allowedDecisions, onDecision, interruptData, args, selectedParentPageId]);
}, [decided, isPanelOpen, selectedAccountId, isTitleValid, allowedDecisions, onDecision, interruptData, args, selectedParentPageId, pendingEdits]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -217,26 +221,12 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
openHitlEditPanel({
title: String(args.title ?? ""),
content: String(args.content ?? ""),
title: pendingEdits?.title ?? String(args.title ?? ""),
content: pendingEdits?.content ?? String(args.content ?? ""),
toolName: "Notion Page",
onSave: (newTitle, newContent) => {
setIsPanelOpen(false);
setDecided("edit");
onDecision({
type: "edit",
edited_action: {
name: interruptData.action_requests[0].name,
args: {
...args,
title: newTitle,
content: newContent,
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
parent_page_id:
selectedParentPageId === "__none__" ? null : selectedParentPageId,
},
},
});
setPendingEdits({ title: newTitle, content: newContent });
},
});
}}
@ -324,10 +314,10 @@ function ApprovalCard({
{/* Content preview */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3">
{args.title != null && (
<p className="text-sm font-medium text-foreground">{String(args.title)}</p>
{(pendingEdits?.title ?? args.title) != null && (
<p className="text-sm font-medium text-foreground">{String(pendingEdits?.title ?? args.title)}</p>
)}
{args.content != null && (
{(pendingEdits?.content ?? args.content) != null && (
<div
className="max-h-[7rem] overflow-hidden text-sm"
style={{
@ -336,7 +326,7 @@ function ApprovalCard({
}}
>
<PlateEditor
markdown={String(args.content)}
markdown={String(pendingEdits?.content ?? args.content)}
readOnly
preset="readonly"
editorVariant="none"

View file

@ -123,6 +123,7 @@ function ApprovalCard({
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{ content: string } | null>(null);
const account = interruptData.context?.account;
const currentTitle = interruptData.context?.current_title;
@ -134,19 +135,20 @@ function ApprovalCard({
const handleApprove = useCallback(() => {
if (decided || isPanelOpen) return;
if (!allowedDecisions.includes("approve")) return;
setDecided("approve");
const isEdited = pendingEdits !== null;
setDecided(isEdited ? "edit" : "approve");
onDecision({
type: "approve",
type: isEdited ? "edit" : "approve",
edited_action: {
name: interruptData.action_requests[0].name,
args: {
page_id: args.page_id,
content: args.content,
content: pendingEdits?.content ?? args.content,
connector_id: account?.id,
},
},
});
}, [decided, isPanelOpen, allowedDecisions, onDecision, interruptData, args, account?.id]);
}, [decided, isPanelOpen, allowedDecisions, onDecision, interruptData, args, account?.id, pendingEdits]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -195,22 +197,11 @@ function ApprovalCard({
setIsPanelOpen(true);
openHitlEditPanel({
title: currentTitle ?? "",
content: String(args.content ?? ""),
content: pendingEdits?.content ?? String(args.content ?? ""),
toolName: "Notion Page",
onSave: (_, newContent) => {
setIsPanelOpen(false);
setDecided("edit");
onDecision({
type: "edit",
edited_action: {
name: interruptData.action_requests[0].name,
args: {
page_id: args.page_id,
content: newContent,
connector_id: account?.id,
},
},
});
setPendingEdits({ content: newContent });
},
});
}}
@ -256,7 +247,7 @@ function ApprovalCard({
{/* Content preview */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3">
{args.content != null ? (
{(pendingEdits?.content ?? args.content) != null ? (
<div
className="max-h-[7rem] overflow-hidden text-sm"
style={{
@ -265,7 +256,7 @@ function ApprovalCard({
}}
>
<PlateEditor
markdown={String(args.content)}
markdown={String(pendingEdits?.content ?? args.content)}
readOnly
preset="readonly"
editorVariant="none"