import { IconArrowLeft, IconDeviceFloppy, IconDots, IconTrash, IconX, } from "@tabler/icons-react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useEffect, useMemo, useState } from "react"; import { Link, useNavigate } from "react-router"; import { extensionPath } from "../../extensions/path.js"; import { injectSessionReplayIframeBootstrap } from "../../extensions/session-replay-iframe.js"; import { SESSION_REPLAY_IFRAME_ATTRIBUTE } from "../../session-replay-iframe-protocol.js"; import { agentNativePath } from "../api-path.js"; import { Popover, PopoverContent, PopoverTrigger, } from "../components/ui/popover.js"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "../components/ui/tooltip.js"; import { useT } from "../i18n.js"; import { cn } from "../utils.js"; import { deleteOrHideExtension, invalidateExtensionRemoval, } from "./delete-extension.js"; import { ExtensionQueryErrorState } from "./ExtensionQueryErrorState.js"; interface SlotDeclaration { id: string; extensionId: string; slotId: string; } interface Extension { id: string; name: string; description?: string; content?: string; canDelete?: boolean; } export interface ExtensionEditorProps { extensionId?: string; } export function ExtensionEditor({ extensionId }: ExtensionEditorProps) { const t = useT(); const navigate = useNavigate(); const queryClient = useQueryClient(); const isEdit = !!extensionId; const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [content, setContent] = useState(""); const [saving, setSaving] = useState(false); const [deleting, setDeleting] = useState(false); const [menuOpen, setMenuOpen] = useState(false); const [confirmingDelete, setConfirmingDelete] = useState(false); const previewHtml = useMemo( () => injectSessionReplayIframeBootstrap(content), [content], ); const slotsQuery = useQuery({ queryKey: ["extension-slots", extensionId], queryFn: async () => { const res = await fetch( agentNativePath(`/_agent-native/slots/extension/${extensionId}`), ); if (!res.ok) { throw new Error(`Failed to load extension slots (${res.status})`); } return res.json(); }, enabled: isEdit && menuOpen, }); const slots = slotsQuery.data ?? []; const existingToolQuery = useQuery({ queryKey: ["extension", extensionId], queryFn: async () => { const res = await fetch( agentNativePath(`/_agent-native/extensions/${extensionId}`), ); if (!res.ok) throw new Error("Failed to fetch extension"); return res.json(); }, enabled: isEdit, }); const existingTool = existingToolQuery.data; useEffect(() => { if (existingTool) { setName(existingTool.name ?? ""); setDescription(existingTool.description ?? ""); setContent(existingTool.content ?? ""); } }, [existingTool]); const handleSave = async () => { if (!name.trim()) return; setSaving(true); try { const body = JSON.stringify({ name: name.trim(), description: description.trim() || undefined, content, }); if (isEdit) { const res = await fetch( agentNativePath(`/_agent-native/extensions/${extensionId}`), { method: "PUT", headers: { "Content-Type": "application/json" }, body, }, ); if (!res.ok) throw new Error("Update failed"); queryClient.invalidateQueries({ queryKey: ["extension", extensionId] }); queryClient.invalidateQueries({ queryKey: ["extensions"] }); navigate(extensionPath(extensionId, name.trim())); } else { const res = await fetch(agentNativePath("/_agent-native/extensions"), { method: "POST", headers: { "Content-Type": "application/json" }, body, }); if (!res.ok) throw new Error("Create failed"); const created = await res.json(); queryClient.invalidateQueries({ queryKey: ["extensions"] }); navigate(extensionPath(created.id, created.name ?? name.trim())); } } finally { setSaving(false); } }; const handleDelete = async () => { if (!extensionId) return; setDeleting(true); const prev = queryClient.getQueryData(["extensions"]); try { queryClient.setQueryData(["extensions"], (old) => (old ?? []).filter((t) => t.id !== extensionId), ); await deleteOrHideExtension({ id: extensionId, canDelete: existingTool?.canDelete, }); invalidateExtensionRemoval(queryClient, extensionId); slots.forEach((s) => queryClient.invalidateQueries({ queryKey: ["slot-installs", s.slotId], }), ); navigate("/extensions"); } catch { if (prev) queryClient.setQueryData(["extensions"], prev); } finally { setDeleting(false); setConfirmingDelete(false); setMenuOpen(false); } }; const handleRemoveFromSlot = async (slotId: string) => { if (!extensionId) return; try { await fetch( agentNativePath( `/_agent-native/slots/${encodeURIComponent(slotId)}/install/${encodeURIComponent(extensionId)}`, ), { method: "DELETE" }, ); } finally { queryClient.invalidateQueries({ queryKey: ["slot-installs", slotId] }); } }; if (isEdit && existingToolQuery.isLoading) { return (
); } if (isEdit && existingToolQuery.isError) { return ( void existingToolQuery.refetch()} retrying={existingToolQuery.isFetching} /> ); } return (

{isEdit ? t("extensions.editExtension") : t("extensions.newExtension")}

{isEdit && ( { setMenuOpen(o); if (!o) setConfirmingDelete(false); }} > {t("extensions.moreOptions")} {!confirmingDelete ? ( <>

{t("extensions.appearsIn")}

{slotsQuery.isError ? ( void slotsQuery.refetch()} retrying={slotsQuery.isFetching} /> ) : slots.length === 0 ? (

{t("extensions.noWidgetAreas")}

) : (

{t("extensions.widgetAreaCount", { count: slots.length, })}

)}
{slots.length > 0 && (
{slots.map((s) => (
{s.slotId} {t("extensions.removeFromWidgetAreaForMe")}
))}
)}
) : (

{existingTool?.canDelete === false ? t("extensions.removeQuestion", { name }) : t("extensions.deleteQuestion", { name })}{" "} {existingTool?.canDelete === false ? t("extensions.hideForYouDescription") : t("extensions.deleteEverywhereConfirmation")}

)}
)}
setName(e.target.value)} placeholder={t("extensions.namePlaceholder")} className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background" />