chore: ran linting

This commit is contained in:
Anish Sarkar 2026-03-21 13:20:13 +05:30
parent 772150eb66
commit de8841fb86
110 changed files with 2673 additions and 1918 deletions

View file

@ -1,8 +1,12 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import {
Select,
@ -11,11 +15,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { useSetAtom } from "jotai";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
interface ConfluenceAccount {
id: number;
@ -108,9 +108,7 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
);
}
function isInsufficientPermissionsResult(
result: unknown,
): result is InsufficientPermissionsResult {
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
return (
typeof result === "object" &&
result !== null &&
@ -161,7 +159,7 @@ function ApprovalCard({
space_id: selectedSpaceId || null,
};
},
[args.title, args.content, selectedAccountId, selectedSpaceId, pendingEdits],
[args.title, args.content, selectedAccountId, selectedSpaceId, pendingEdits]
);
const handleApprove = useCallback(() => {
@ -177,7 +175,17 @@ function ApprovalCard({
args: buildFinalArgs(),
},
});
}, [phase, setProcessing, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, buildFinalArgs, pendingEdits]);
}, [
phase,
setProcessing,
isPanelOpen,
canApprove,
allowedDecisions,
onDecision,
interruptData,
buildFinalArgs,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -202,15 +210,16 @@ function ApprovalCard({
: "Create Confluence Page"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={pendingEdits ? "Creating page with your changes" : "Creating page"} size="sm" />
<TextShimmerLoader
text={pendingEdits ? "Creating page with your changes" : "Creating page"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits ? "Page created with your changes" : "Page created"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Page creation was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Page creation was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -225,8 +234,8 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
openHitlEditPanel({
title: pendingEdits?.title ?? (args.title ?? ""),
content: pendingEdits?.content ?? (args.content ?? ""),
title: pendingEdits?.title ?? args.title ?? "",
content: pendingEdits?.content ?? args.content ?? "",
toolName: "Confluence Page",
onSave: (newTitle, newContent) => {
setIsPanelOpen(false);
@ -290,10 +299,7 @@ function ApprovalCard({
<p className="text-xs font-medium text-muted-foreground">
Space <span className="text-destructive">*</span>
</p>
<Select
value={selectedSpaceId}
onValueChange={setSelectedSpaceId}
>
<Select value={selectedSpaceId} onValueChange={setSelectedSpaceId}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a space" />
</SelectTrigger>
@ -379,9 +385,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
All Confluence accounts expired
</p>
<p className="text-sm font-semibold text-destructive">All Confluence accounts expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -391,9 +395,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
);
}
function InsufficientPermissionsCard({
result,
}: { result: InsufficientPermissionsResult }) {
function InsufficientPermissionsCard({ result }: { result: InsufficientPermissionsResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
@ -474,7 +476,8 @@ export const CreateConfluencePageToolUI = makeAssistantToolUI<
}
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isInsufficientPermissionsResult(result)) return <InsufficientPermissionsCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
return <SuccessCard result={result as SuccessResult} />;

View file

@ -3,9 +3,9 @@
import { makeAssistantToolUI } from "@assistant-ui/react";
import { CornerDownLeftIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface InterruptResult {
@ -132,9 +132,7 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
);
}
function isInsufficientPermissionsResult(
result: unknown,
): result is InsufficientPermissionsResult {
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
return (
typeof result === "object" &&
result !== null &&
@ -174,7 +172,15 @@ function ApprovalCard({
},
},
});
}, [phase, setProcessing, onDecision, interruptData, page?.page_id, context?.account?.id, deleteFromKb]);
}, [
phase,
setProcessing,
onDecision,
interruptData,
page?.page_id,
context?.account?.id,
deleteFromKb,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -203,9 +209,7 @@ function ApprovalCard({
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">Page deleted</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Page deletion was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Page deletion was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -238,9 +242,7 @@ function ApprovalCard({
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm space-y-1">
<div className="font-medium">{page.page_title}</div>
{page.space_id && (
<div className="text-xs text-muted-foreground">
Space: {page.space_id}
</div>
<div className="text-xs text-muted-foreground">Space: {page.space_id}</div>
)}
</div>
</div>
@ -279,11 +281,7 @@ function ApprovalCard({
<>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button
size="sm"
className="rounded-lg gap-1.5"
onClick={handleApprove}
>
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
Approve
<CornerDownLeftIcon className="size-3 opacity-60" />
</Button>
@ -309,9 +307,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Confluence authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Confluence authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -321,9 +317,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
);
}
function InsufficientPermissionsCard({
result,
}: { result: InsufficientPermissionsResult }) {
function InsufficientPermissionsCard({ result }: { result: InsufficientPermissionsResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
@ -357,9 +351,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
Page not found
</p>
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Page not found</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -437,7 +429,8 @@ export const DeleteConfluencePageToolUI = makeAssistantToolUI<
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isInsufficientPermissionsResult(result)) return <InsufficientPermissionsCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isWarningResult(result)) return <WarningCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;

View file

@ -4,11 +4,11 @@ import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
interface InterruptResult {
__interrupt__: true;
@ -116,9 +116,7 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
);
}
function isInsufficientPermissionsResult(
result: unknown,
): result is InsufficientPermissionsResult {
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
return (
typeof result === "object" &&
result !== null &&
@ -169,8 +167,7 @@ function ApprovalCard({
const canEdit = allowedDecisions.includes("edit");
const hasProposedChanges =
actionArgs.new_title || args.new_title ||
actionArgs.new_content || args.new_content;
actionArgs.new_title || args.new_title || actionArgs.new_content || args.new_content;
const buildFinalArgs = useCallback(() => {
return {
@ -196,7 +193,16 @@ function ApprovalCard({
args: buildFinalArgs(),
},
});
}, [phase, setProcessing, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs, hasPanelEdits]);
}, [
phase,
setProcessing,
isPanelOpen,
allowedDecisions,
onDecision,
interruptData,
buildFinalArgs,
hasPanelEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -221,15 +227,16 @@ function ApprovalCard({
: "Update Confluence Page"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={hasPanelEdits ? "Updating page with your changes" : "Updating page"} size="sm" />
<TextShimmerLoader
text={hasPanelEdits ? "Updating page with your changes" : "Updating page"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{hasPanelEdits ? "Page updated with your changes" : "Page updated"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Page update was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Page update was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -293,7 +300,8 @@ function ApprovalCard({
className="max-h-[5rem] overflow-hidden text-xs text-muted-foreground"
style={{
maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to bottom, black 50%, transparent 100%)",
}}
>
<PlateEditor
@ -306,9 +314,7 @@ function ApprovalCard({
</div>
)}
{page.space_id && (
<div className="text-xs text-muted-foreground">
Space: {page.space_id}
</div>
<div className="text-xs text-muted-foreground">Space: {page.space_id}</div>
)}
</div>
</div>
@ -322,14 +328,18 @@ function ApprovalCard({
{/* Content preview — proposed changes */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3">
{(hasProposedChanges || hasPanelEdits) ? (
{hasProposedChanges || hasPanelEdits ? (
<>
{(hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title)) && (
<p className="text-sm font-medium text-foreground">
{String(hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title))}
{String(
hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title)
)}
</p>
)}
{(hasPanelEdits ? editedArgs.content : (actionArgs.new_content ?? args.new_content)) && (
{(hasPanelEdits
? editedArgs.content
: (actionArgs.new_content ?? args.new_content)) && (
<div
className="max-h-[7rem] overflow-hidden text-sm"
style={{
@ -338,7 +348,11 @@ function ApprovalCard({
}}
>
<PlateEditor
markdown={String(hasPanelEdits ? editedArgs.content : (actionArgs.new_content ?? args.new_content))}
markdown={String(
hasPanelEdits
? editedArgs.content
: (actionArgs.new_content ?? args.new_content)
)}
readOnly
preset="readonly"
editorVariant="none"
@ -393,9 +407,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Confluence authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Confluence authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -405,9 +417,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
);
}
function InsufficientPermissionsCard({
result,
}: { result: InsufficientPermissionsResult }) {
function InsufficientPermissionsCard({ result }: { result: InsufficientPermissionsResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
@ -441,9 +451,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
Page not found
</p>
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Page not found</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -509,7 +517,8 @@ export const UpdateConfluencePageToolUI = makeAssistantToolUI<
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isInsufficientPermissionsResult(result)) return <InsufficientPermissionsCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
return <SuccessCard result={result as SuccessResult} />;

View file

@ -1,13 +1,13 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
CornerDownLeftIcon,
Pen,
UserIcon,
UsersIcon,
} from "lucide-react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen, UserIcon, UsersIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import {
Select,
@ -16,11 +16,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useSetAtom } from "jotai";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface GmailAccount {
@ -132,7 +127,11 @@ function ApprovalCard({
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{
subject: string; body: string; to: string; cc: string; bcc: string;
subject: string;
body: string;
to: string;
cc: string;
bcc: string;
} | null>(null);
const accounts = interruptData.context?.accounts ?? [];
@ -175,7 +174,18 @@ function ApprovalCard({
},
},
});
}, [phase, isPanelOpen, canApprove, allowedDecisions, setProcessing, onDecision, interruptData, args, selectedAccountId, pendingEdits]);
}, [
phase,
isPanelOpen,
canApprove,
allowedDecisions,
setProcessing,
onDecision,
interruptData,
args,
selectedAccountId,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -200,16 +210,17 @@ function ApprovalCard({
? "Gmail Draft Approved"
: "Create Gmail Draft"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={pendingEdits ? "Creating draft with your changes" : "Creating draft"} size="sm" />
{phase === "processing" ? (
<TextShimmerLoader
text={pendingEdits ? "Creating draft with your changes" : "Creating draft"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits ? "Draft created with your changes" : "Draft created"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Draft creation was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Draft creation was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -225,13 +236,28 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
const extraFields: ExtraField[] = [
{ key: "to", label: "To", type: "emails", value: pendingEdits?.to ?? args.to ?? "" },
{ key: "cc", label: "CC", type: "emails", value: pendingEdits?.cc ?? args.cc ?? "" },
{ key: "bcc", label: "BCC", type: "emails", value: pendingEdits?.bcc ?? args.bcc ?? "" },
{
key: "to",
label: "To",
type: "emails",
value: pendingEdits?.to ?? args.to ?? "",
},
{
key: "cc",
label: "CC",
type: "emails",
value: pendingEdits?.cc ?? args.cc ?? "",
},
{
key: "bcc",
label: "BCC",
type: "emails",
value: pendingEdits?.bcc ?? args.bcc ?? "",
},
];
openHitlEditPanel({
title: pendingEdits?.subject ?? (args.subject ?? ""),
content: pendingEdits?.body ?? (args.body ?? ""),
title: pendingEdits?.subject ?? args.subject ?? "",
content: pendingEdits?.body ?? args.body ?? "",
toolName: "Gmail Draft",
extraFields,
onSave: (newTitle, newContent, extraFieldValues) => {
@ -322,7 +348,9 @@ function ApprovalCard({
<div className="px-5 pt-1">
{(pendingEdits?.subject ?? args.subject) != null && (
<p className="text-sm font-medium text-foreground">{pendingEdits?.subject ?? args.subject}</p>
<p className="text-sm font-medium text-foreground">
{pendingEdits?.subject ?? args.subject}
</p>
)}
{(pendingEdits?.body ?? args.body) != null && (
<div
@ -398,9 +426,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Gmail authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Gmail authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -1,14 +1,13 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
CornerDownLeftIcon,
MailIcon,
Pen,
UserIcon,
UsersIcon,
} from "lucide-react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, MailIcon, Pen, UserIcon, UsersIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import {
Select,
@ -17,11 +16,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useSetAtom } from "jotai";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface GmailAccount {
@ -132,7 +126,11 @@ function ApprovalCard({
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{
subject: string; body: string; to: string; cc: string; bcc: string;
subject: string;
body: string;
to: string;
cc: string;
bcc: string;
} | null>(null);
const accounts = interruptData.context?.accounts ?? [];
@ -175,7 +173,18 @@ function ApprovalCard({
},
},
});
}, [phase, isPanelOpen, canApprove, allowedDecisions, setProcessing, onDecision, interruptData, args, selectedAccountId, pendingEdits]);
}, [
phase,
isPanelOpen,
canApprove,
allowedDecisions,
setProcessing,
onDecision,
interruptData,
args,
selectedAccountId,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -200,16 +209,17 @@ function ApprovalCard({
? "Email Sending Approved"
: "Send Email"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={pendingEdits ? "Sending email with your changes" : "Sending email"} size="sm" />
{phase === "processing" ? (
<TextShimmerLoader
text={pendingEdits ? "Sending email with your changes" : "Sending email"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits ? "Email sent with your changes" : "Email sent"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Email sending was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Email sending was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -225,13 +235,28 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
const extraFields: ExtraField[] = [
{ key: "to", label: "To", type: "emails", value: pendingEdits?.to ?? args.to ?? "" },
{ key: "cc", label: "CC", type: "emails", value: pendingEdits?.cc ?? args.cc ?? "" },
{ key: "bcc", label: "BCC", type: "emails", value: pendingEdits?.bcc ?? args.bcc ?? "" },
{
key: "to",
label: "To",
type: "emails",
value: pendingEdits?.to ?? args.to ?? "",
},
{
key: "cc",
label: "CC",
type: "emails",
value: pendingEdits?.cc ?? args.cc ?? "",
},
{
key: "bcc",
label: "BCC",
type: "emails",
value: pendingEdits?.bcc ?? args.bcc ?? "",
},
];
openHitlEditPanel({
title: pendingEdits?.subject ?? (args.subject ?? ""),
content: pendingEdits?.body ?? (args.body ?? ""),
title: pendingEdits?.subject ?? args.subject ?? "",
content: pendingEdits?.body ?? args.body ?? "",
toolName: "Send Email",
extraFields,
onSave: (newTitle, newContent, extraFieldValues) => {
@ -264,32 +289,32 @@ function ApprovalCard({
<p className="text-sm text-destructive">{interruptData.context.error}</p>
) : (
accounts.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Gmail Account <span className="text-destructive">*</span>
</p>
<Select value={selectedAccountId} onValueChange={setSelectedAccountId}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an account" />
</SelectTrigger>
<SelectContent>
{validAccounts.map((account) => (
<SelectItem key={account.id} value={String(account.id)}>
{account.name}
</SelectItem>
))}
{expiredAccounts.map((a) => (
<div
key={a.id}
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
>
{a.name} (expired, retry after re-auth)
</div>
))}
</SelectContent>
</Select>
</div>
)
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Gmail Account <span className="text-destructive">*</span>
</p>
<Select value={selectedAccountId} onValueChange={setSelectedAccountId}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an account" />
</SelectTrigger>
<SelectContent>
{validAccounts.map((account) => (
<SelectItem key={account.id} value={String(account.id)}>
{account.name}
</SelectItem>
))}
{expiredAccounts.map((a) => (
<div
key={a.id}
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
>
{a.name} (expired, retry after re-auth)
</div>
))}
</SelectContent>
</Select>
</div>
)
)}
</div>
</>
@ -320,7 +345,9 @@ function ApprovalCard({
<div className="px-5 pt-1">
{(pendingEdits?.subject ?? args.subject) != null && (
<p className="text-sm font-medium text-foreground">{pendingEdits?.subject ?? args.subject}</p>
<p className="text-sm font-medium text-foreground">
{pendingEdits?.subject ?? args.subject}
</p>
)}
{(pendingEdits?.body ?? args.body) != null && (
<div
@ -396,9 +423,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Gmail authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Gmail authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -1,16 +1,11 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
CalendarIcon,
CornerDownLeftIcon,
MailIcon,
UserIcon,
} from "lucide-react";
import { CalendarIcon, CornerDownLeftIcon, MailIcon, UserIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface GmailAccount {
@ -192,14 +187,12 @@ function ApprovalCard({
? "Email Trash Approved"
: "Trash Email"}
</p>
{phase === "processing" ? (
{phase === "processing" ? (
<TextShimmerLoader text="Trashing email" size="sm" />
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">Email trashed</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Email trash was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Email trash was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -280,11 +273,7 @@ function ApprovalCard({
<>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button
size="sm"
className="rounded-lg gap-1.5"
onClick={handleApprove}
>
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
Approve
<CornerDownLeftIcon className="size-3 opacity-60" />
</Button>
@ -324,9 +313,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Gmail authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Gmail authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -1,20 +1,14 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
CornerDownLeftIcon,
MailIcon,
Pen,
UserIcon,
UsersIcon,
} from "lucide-react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, MailIcon, Pen, UserIcon, UsersIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useSetAtom } from "jotai";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { Button } from "@/components/ui/button";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface GmailAccount {
@ -127,15 +121,12 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
);
}
function isInsufficientPermissionsResult(
result: unknown,
): result is InsufficientPermissionsResult {
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
return (
typeof result === "object" &&
result !== null &&
"status" in result &&
(result as InsufficientPermissionsResult).status ===
"insufficient_permissions"
(result as InsufficientPermissionsResult).status === "insufficient_permissions"
);
}
@ -177,17 +168,11 @@ function ApprovalCard({
const existingBody = context?.existing_body;
const reviewConfig = interruptData.review_configs?.[0];
const allowedDecisions = reviewConfig?.allowed_decisions ?? [
"approve",
"reject",
];
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
const canEdit = allowedDecisions.includes("edit");
const currentSubject =
pendingEdits?.subject ??
args.subject ??
email?.subject ??
args.draft_subject_or_id;
pendingEdits?.subject ?? args.subject ?? email?.subject ?? args.draft_subject_or_id;
const currentBody = pendingEdits?.body ?? args.body;
const currentTo = pendingEdits?.to ?? args.to ?? "";
const currentCc = pendingEdits?.cc ?? args.cc ?? "";
@ -259,23 +244,15 @@ function ApprovalCard({
</p>
{phase === "processing" ? (
<TextShimmerLoader
text={
pendingEdits
? "Updating draft with your changes"
: "Updating draft"
}
text={pendingEdits ? "Updating draft with your changes" : "Updating draft"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits
? "Draft updated with your changes"
: "Draft updated"}
{pendingEdits ? "Draft updated with your changes" : "Draft updated"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Draft update was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Draft update was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -310,16 +287,12 @@ function ApprovalCard({
value: currentBcc,
},
];
openHitlEditPanel({
title: currentSubject,
content: editableBody,
toolName: "Gmail Draft",
openHitlEditPanel({
title: currentSubject,
content: editableBody,
toolName: "Gmail Draft",
extraFields,
onSave: (
newTitle,
newContent,
extraFieldValues,
) => {
onSave: (newTitle, newContent, extraFieldValues) => {
setIsPanelOpen(false);
const extras = extraFieldValues ?? {};
setPendingEdits({
@ -346,16 +319,12 @@ function ApprovalCard({
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 space-y-4 select-none">
{context.error ? (
<p className="text-sm text-destructive">
{context.error}
</p>
<p className="text-sm text-destructive">{context.error}</p>
) : (
<>
{account && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Gmail Account
</p>
<p className="text-xs font-medium text-muted-foreground">Gmail Account</p>
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm">
{account.name}
</div>
@ -364,15 +333,11 @@ function ApprovalCard({
{email && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Draft to Update
</p>
<p className="text-xs font-medium text-muted-foreground">Draft to Update</p>
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm space-y-1">
<div className="flex items-center gap-1.5">
<MailIcon className="size-3 shrink-0 text-muted-foreground" />
<span className="font-medium">
{email.subject}
</span>
<span className="font-medium">{email.subject}</span>
</div>
</div>
</div>
@ -408,18 +373,14 @@ function ApprovalCard({
<div className="px-5 pt-1">
{currentSubject != null && (
<p className="text-sm font-medium text-foreground">
{currentSubject}
</p>
<p className="text-sm font-medium text-foreground">{currentSubject}</p>
)}
{editableBody ? (
<div
{editableBody ? (
<div
className="mt-2 max-h-[7rem] overflow-hidden text-sm"
style={{
maskImage:
"linear-gradient(to bottom, black 50%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to bottom, black 50%, transparent 100%)",
maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
}}
>
<PlateEditor
@ -477,9 +438,7 @@ function ErrorCard({ result }: { result: ErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Failed to update Gmail draft
</p>
<p className="text-sm font-semibold text-destructive">Failed to update Gmail draft</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -493,9 +452,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Gmail authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Gmail authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -505,9 +462,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
);
}
function InsufficientPermissionsCard({
result,
}: { result: InsufficientPermissionsResult }) {
function InsufficientPermissionsCard({ result }: { result: InsufficientPermissionsResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
@ -577,7 +532,7 @@ export const UpdateGmailDraftToolUI = makeAssistantToolUI<
window.dispatchEvent(
new CustomEvent("hitl-decision", {
detail: { decisions: [decision] },
}),
})
);
}}
/>

View file

@ -1,16 +1,13 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
ClockIcon,
MapPinIcon,
UsersIcon,
GlobeIcon,
CornerDownLeftIcon,
Pen,
} from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useSetAtom } from "jotai";
import { ClockIcon, CornerDownLeftIcon, GlobeIcon, MapPinIcon, Pen, UsersIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import {
Select,
@ -19,11 +16,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
interface GoogleCalendarAccount {
id: number;
@ -160,8 +153,12 @@ function ApprovalCard({
const [wasEdited, setWasEdited] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{
summary: string; description: string; start_datetime: string;
end_datetime: string; location: string; attendees: string;
summary: string;
description: string;
start_datetime: string;
end_datetime: string;
location: string;
attendees: string;
} | null>(null);
const accounts = interruptData.context?.accounts ?? [];
@ -236,7 +233,19 @@ function ApprovalCard({
args: finalArgs,
},
});
}, [phase, isPanelOpen, canApprove, allowedDecisions, setProcessing, onDecision, interruptData, args, selectedAccountId, selectedCalendarId, pendingEdits]);
}, [
phase,
isPanelOpen,
canApprove,
allowedDecisions,
setProcessing,
onDecision,
interruptData,
args,
selectedAccountId,
selectedCalendarId,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -250,7 +259,10 @@ function ApprovalCard({
const attendeesList = (args.attendees as string[]) ?? [];
const displayAttendees = pendingEdits?.attendees
? pendingEdits.attendees.split(",").map((e) => e.trim()).filter(Boolean)
? pendingEdits.attendees
.split(",")
.map((e) => e.trim())
.filter(Boolean)
: attendeesList;
return (
@ -266,16 +278,17 @@ function ApprovalCard({
? "Calendar Event Approved"
: "Create Calendar Event"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={wasEdited ? "Creating event with your changes" : "Creating event"} size="sm" />
{phase === "processing" ? (
<TextShimmerLoader
text={wasEdited ? "Creating event with your changes" : "Creating event"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{wasEdited ? "Event created with your changes" : "Event created"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Event creation was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Event creation was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -291,14 +304,34 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
const extraFields: ExtraField[] = [
{ 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", type: "emails", value: pendingEdits?.attendees ?? 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",
type: "emails",
value: pendingEdits?.attendees ?? attendeesList.join(", "),
},
];
openHitlEditPanel({
title: pendingEdits?.summary ?? (args.summary ?? ""),
content: pendingEdits?.description ?? (args.description ?? ""),
title: pendingEdits?.summary ?? args.summary ?? "",
content: pendingEdits?.description ?? args.description ?? "",
toolName: "Calendar Event",
extraFields,
onSave: (newTitle, newContent, extraFieldValues) => {
@ -307,10 +340,16 @@ function ApprovalCard({
setPendingEdits({
summary: newTitle,
description: newContent,
start_datetime: extras.start_datetime ?? pendingEdits?.start_datetime ?? args.start_datetime ?? "",
end_datetime: extras.end_datetime ?? pendingEdits?.end_datetime ?? args.end_datetime ?? "",
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(", "),
attendees:
extras.attendees ?? pendingEdits?.attendees ?? attendeesList.join(", "),
});
},
onClose: () => setIsPanelOpen(false),
@ -372,7 +411,8 @@ function ApprovalCard({
<SelectContent>
{calendars.map((cal) => (
<SelectItem key={cal.id} value={cal.id}>
{cal.summary}{cal.primary ? " (primary)" : ""}
{cal.summary}
{cal.primary ? " (primary)" : ""}
</SelectItem>
))}
</SelectContent>
@ -399,16 +439,26 @@ function ApprovalCard({
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3 pb-3 space-y-2">
{(pendingEdits?.summary ?? args.summary) && (
<p className="text-sm font-medium text-foreground">{pendingEdits?.summary ?? args.summary}</p>
<p className="text-sm font-medium text-foreground">
{pendingEdits?.summary ?? args.summary}
</p>
)}
{((pendingEdits?.start_datetime ?? args.start_datetime) || (pendingEdits?.end_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>
{(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) : ""}
{(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>
)}

View file

@ -1,16 +1,11 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
CalendarIcon,
ClockIcon,
MapPinIcon,
CornerDownLeftIcon,
} from "lucide-react";
import { CalendarIcon, ClockIcon, CornerDownLeftIcon, MapPinIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface GoogleCalendarAccount {
@ -220,14 +215,12 @@ function ApprovalCard({
? "Calendar Event Deletion Approved"
: "Delete Calendar Event"}
</p>
{phase === "processing" ? (
{phase === "processing" ? (
<TextShimmerLoader text="Deleting event" size="sm" />
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">Event deleted</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Event deletion was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Event deletion was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -247,7 +240,9 @@ function ApprovalCard({
<>
{account && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">Google Calendar Account</p>
<p className="text-xs font-medium text-muted-foreground">
Google Calendar Account
</p>
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm">
{account.name}
</div>
@ -315,11 +310,7 @@ function ApprovalCard({
<>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button
size="sm"
className="rounded-lg gap-1.5"
onClick={handleApprove}
>
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
Approve
<CornerDownLeftIcon className="size-3 opacity-60" />
</Button>

View file

@ -1,3 +1,3 @@
export { CreateCalendarEventToolUI } from "./create-event";
export { UpdateCalendarEventToolUI } from "./update-event";
export { DeleteCalendarEventToolUI } from "./delete-event";
export { UpdateCalendarEventToolUI } from "./update-event";

View file

@ -1,22 +1,22 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import {
ClockIcon,
MapPinIcon,
UsersIcon,
ArrowRightIcon,
ClockIcon,
CornerDownLeftIcon,
MapPinIcon,
Pen,
UsersIcon,
} from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useSetAtom } from "jotai";
import { Button } from "@/components/ui/button";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
interface GoogleCalendarAccount {
id: number;
@ -180,8 +180,12 @@ function ApprovalCard({
const [wasEdited, setWasEdited] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{
summary: string; description: string; start_datetime: string;
end_datetime: string; location: string; attendees: string;
summary: string;
description: string;
start_datetime: string;
end_datetime: string;
location: string;
attendees: string;
} | null>(null);
const reviewConfig = interruptData.review_configs[0];
@ -196,19 +200,21 @@ function ApprovalCard({
const effectiveNewSummary = actionArgs.new_summary ?? args.new_summary;
const effectiveNewStartDatetime = actionArgs.new_start_datetime ?? args.new_start_datetime;
const effectiveNewEndDatetime = actionArgs.new_end_datetime ?? args.new_end_datetime;
const effectiveNewLocation = actionArgs.new_location !== undefined
? actionArgs.new_location
: args.new_location;
const effectiveNewAttendees = proposedAttendees
?? (Array.isArray(args.new_attendees) ? args.new_attendees : null);
const effectiveNewDescription = actionArgs.new_description !== undefined
? actionArgs.new_description
: args.new_description;
const effectiveNewLocation =
actionArgs.new_location !== undefined ? actionArgs.new_location : args.new_location;
const effectiveNewAttendees =
proposedAttendees ?? (Array.isArray(args.new_attendees) ? args.new_attendees : null);
const effectiveNewDescription =
actionArgs.new_description !== undefined ? actionArgs.new_description : args.new_description;
const changes: Array<{ label: string; oldVal: string; newVal: string }> = [];
if (effectiveNewSummary && String(effectiveNewSummary) !== (event?.summary ?? "")) {
changes.push({ label: "Summary", oldVal: event?.summary ?? "", newVal: String(effectiveNewSummary) });
changes.push({
label: "Summary",
oldVal: event?.summary ?? "",
newVal: String(effectiveNewSummary),
});
}
if (effectiveNewStartDatetime && String(effectiveNewStartDatetime) !== (event?.start ?? "")) {
changes.push({
@ -224,8 +230,15 @@ function ApprovalCard({
newVal: formatDateTime(String(effectiveNewEndDatetime)),
});
}
if (effectiveNewLocation !== undefined && String(effectiveNewLocation ?? "") !== (event?.location ?? "")) {
changes.push({ label: "Location", oldVal: event?.location ?? "", newVal: String(effectiveNewLocation ?? "") });
if (
effectiveNewLocation !== undefined &&
String(effectiveNewLocation ?? "") !== (event?.location ?? "")
) {
changes.push({
label: "Location",
oldVal: event?.location ?? "",
newVal: String(effectiveNewLocation ?? ""),
});
}
if (effectiveNewAttendees) {
const oldStr = currentAttendees.join(", ");
@ -242,7 +255,10 @@ function ApprovalCard({
const buildFinalArgs = useCallback(() => {
if (pendingEdits) {
const attendeesArr = pendingEdits.attendees
? pendingEdits.attendees.split(",").map((e) => e.trim()).filter(Boolean)
? pendingEdits.attendees
.split(",")
.map((e) => e.trim())
.filter(Boolean)
: null;
return {
event_id: event?.event_id,
@ -282,7 +298,16 @@ function ApprovalCard({
args: buildFinalArgs(),
},
});
}, [phase, isPanelOpen, allowedDecisions, setProcessing, onDecision, interruptData, buildFinalArgs, pendingEdits]);
}, [
phase,
isPanelOpen,
allowedDecisions,
setProcessing,
onDecision,
interruptData,
buildFinalArgs,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -308,15 +333,16 @@ function ApprovalCard({
: "Update Calendar Event"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={wasEdited ? "Updating event with your changes" : "Updating event"} size="sm" />
<TextShimmerLoader
text={wasEdited ? "Updating event with your changes" : "Updating event"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{wasEdited ? "Event updated with your changes" : "Event updated"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Event update was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Event update was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -331,24 +357,48 @@ function ApprovalCard({
className="rounded-lg text-muted-foreground -mt-1 -mr-2"
onClick={() => {
setIsPanelOpen(true);
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 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 },
{
key: "start_datetime",
label: "Start",
type: "datetime-local",
value: proposedStart,
},
{ key: "end_datetime", label: "End", type: "datetime-local", value: proposedEnd },
{ key: "location", label: "Location", type: "text", value: proposedLocation },
{ key: "attendees", label: "Attendees", type: "emails", value: proposedAttendeesStr },
{
key: "attendees",
label: "Attendees",
type: "emails",
value: proposedAttendeesStr,
},
];
openHitlEditPanel({
title: proposedSummary,
@ -377,7 +427,7 @@ function ApprovalCard({
)}
</div>
{/* Content section */}
{/* Content section */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 space-y-4 select-none">
{context?.error ? (
@ -433,9 +483,13 @@ function ApprovalCard({
<div key={change.label} className="text-xs space-y-0.5">
<span className="text-muted-foreground">{change.label}</span>
<div className="flex items-center gap-1.5 flex-wrap">
<span className="text-muted-foreground line-through">{change.oldVal || "(empty)"}</span>
<span className="text-muted-foreground line-through">
{change.oldVal || "(empty)"}
</span>
<ArrowRightIcon className="size-3 text-muted-foreground shrink-0" />
<span className="font-medium text-foreground">{change.newVal || "(empty)"}</span>
<span className="font-medium text-foreground">
{change.newVal || "(empty)"}
</span>
</div>
</div>
))}
@ -446,7 +500,8 @@ function ApprovalCard({
className="mt-1 max-h-[5rem] overflow-hidden"
style={{
maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to bottom, black 50%, transparent 100%)",
}}
>
<PlateEditor

View file

@ -1,12 +1,12 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
CornerDownLeftIcon,
FileIcon,
Pen,
} from "lucide-react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, FileIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import {
Select,
@ -15,11 +15,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { useSetAtom } from "jotai";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
interface GoogleDriveAccount {
id: number;
@ -139,8 +135,8 @@ function ApprovalCard({
const [pendingEdits, setPendingEdits] = useState<{ name: string; content: string } | null>(null);
const accounts = interruptData.context?.accounts ?? [];
const validAccounts = accounts.filter(a => !a.auth_expired);
const expiredAccounts = accounts.filter(a => a.auth_expired);
const validAccounts = accounts.filter((a) => !a.auth_expired);
const expiredAccounts = accounts.filter((a) => a.auth_expired);
const defaultAccountId = useMemo(() => {
if (validAccounts.length === 1) return String(validAccounts[0].id);
@ -162,7 +158,8 @@ function ApprovalCard({
setParentFolderId("__root__");
}, []);
const fileTypeLabel = FILE_TYPE_LABELS[selectedFileType] ?? FILE_TYPE_LABELS[args.file_type] ?? "Google Drive File";
const fileTypeLabel =
FILE_TYPE_LABELS[selectedFileType] ?? FILE_TYPE_LABELS[args.file_type] ?? "Google Drive File";
const isNameValid = useMemo(() => {
const name = pendingEdits?.name ?? args.name;
@ -194,7 +191,20 @@ function ApprovalCard({
},
},
});
}, [phase, setProcessing, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, args, selectedFileType, selectedAccountId, parentFolderId, pendingEdits]);
}, [
phase,
setProcessing,
isPanelOpen,
canApprove,
allowedDecisions,
onDecision,
interruptData,
args,
selectedFileType,
selectedAccountId,
parentFolderId,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -218,16 +228,17 @@ function ApprovalCard({
? `${fileTypeLabel} Approved`
: `Create ${fileTypeLabel}`}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={pendingEdits ? "Creating file with your changes" : "Creating file"} size="sm" />
{phase === "processing" ? (
<TextShimmerLoader
text={pendingEdits ? "Creating file with your changes" : "Creating file"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits ? "File created with your changes" : "File created"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
File creation was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">File creation was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -242,8 +253,8 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
openHitlEditPanel({
title: pendingEdits?.name ?? (args.name ?? ""),
content: pendingEdits?.content ?? (args.content ?? ""),
title: pendingEdits?.name ?? args.name ?? "",
content: pendingEdits?.content ?? args.content ?? "",
toolName: fileTypeLabel,
onSave: (newName, newContent) => {
setIsPanelOpen(false);
@ -268,33 +279,33 @@ function ApprovalCard({
<p className="text-sm text-destructive">{interruptData.context.error}</p>
) : (
<>
{accounts.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Google Drive Account <span className="text-destructive">*</span>
</p>
<Select value={selectedAccountId} onValueChange={handleAccountChange}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an account" />
</SelectTrigger>
<SelectContent>
{validAccounts.map((account) => (
<SelectItem key={account.id} value={String(account.id)}>
{account.name}
</SelectItem>
))}
{expiredAccounts.map((a) => (
<div
key={a.id}
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
>
{a.name} (expired, retry after re-auth)
</div>
))}
</SelectContent>
</Select>
</div>
)}
{accounts.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Google Drive Account <span className="text-destructive">*</span>
</p>
<Select value={selectedAccountId} onValueChange={handleAccountChange}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an account" />
</SelectTrigger>
<SelectContent>
{validAccounts.map((account) => (
<SelectItem key={account.id} value={String(account.id)}>
{account.name}
</SelectItem>
))}
{expiredAccounts.map((a) => (
<div
key={a.id}
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
>
{a.name} (expired, retry after re-auth)
</div>
))}
</SelectContent>
</Select>
</div>
)}
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
@ -311,31 +322,29 @@ function ApprovalCard({
</Select>
</div>
{selectedAccountId && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Parent Folder
</p>
<Select value={parentFolderId} onValueChange={setParentFolderId}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Drive Root" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__root__">Drive Root</SelectItem>
{availableParentFolders.map((folder) => (
<SelectItem key={folder.folder_id} value={folder.folder_id}>
{folder.name}
</SelectItem>
))}
</SelectContent>
</Select>
{availableParentFolders.length === 0 && (
<p className="text-xs text-muted-foreground">
No folders found. File will be created at Drive root.
</p>
)}
</div>
)}
{selectedAccountId && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">Parent Folder</p>
<Select value={parentFolderId} onValueChange={setParentFolderId}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Drive Root" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__root__">Drive Root</SelectItem>
{availableParentFolders.map((folder) => (
<SelectItem key={folder.folder_id} value={folder.folder_id}>
{folder.name}
</SelectItem>
))}
</SelectContent>
</Select>
{availableParentFolders.length === 0 && (
<p className="text-xs text-muted-foreground">
No folders found. File will be created at Drive root.
</p>
)}
</div>
)}
</>
)}
</div>
@ -345,9 +354,11 @@ function ApprovalCard({
{/* Content preview */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3">
{(pendingEdits?.name ?? args.name) != null && (
<p className="text-sm font-medium text-foreground">{String(pendingEdits?.name ?? args.name)}</p>
)}
{(pendingEdits?.name ?? args.name) != null && (
<p className="text-sm font-medium text-foreground">
{String(pendingEdits?.name ?? args.name)}
</p>
)}
{(pendingEdits?.content ?? args.content) != null && (
<div
className="mt-2 max-h-[7rem] overflow-hidden text-sm"

View file

@ -1,14 +1,11 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
CornerDownLeftIcon,
InfoIcon,
} from "lucide-react";
import { CornerDownLeftIcon, InfoIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface GoogleDriveAccount {
@ -207,14 +204,12 @@ function ApprovalCard({
? "Google Drive File Deletion Approved"
: "Delete Google Drive File"}
</p>
{phase === "processing" ? (
{phase === "processing" ? (
<TextShimmerLoader text="Trashing file" size="sm" />
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">File trashed</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
File deletion was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">File deletion was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -274,22 +269,23 @@ function ApprovalCard({
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 space-y-3 select-none">
<p className="text-xs text-muted-foreground">
The file will be moved to Google Drive trash. You can restore it from trash within 30 days.
The file will be moved to Google Drive trash. You can restore it from trash within 30
days.
</p>
<div className="flex items-center gap-2.5">
<Checkbox
id="delete-from-kb"
checked={deleteFromKb}
onCheckedChange={(v) => setDeleteFromKb(v === true)}
className="shrink-0"
/>
<label htmlFor="delete-from-kb" className="flex-1 cursor-pointer">
<span className="text-sm text-foreground">Also remove from knowledge base</span>
<p className="text-xs text-muted-foreground mt-0.5">
This will permanently delete the file from your knowledge base (cannot be undone)
</p>
</label>
</div>
<div className="flex items-center gap-2.5">
<Checkbox
id="delete-from-kb"
checked={deleteFromKb}
onCheckedChange={(v) => setDeleteFromKb(v === true)}
className="shrink-0"
/>
<label htmlFor="delete-from-kb" className="flex-1 cursor-pointer">
<span className="text-sm text-foreground">Also remove from knowledge base</span>
<p className="text-xs text-muted-foreground mt-0.5">
This will permanently delete the file from your knowledge base (cannot be undone)
</p>
</label>
</div>
</div>
</>
)}
@ -298,27 +294,23 @@ function ApprovalCard({
{phase === "pending" && (
<>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button
size="sm"
className="rounded-lg gap-1.5"
onClick={handleApprove}
>
Approve
<CornerDownLeftIcon className="size-3 opacity-60" />
</Button>
<Button
size="sm"
variant="ghost"
className="rounded-lg text-muted-foreground"
onClick={() => {
setRejected();
onDecision({ type: "reject", message: "User rejected the action." });
}}
>
Reject
</Button>
</div>
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
Approve
<CornerDownLeftIcon className="size-3 opacity-60" />
</Button>
<Button
size="sm"
variant="ghost"
className="rounded-lg text-muted-foreground"
onClick={() => {
setRejected();
onDecision({ type: "reject", message: "User rejected the action." });
}}
>
Reject
</Button>
</div>
</>
)}
</div>

View file

@ -1,8 +1,12 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import {
Select,
@ -11,11 +15,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { useSetAtom } from "jotai";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
interface JiraAccount {
id: number;
@ -151,7 +151,9 @@ function ApprovalCard({
const { phase, setProcessing, setRejected } = useHitlPhase(interruptData);
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{ title: string; description: string } | null>(null);
const [pendingEdits, setPendingEdits] = useState<{ title: string; description: string } | null>(
null
);
const [selectedAccountId, setSelectedAccountId] = useState("");
const [selectedProjectKey, setSelectedProjectKey] = useState(args.project_key ?? "");
@ -177,14 +179,23 @@ function ApprovalCard({
(overrides?: { title?: string; description?: string }) => {
return {
summary: overrides?.title ?? pendingEdits?.title ?? args.summary,
description: overrides?.description ?? pendingEdits?.description ?? args.description ?? null,
description:
overrides?.description ?? pendingEdits?.description ?? args.description ?? null,
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
project_key: selectedProjectKey || null,
issue_type: selectedIssueType === "__none__" ? null : selectedIssueType,
priority: selectedPriority === "__none__" ? null : selectedPriority,
};
},
[args.summary, args.description, selectedAccountId, selectedProjectKey, selectedIssueType, selectedPriority, pendingEdits]
[
args.summary,
args.description,
selectedAccountId,
selectedProjectKey,
selectedIssueType,
selectedPriority,
pendingEdits,
]
);
const handleApprove = useCallback(() => {
@ -200,7 +211,17 @@ function ApprovalCard({
args: buildFinalArgs(),
},
});
}, [phase, setProcessing, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, buildFinalArgs, pendingEdits]);
}, [
phase,
setProcessing,
isPanelOpen,
canApprove,
allowedDecisions,
onDecision,
interruptData,
buildFinalArgs,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -225,15 +246,16 @@ function ApprovalCard({
: "Create Jira Issue"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={pendingEdits ? "Creating issue with your changes" : "Creating issue"} size="sm" />
<TextShimmerLoader
text={pendingEdits ? "Creating issue with your changes" : "Creating issue"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits ? "Issue created with your changes" : "Issue created"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Issue creation was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Issue creation was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -248,8 +270,8 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
openHitlEditPanel({
title: pendingEdits?.title ?? (args.summary ?? ""),
content: pendingEdits?.description ?? (args.description ?? ""),
title: pendingEdits?.title ?? args.summary ?? "",
content: pendingEdits?.description ?? args.description ?? "",
toolName: "Jira Issue",
onSave: (newTitle, newDescription) => {
setIsPanelOpen(false);
@ -316,10 +338,7 @@ function ApprovalCard({
<p className="text-xs font-medium text-muted-foreground">
Project <span className="text-destructive">*</span>
</p>
<Select
value={selectedProjectKey}
onValueChange={setSelectedProjectKey}
>
<Select value={selectedProjectKey} onValueChange={setSelectedProjectKey}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a project" />
</SelectTrigger>
@ -336,32 +355,26 @@ function ApprovalCard({
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground">Issue Type</p>
<Select
value={selectedIssueType}
onValueChange={setSelectedIssueType}
>
<Select value={selectedIssueType} onValueChange={setSelectedIssueType}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Task" />
</SelectTrigger>
<SelectContent>
{issueTypes.length > 0
? issueTypes.map((t) => (
{issueTypes.length > 0 ? (
issueTypes.map((t) => (
<SelectItem key={t.id} value={t.name}>
{t.name}
</SelectItem>
))
: (
<SelectItem value="Task">Task</SelectItem>
)}
) : (
<SelectItem value="Task">Task</SelectItem>
)}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground">Priority</p>
<Select
value={selectedPriority}
onValueChange={setSelectedPriority}
>
<Select value={selectedPriority} onValueChange={setSelectedPriority}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Default" />
</SelectTrigger>
@ -388,7 +401,9 @@ function ApprovalCard({
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3">
{(pendingEdits?.title ?? args.summary) != null && (
<p className="text-sm font-medium text-foreground">{pendingEdits?.title ?? args.summary}</p>
<p className="text-sm font-medium text-foreground">
{pendingEdits?.title ?? args.summary}
</p>
)}
{(pendingEdits?.description ?? args.description) != null && (
<div
@ -450,9 +465,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
All Jira accounts expired
</p>
<p className="text-sm font-semibold text-destructive">All Jira accounts expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -3,9 +3,9 @@
import { makeAssistantToolUI } from "@assistant-ui/react";
import { CornerDownLeftIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface JiraAccount {
@ -204,9 +204,7 @@ function ApprovalCard({
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">Issue deleted</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Issue deletion was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Issue deletion was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -280,11 +278,7 @@ function ApprovalCard({
<>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button
size="sm"
className="rounded-lg gap-1.5"
onClick={handleApprove}
>
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
Approve
<CornerDownLeftIcon className="size-3 opacity-60" />
</Button>
@ -310,9 +304,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Jira authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Jira authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -356,9 +348,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
Issue not found
</p>
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Issue not found</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -4,6 +4,9 @@ import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@ -13,10 +16,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
interface JiraIssue {
issue_id: string;
@ -194,9 +194,12 @@ function ApprovalCard({
const canEdit = allowedDecisions.includes("edit");
const hasProposedChanges =
actionArgs.new_summary || args.new_summary ||
actionArgs.new_description || args.new_description ||
actionArgs.new_priority || args.new_priority;
actionArgs.new_summary ||
args.new_summary ||
actionArgs.new_description ||
args.new_description ||
actionArgs.new_priority ||
args.new_priority;
const buildFinalArgs = useCallback(() => {
return {
@ -222,7 +225,16 @@ function ApprovalCard({
args: buildFinalArgs(),
},
});
}, [phase, setProcessing, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs, hasPanelEdits]);
}, [
phase,
setProcessing,
isPanelOpen,
allowedDecisions,
onDecision,
interruptData,
buildFinalArgs,
hasPanelEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -247,15 +259,16 @@ function ApprovalCard({
: "Update Jira Issue"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={hasPanelEdits ? "Updating issue with your changes" : "Updating issue"} size="sm" />
<TextShimmerLoader
text={hasPanelEdits ? "Updating issue with your changes" : "Updating issue"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{hasPanelEdits ? "Issue updated with your changes" : "Issue updated"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Issue update was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Issue update was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -366,14 +379,20 @@ function ApprovalCard({
{/* Content preview — proposed changes */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3">
{(hasProposedChanges || hasPanelEdits) ? (
{hasProposedChanges || hasPanelEdits ? (
<>
{(hasPanelEdits ? editedArgs.summary : (actionArgs.new_summary ?? args.new_summary)) && (
{(hasPanelEdits
? editedArgs.summary
: (actionArgs.new_summary ?? args.new_summary)) && (
<p className="text-sm font-medium text-foreground">
{String(hasPanelEdits ? editedArgs.summary : (actionArgs.new_summary ?? args.new_summary))}
{String(
hasPanelEdits ? editedArgs.summary : (actionArgs.new_summary ?? args.new_summary)
)}
</p>
)}
{(hasPanelEdits ? editedArgs.description : (actionArgs.new_description ?? args.new_description)) && (
{(hasPanelEdits
? editedArgs.description
: (actionArgs.new_description ?? args.new_description)) && (
<div
className="max-h-[7rem] overflow-hidden text-sm"
style={{
@ -382,7 +401,11 @@ function ApprovalCard({
}}
>
<PlateEditor
markdown={String(hasPanelEdits ? editedArgs.description : (actionArgs.new_description ?? args.new_description))}
markdown={String(
hasPanelEdits
? editedArgs.description
: (actionArgs.new_description ?? args.new_description)
)}
readOnly
preset="readonly"
editorVariant="none"
@ -445,9 +468,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Jira authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Jira authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -491,9 +512,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
Issue not found
</p>
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Issue not found</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -1,8 +1,12 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@ -13,11 +17,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { useSetAtom } from "jotai";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
interface LinearLabel {
id: string;
@ -148,7 +148,9 @@ function ApprovalCard({
const { phase, setProcessing, setRejected } = useHitlPhase(interruptData);
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{ title: string; description: string } | null>(null);
const [pendingEdits, setPendingEdits] = useState<{ title: string; description: string } | null>(
null
);
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState("");
const [selectedTeamId, setSelectedTeamId] = useState("");
@ -178,18 +180,32 @@ function ApprovalCard({
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
const canEdit = allowedDecisions.includes("edit");
const buildFinalArgs = useCallback((overrides?: { title?: string; description?: string }) => {
return {
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,
assignee_id: selectedAssigneeId === "__none__" ? null : selectedAssigneeId,
priority: Number(selectedPriority),
label_ids: selectedLabelIds,
};
}, [args.title, args.description, selectedWorkspaceId, selectedTeamId, selectedStateId, selectedAssigneeId, selectedPriority, selectedLabelIds, pendingEdits]);
const buildFinalArgs = useCallback(
(overrides?: { title?: string; description?: string }) => {
return {
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,
assignee_id: selectedAssigneeId === "__none__" ? null : selectedAssigneeId,
priority: Number(selectedPriority),
label_ids: selectedLabelIds,
};
},
[
args.title,
args.description,
selectedWorkspaceId,
selectedTeamId,
selectedStateId,
selectedAssigneeId,
selectedPriority,
selectedLabelIds,
pendingEdits,
]
);
const handleApprove = useCallback(() => {
if (phase !== "pending") return;
@ -204,7 +220,17 @@ function ApprovalCard({
args: buildFinalArgs(),
},
});
}, [phase, setProcessing, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, buildFinalArgs, pendingEdits]);
}, [
phase,
setProcessing,
isPanelOpen,
canApprove,
allowedDecisions,
onDecision,
interruptData,
buildFinalArgs,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -229,15 +255,16 @@ function ApprovalCard({
: "Create Linear Issue"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={pendingEdits ? "Creating issue with your changes" : "Creating issue"} size="sm" />
<TextShimmerLoader
text={pendingEdits ? "Creating issue with your changes" : "Creating issue"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits ? "Issue created with your changes" : "Issue created"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Issue creation was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Issue creation was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -252,8 +279,8 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
openHitlEditPanel({
title: pendingEdits?.title ?? (args.title ?? ""),
content: pendingEdits?.description ?? (args.description ?? ""),
title: pendingEdits?.title ?? args.title ?? "",
content: pendingEdits?.description ?? args.description ?? "",
toolName: "Linear Issue",
onSave: (newTitle, newDescription) => {
setIsPanelOpen(false);
@ -269,7 +296,7 @@ function ApprovalCard({
)}
</div>
{/* Context section — real pickers in pending */}
{/* Context section — real pickers in pending */}
{phase === "pending" && (
<>
<div className="mx-5 h-px bg-border/50" />
@ -278,43 +305,43 @@ function ApprovalCard({
<p className="text-sm text-destructive">{interruptData.context.error}</p>
) : (
<>
{workspaces.length > 0 && (
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground">
Linear Account <span className="text-destructive">*</span>
</p>
<Select
value={selectedWorkspaceId}
onValueChange={(v) => {
setSelectedWorkspaceId(v);
setSelectedTeamId("");
setSelectedStateId("__none__");
setSelectedAssigneeId("__none__");
setSelectedPriority("0");
setSelectedLabelIds([]);
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an account" />
</SelectTrigger>
<SelectContent>
{validWorkspaces.map((w) => (
<SelectItem key={w.id} value={String(w.id)}>
{w.name}
</SelectItem>
))}
{expiredWorkspaces.map((w) => (
<div
key={w.id}
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
>
{w.name} (expired, retry after re-auth)
</div>
))}
</SelectContent>
</Select>
</div>
)}
{workspaces.length > 0 && (
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground">
Linear Account <span className="text-destructive">*</span>
</p>
<Select
value={selectedWorkspaceId}
onValueChange={(v) => {
setSelectedWorkspaceId(v);
setSelectedTeamId("");
setSelectedStateId("__none__");
setSelectedAssigneeId("__none__");
setSelectedPriority("0");
setSelectedLabelIds([]);
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an account" />
</SelectTrigger>
<SelectContent>
{validWorkspaces.map((w) => (
<SelectItem key={w.id} value={String(w.id)}>
{w.name}
</SelectItem>
))}
{expiredWorkspaces.map((w) => (
<div
key={w.id}
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
>
{w.name} (expired, retry after re-auth)
</div>
))}
</SelectContent>
</Select>
</div>
)}
{selectedWorkspace && (
<>
@ -366,7 +393,10 @@ function ApprovalCard({
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground">Assignee</p>
<Select value={selectedAssigneeId} onValueChange={setSelectedAssigneeId}>
<Select
value={selectedAssigneeId}
onValueChange={setSelectedAssigneeId}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Unassigned" />
</SelectTrigger>
@ -520,9 +550,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
All Linear accounts expired
</p>
<p className="text-sm font-semibold text-destructive">All Linear accounts expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -3,9 +3,9 @@
import { makeAssistantToolUI } from "@assistant-ui/react";
import { CornerDownLeftIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface InterruptResult {
@ -150,7 +150,15 @@ function ApprovalCard({
},
},
});
}, [phase, setProcessing, onDecision, interruptData, issue?.id, context?.workspace?.id, deleteFromKb]);
}, [
phase,
setProcessing,
onDecision,
interruptData,
issue?.id,
context?.workspace?.id,
deleteFromKb,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -174,14 +182,12 @@ function ApprovalCard({
? "Linear Issue Deletion Approved"
: "Delete Linear Issue"}
</p>
{phase === "processing" ? (
{phase === "processing" ? (
<TextShimmerLoader text="Deleting issue" size="sm" />
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">Issue deleted</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Issue deletion was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Issue deletion was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -232,20 +238,20 @@ function ApprovalCard({
<>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 select-none">
<div className="flex items-center gap-2.5">
<Checkbox
id="linear-delete-from-kb"
checked={deleteFromKb}
onCheckedChange={(v) => setDeleteFromKb(v === true)}
className="shrink-0"
/>
<label htmlFor="linear-delete-from-kb" className="flex-1 cursor-pointer">
<span className="text-sm text-foreground">Also remove from knowledge base</span>
<p className="text-xs text-muted-foreground mt-0.5">
This will permanently delete the issue from your knowledge base (cannot be undone)
</p>
</label>
</div>
<div className="flex items-center gap-2.5">
<Checkbox
id="linear-delete-from-kb"
checked={deleteFromKb}
onCheckedChange={(v) => setDeleteFromKb(v === true)}
className="shrink-0"
/>
<label htmlFor="linear-delete-from-kb" className="flex-1 cursor-pointer">
<span className="text-sm text-foreground">Also remove from knowledge base</span>
<p className="text-xs text-muted-foreground mt-0.5">
This will permanently delete the issue from your knowledge base (cannot be undone)
</p>
</label>
</div>
</div>
</>
)}
@ -254,12 +260,8 @@ function ApprovalCard({
{phase === "pending" && (
<>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button
size="sm"
className="rounded-lg gap-1.5"
onClick={handleApprove}
>
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
Approve
<CornerDownLeftIcon className="size-3 opacity-60" />
</Button>
@ -285,9 +287,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Linear authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Linear authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -315,9 +315,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
Issue not found
</p>
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Issue not found</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -4,6 +4,9 @@ import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@ -14,10 +17,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
interface LinearLabel {
id: string;
@ -110,7 +110,12 @@ interface AuthErrorResult {
connector_type: string;
}
type UpdateLinearIssueResult = InterruptResult | SuccessResult | ErrorResult | NotFoundResult | AuthErrorResult;
type UpdateLinearIssueResult =
| InterruptResult
| SuccessResult
| ErrorResult
| NotFoundResult
| AuthErrorResult;
function isInterruptResult(result: unknown): result is InterruptResult {
return (
@ -178,7 +183,9 @@ function ApprovalCard({
const issue = context?.issue;
const initialEditState = {
title: actionArgs.new_title ? String(actionArgs.new_title) : (issue?.title ?? args.new_title ?? ""),
title: actionArgs.new_title
? String(actionArgs.new_title)
: (issue?.title ?? args.new_title ?? ""),
description: actionArgs.new_description
? String(actionArgs.new_description)
: (issue?.description ?? args.new_description ?? ""),
@ -256,8 +263,10 @@ function ApprovalCard({
);
const hasProposedChanges =
actionArgs.new_title || args.new_title ||
actionArgs.new_description || args.new_description ||
actionArgs.new_title ||
args.new_title ||
actionArgs.new_description ||
args.new_description ||
proposedStateName ||
proposedAssigneeName ||
proposedPriorityLabel ||
@ -276,7 +285,16 @@ function ApprovalCard({
args: buildFinalArgs(),
},
});
}, [phase, setProcessing, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs, hasPanelEdits]);
}, [
phase,
setProcessing,
isPanelOpen,
allowedDecisions,
onDecision,
interruptData,
buildFinalArgs,
hasPanelEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -301,15 +319,16 @@ function ApprovalCard({
: "Update Linear Issue"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={hasPanelEdits ? "Updating issue with your changes" : "Updating issue"} size="sm" />
<TextShimmerLoader
text={hasPanelEdits ? "Updating issue with your changes" : "Updating issue"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{hasPanelEdits ? "Issue updated with your changes" : "Issue updated"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Issue update was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Issue update was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -346,7 +365,7 @@ function ApprovalCard({
)}
</div>
{/* Context section — workspace + current issue + pickers in pending */}
{/* Context section — workspace + current issue + pickers in pending */}
{phase === "pending" && (
<>
<div className="mx-5 h-px bg-border/50" />
@ -385,7 +404,9 @@ function ApprovalCard({
)}
{issue.current_assignee && <span>{issue.current_assignee.name}</span>}
{priorities.find((p) => p.priority === issue.priority) && (
<span>{priorities.find((p) => p.priority === issue.priority)?.label}</span>
<span>
{priorities.find((p) => p.priority === issue.priority)?.label}
</span>
)}
</div>
{issue.current_labels && issue.current_labels.length > 0 && (
@ -510,9 +531,7 @@ function ApprovalCard({
? `${label.color}70`
: `${label.color}28`,
color: label.color,
borderColor: isSelected
? `${label.color}cc`
: "transparent",
borderColor: isSelected ? `${label.color}cc` : "transparent",
}}
>
<span
@ -538,12 +557,18 @@ function ApprovalCard({
{/* Content preview — proposed changes */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3">
{(hasProposedChanges || hasPanelEdits) ? (
{hasProposedChanges || hasPanelEdits ? (
<>
{(hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title)) && (
<p className="text-sm font-medium text-foreground">{String(hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title))}</p>
<p className="text-sm font-medium text-foreground">
{String(
hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title)
)}
</p>
)}
{(hasPanelEdits ? editedArgs.description : (actionArgs.new_description ?? args.new_description)) && (
{(hasPanelEdits
? editedArgs.description
: (actionArgs.new_description ?? args.new_description)) && (
<div
className="max-h-[7rem] overflow-hidden text-sm"
style={{
@ -552,7 +577,11 @@ function ApprovalCard({
}}
>
<PlateEditor
markdown={String(hasPanelEdits ? editedArgs.description : (actionArgs.new_description ?? args.new_description))}
markdown={String(
hasPanelEdits
? editedArgs.description
: (actionArgs.new_description ?? args.new_description)
)}
readOnly
preset="readonly"
editorVariant="none"
@ -641,9 +670,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Linear authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Linear authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -671,9 +698,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
Issue not found
</p>
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Issue not found</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -4,6 +4,9 @@ import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import {
Select,
@ -12,9 +15,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface InterruptResult {
@ -123,8 +123,8 @@ function ApprovalCard({
const [pendingEdits, setPendingEdits] = useState<{ title: string; content: string } | null>(null);
const accounts = interruptData.context?.accounts ?? [];
const validAccounts = accounts.filter(a => !a.auth_expired);
const expiredAccounts = accounts.filter(a => a.auth_expired);
const validAccounts = accounts.filter((a) => !a.auth_expired);
const expiredAccounts = accounts.filter((a) => a.auth_expired);
const parentPages = interruptData.context?.parent_pages ?? {};
const defaultAccountId = useMemo(() => {
@ -166,12 +166,23 @@ function ApprovalCard({
...args,
...(pendingEdits && { title: pendingEdits.title, content: pendingEdits.content }),
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
parent_page_id:
selectedParentPageId === "__none__" ? null : selectedParentPageId,
parent_page_id: selectedParentPageId === "__none__" ? null : selectedParentPageId,
},
},
});
}, [phase, isPanelOpen, selectedAccountId, isTitleValid, allowedDecisions, setProcessing, onDecision, interruptData, args, selectedParentPageId, pendingEdits]);
}, [
phase,
isPanelOpen,
selectedAccountId,
isTitleValid,
allowedDecisions,
setProcessing,
onDecision,
interruptData,
args,
selectedParentPageId,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -184,9 +195,7 @@ function ApprovalCard({
}, [handleApprove]);
return (
<div
className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300"
>
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
{/* Header */}
<div className="flex items-start justify-between px-5 pt-5 pb-4 select-none">
<div>
@ -198,15 +207,16 @@ function ApprovalCard({
: "Create Notion Page"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={pendingEdits ? "Creating page with your changes" : "Creating page"} size="sm" />
<TextShimmerLoader
text={pendingEdits ? "Creating page with your changes" : "Creating page"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits ? "Page created with your changes" : "Page created"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Page creation was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Page creation was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -238,7 +248,7 @@ function ApprovalCard({
)}
</div>
{/* Account/workspace picker — real UI in pending */}
{/* Account/workspace picker — real UI in pending */}
{phase === "pending" && interruptData.context && (
<>
<div className="mx-5 h-px bg-border/50" />
@ -247,39 +257,39 @@ function ApprovalCard({
<p className="text-sm text-destructive">{interruptData.context.error}</p>
) : (
<>
{accounts.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Notion Account <span className="text-destructive">*</span>
</p>
<Select
value={selectedAccountId}
onValueChange={(value) => {
setSelectedAccountId(value);
setSelectedParentPageId("__none__");
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an account" />
</SelectTrigger>
<SelectContent>
{validAccounts.map((account) => (
<SelectItem key={account.id} value={String(account.id)}>
{account.workspace_name}
</SelectItem>
))}
{expiredAccounts.map((a) => (
<div
key={a.id}
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
>
{a.workspace_name} (expired, retry after re-auth)
</div>
))}
</SelectContent>
</Select>
</div>
)}
{accounts.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Notion Account <span className="text-destructive">*</span>
</p>
<Select
value={selectedAccountId}
onValueChange={(value) => {
setSelectedAccountId(value);
setSelectedParentPageId("__none__");
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an account" />
</SelectTrigger>
<SelectContent>
{validAccounts.map((account) => (
<SelectItem key={account.id} value={String(account.id)}>
{account.workspace_name}
</SelectItem>
))}
{expiredAccounts.map((a) => (
<div
key={a.id}
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
>
{a.workspace_name} (expired, retry after re-auth)
</div>
))}
</SelectContent>
</Select>
</div>
)}
{selectedAccountId && (
<div className="space-y-2">
@ -316,7 +326,9 @@ function ApprovalCard({
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3">
{(pendingEdits?.title ?? args.title) != null && (
<p className="text-sm font-medium text-foreground">{String(pendingEdits?.title ?? args.title)}</p>
<p className="text-sm font-medium text-foreground">
{String(pendingEdits?.title ?? args.title)}
</p>
)}
{(pendingEdits?.content ?? args.content) != null && (
<div
@ -378,9 +390,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Notion authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Notion authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -3,9 +3,9 @@
import { makeAssistantToolUI } from "@assistant-ui/react";
import { CornerDownLeftIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface InterruptResult {
@ -183,14 +183,12 @@ function ApprovalCard({
? "Notion Page Deletion Approved"
: "Delete Notion Page"}
</p>
{phase === "processing" ? (
{phase === "processing" ? (
<TextShimmerLoader text="Deleting page" size="sm" />
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">Page deleted</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Page deletion was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Page deletion was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -259,11 +257,7 @@ function ApprovalCard({
<>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button
size="sm"
className="rounded-lg gap-1.5"
onClick={handleApprove}
>
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
Approve
<CornerDownLeftIcon className="size-3 opacity-60" />
</Button>
@ -289,9 +283,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
All Notion accounts expired
</p>
<p className="text-sm font-semibold text-destructive">All Notion accounts expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -319,9 +311,7 @@ function InfoCard({ result }: { result: InfoResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
Page not found
</p>
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Page not found</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -4,10 +4,10 @@ import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { Button } from "@/components/ui/button";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface InterruptResult {
@ -68,7 +68,12 @@ interface AuthErrorResult {
connector_type: string;
}
type UpdateNotionPageResult = InterruptResult | SuccessResult | ErrorResult | InfoResult | AuthErrorResult;
type UpdateNotionPageResult =
| InterruptResult
| SuccessResult
| ErrorResult
| InfoResult
| AuthErrorResult;
function isInterruptResult(result: unknown): result is InterruptResult {
return (
@ -148,7 +153,17 @@ function ApprovalCard({
},
},
});
}, [phase, isPanelOpen, allowedDecisions, setProcessing, onDecision, interruptData, args, account?.id, pendingEdits]);
}, [
phase,
isPanelOpen,
allowedDecisions,
setProcessing,
onDecision,
interruptData,
args,
account?.id,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -173,15 +188,16 @@ function ApprovalCard({
: "Update Notion Page"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={pendingEdits ? "Updating page with your changes" : "Updating page"} size="sm" />
<TextShimmerLoader
text={pendingEdits ? "Updating page with your changes" : "Updating page"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits ? "Page updated with your changes" : "Page updated"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Page update was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Page update was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -213,7 +229,7 @@ function ApprovalCard({
)}
</div>
{/* Context section — real UI in pending/processing/complete */}
{/* Context section — real UI in pending/processing/complete */}
{phase !== "rejected" && interruptData.context && (
<>
<div className="mx-5 h-px bg-border/50" />
@ -310,9 +326,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Notion authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Notion authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -340,9 +354,7 @@ function InfoCard({ result }: { result: InfoResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
Page not found
</p>
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Page not found</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">