Files
wehub-resource-sync e30e75b5d4
Code Quality / Oxlint + Oxfmt (push) Waiting to run
Code Quality / Template Sync (push) Waiting to run
Code Quality / Build Changed Packages (push) Waiting to run
Code Quality / Test Changed Packages (push) Waiting to run
Deploy Expo Example / Deploy Production (push) Waiting to run
Deploy Ink Example / Deploy Production (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.12) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Waiting to run
Deploy Shadcn Registry / Deploy Production (push) Waiting to run
Template Metrics / LOC + Bundle Size (push) Waiting to run
Changesets / Create Version PR (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

154 lines
4.5 KiB
TypeScript

"use client";
import { useState } from "react";
import {
type OpenCodePermissionRequest,
type OpenCodePermissionResponse,
useOpenCodeRuntimeExtras,
} from "@assistant-ui/react-opencode";
import {
CheckCircle2Icon,
LoaderIcon,
ShieldAlertIcon,
XCircleIcon,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
const formatPermissionReply = (reply: OpenCodePermissionResponse) => {
switch (reply) {
case "always":
return "Always allow";
case "once":
return "Allowed once";
case "reject":
return "Rejected";
}
};
export const OpenCodePermissionCard = ({
request,
state,
reply,
}: {
request: OpenCodePermissionRequest;
state: "pending" | "resolved";
reply?: OpenCodePermissionResponse;
}) => {
const { replyToPermission } = useOpenCodeRuntimeExtras();
const [submittingReply, setSubmittingReply] =
useState<OpenCodePermissionResponse | null>(null);
const [error, setError] = useState<string | null>(null);
const submitReply = async (nextReply: OpenCodePermissionResponse) => {
setSubmittingReply(nextReply);
setError(null);
try {
await replyToPermission(request.id, nextReply);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to submit reply.");
setSubmittingReply(null);
}
};
const isPending = state === "pending";
return (
<Card className="gap-4 py-4">
<CardHeader className="px-4">
<div className="flex items-start gap-3">
<div className="rounded-md bg-amber-500/10 p-2 text-amber-700">
{isPending ? (
<ShieldAlertIcon className="size-4" />
) : reply === "reject" ? (
<XCircleIcon className="size-4" />
) : (
<CheckCircle2Icon className="size-4" />
)}
</div>
<div className="min-w-0 flex-1">
<CardTitle className="text-sm">
{isPending ? "Approval required" : formatPermissionReply(reply!)}
</CardTitle>
<CardDescription className="mt-1 text-xs">
{request.title ??
`OpenCode requested approval for ${request.toolName ?? request.permission}.`}
</CardDescription>
</div>
<Badge variant={isPending ? "secondary" : "outline"}>
{request.toolName ?? request.permission}
</Badge>
</div>
</CardHeader>
{(request.patterns.length > 0 || request.toolInput !== undefined) && (
<CardContent className="space-y-3 px-4">
{request.patterns.length > 0 ? (
<div className="flex flex-wrap gap-2">
{request.patterns.map((pattern) => (
<Badge key={pattern} variant="outline" className="font-mono">
{pattern}
</Badge>
))}
</div>
) : null}
{request.toolInput !== undefined ? (
<pre className="bg-muted overflow-x-auto rounded-md p-3 text-xs">
{JSON.stringify(request.toolInput, null, 2)}
</pre>
) : null}
</CardContent>
)}
{isPending ? (
<CardFooter className="flex flex-wrap gap-2 px-4">
<Button
size="sm"
onClick={() => submitReply("once")}
disabled={submittingReply !== null}
>
{submittingReply === "once" && (
<LoaderIcon className="size-4 animate-spin" />
)}
Allow once
</Button>
<Button
size="sm"
variant="secondary"
onClick={() => submitReply("always")}
disabled={submittingReply !== null}
>
{submittingReply === "always" && (
<LoaderIcon className="size-4 animate-spin" />
)}
Always allow
</Button>
<Button
size="sm"
variant="outline"
onClick={() => submitReply("reject")}
disabled={submittingReply !== null}
>
{submittingReply === "reject" && (
<LoaderIcon className="size-4 animate-spin" />
)}
Reject
</Button>
{error ? (
<p className="text-destructive w-full text-xs">{error}</p>
) : null}
</CardFooter>
) : null}
</Card>
);
};