feat: enhance permission handling and user feedback for Gmail and Google Calendar tools

- Implemented logic to persist authentication expiration status for connectors when insufficient permissions are detected, improving error handling and user experience.
- Updated messages to guide users to re-authenticate in connector settings for Gmail, Google Calendar, and Google Drive tools.
- Added InsufficientPermissionsResult type and corresponding UI components to display permission-related messages consistently across Gmail and Google Calendar tools.
This commit is contained in:
Anish Sarkar 2026-03-20 19:36:00 +05:30
parent f4c0c8c945
commit 283b4194cc
18 changed files with 423 additions and 123 deletions

View file

@ -3,8 +3,6 @@
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
CornerDownLeftIcon,
FileEditIcon,
MailIcon,
Pen,
UserIcon,
UsersIcon,
@ -66,10 +64,17 @@ interface AuthErrorResult {
connector_type?: string;
}
interface InsufficientPermissionsResult {
status: "insufficient_permissions";
connector_id: number;
message: string;
}
type CreateGmailDraftResult =
| InterruptResult
| SuccessResult
| ErrorResult
| InsufficientPermissionsResult
| AuthErrorResult;
function isInterruptResult(result: unknown): result is InterruptResult {
@ -99,6 +104,15 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
);
}
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
return (
typeof result === "object" &&
result !== null &&
"status" in result &&
(result as InsufficientPermissionsResult).status === "insufficient_permissions"
);
}
function ApprovalCard({
args,
interruptData,
@ -168,7 +182,6 @@ function ApprovalCard({
{/* Header */}
<div className="flex items-start justify-between px-5 pt-5 pb-4 select-none">
<div className="flex items-center gap-2">
<FileEditIcon className="size-4 text-muted-foreground shrink-0" />
<div>
<p className="text-sm font-semibold text-foreground">
{decided === "reject"
@ -388,12 +401,27 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
);
}
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">
<p className="text-sm font-semibold text-destructive">
Additional Gmail permissions required
</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
<p className="text-sm text-muted-foreground">{result.message}</p>
</div>
</div>
);
}
function SuccessCard({ result }: { result: SuccessResult }) {
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">
<div className="flex items-center gap-2">
<MailIcon className="size-4 text-muted-foreground shrink-0" />
<p className="text-sm font-semibold text-foreground">
{result.message || "Gmail draft created successfully"}
</p>
@ -443,6 +471,8 @@ export const CreateGmailDraftToolUI = makeAssistantToolUI<
}
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
return <SuccessCard result={result as SuccessResult} />;

View file

@ -65,10 +65,17 @@ interface AuthErrorResult {
connector_type?: string;
}
interface InsufficientPermissionsResult {
status: "insufficient_permissions";
connector_id: number;
message: string;
}
type SendGmailEmailResult =
| InterruptResult
| SuccessResult
| ErrorResult
| InsufficientPermissionsResult
| AuthErrorResult;
function isInterruptResult(result: unknown): result is InterruptResult {
@ -98,6 +105,15 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
);
}
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
return (
typeof result === "object" &&
result !== null &&
"status" in result &&
(result as InsufficientPermissionsResult).status === "insufficient_permissions"
);
}
function ApprovalCard({
args,
interruptData,
@ -387,6 +403,22 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
);
}
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">
<p className="text-sm font-semibold text-destructive">
Additional Gmail permissions required
</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
<p className="text-sm text-muted-foreground">{result.message}</p>
</div>
</div>
);
}
function SuccessCard({ result }: { result: SuccessResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
@ -442,6 +474,8 @@ export const SendGmailEmailToolUI = makeAssistantToolUI<
}
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
return <SuccessCard result={result as SuccessResult} />;

View file

@ -72,11 +72,18 @@ interface AuthErrorResult {
connector_type?: string;
}
interface InsufficientPermissionsResult {
status: "insufficient_permissions";
connector_id: number;
message: string;
}
type TrashGmailEmailResult =
| InterruptResult
| SuccessResult
| ErrorResult
| NotFoundResult
| InsufficientPermissionsResult
| AuthErrorResult;
function isInterruptResult(result: unknown): result is InterruptResult {
@ -115,6 +122,15 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
);
}
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
return (
typeof result === "object" &&
result !== null &&
"status" in result &&
(result as InsufficientPermissionsResult).status === "insufficient_permissions"
);
}
function formatDate(dateStr: string): string {
return new Date(dateStr).toLocaleDateString(undefined, { dateStyle: "medium" });
}
@ -318,6 +334,22 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
);
}
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">
<p className="text-sm font-semibold text-destructive">
Additional Gmail permissions required
</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
<p className="text-sm text-muted-foreground">{result.message}</p>
</div>
</div>
);
}
function NotFoundCard({ result }: { result: NotFoundResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border border-amber-500/50 bg-muted/30 select-none">
@ -398,6 +430,8 @@ export const TrashGmailEmailToolUI = makeAssistantToolUI<
}
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;