34 / 122 · Tool use
Permission grant
Granting a capability rather than approving one action, with the reach spelled out.
Filesystem accessrequested by filesystem-mcp
this grantsRead and write files under the workspaceRun commands you have already approvedNo network access
Installation
npx shadcn@latest add "@assistant-ui/elements-permission-grant"
Usage
import { PermissionGrant } from "@/components/elements/permission-grant";
<PermissionGrant
capability="Filesystem access"
requester="filesystem-mcp"
reach={reach}
scope="pending"
onGrant={setScope}
/>Props
capability*stringWhat is being granted, named as a capability rather than an action.
requester*stringWho is asking.
reach*string[]Exactly what the grant covers, and what it does not. Write the limits in too.
scope*"pending" | "session" | "always" | "denied"pending shows the choices; anything else shows the settled outcome.
onGrant(scope: GrantScope) => voidCalled with the scope the user chose.
classNamestringExtra classes merged onto the root.
Source
permission-grant.tsx"use client";
import { KeyRoundIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { field, inkButton, mono, paper } from "./surfaces";
export type GrantScope = "session" | "always" | "denied";
export function PermissionGrant({
capability,
requester,
reach,
scope,
onGrant,
className,
}: {
capability: string;
requester: string;
reach: readonly string[];
scope: GrantScope | "pending";
onGrant?: (scope: GrantScope) => void;
className?: string;
}) {
return (
<div
className={cn(
paper,
"flex w-full max-w-sm flex-col gap-3.5 rounded-[20px] p-4",
className,
)}
>
<div className="flex items-center gap-2.5">
<span className="bg-foreground/[0.05] text-foreground/45 flex size-7 shrink-0 items-center justify-center rounded-lg">
<KeyRoundIcon className="size-3.5" />
</span>
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-[13.5px] font-medium">
{capability}
</span>
<span className="text-foreground/45 truncate text-xs">
requested by {requester}
</span>
</div>
</div>
<div className="flex flex-col gap-1">
<span className={cn(mono, "text-foreground/30")}>this grants</span>
{reach.map((item) => (
<span
key={item}
className="text-foreground/60 flex items-baseline gap-2 text-xs"
>
<span
aria-hidden
className="bg-foreground/20 size-1 rounded-full"
/>
{item}
</span>
))}
</div>
<div className="flex h-8 items-center justify-end gap-2">
{scope === "pending" ? (
<>
<button
type="button"
onClick={() => onGrant?.("denied")}
className="text-foreground/55 hover:bg-foreground/[0.06] hover:text-foreground/90 h-8 rounded-full px-3 text-xs font-medium transition-[background-color,color,scale] duration-150 active:scale-[0.96]"
>
Deny
</button>
<button
type="button"
onClick={() => onGrant?.("session")}
className="text-foreground/55 hover:bg-foreground/[0.06] hover:text-foreground/90 h-8 rounded-full px-3 text-xs font-medium transition-[background-color,color,scale] duration-150 active:scale-[0.96]"
>
This session
</button>
<button
type="button"
onClick={() => onGrant?.("always")}
className={cn(
inkButton,
"flex h-8 items-center rounded-full px-3 text-xs font-medium",
)}
>
Always
</button>
</>
) : (
<span
key={scope}
className={cn(
field,
mono,
"fade-in animate-in text-foreground/55 rounded-full px-2.5 py-1.5 duration-300",
)}
>
{scope === "denied" ? "denied" : `granted · ${scope}`}
</span>
)}
</div>
</div>
);
}