import { IconDotsVertical, IconEye, IconEyeOff, IconPlus, IconTool, IconTrash, } from "@tabler/icons-react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useState, useEffect } from "react"; import { Link } from "react-router"; import { extensionPath } from "../../extensions/path.js"; import { sendToAgentChat } from "../agent-chat.js"; import { AgentToggleButton } from "../AgentPanel.js"; import { agentNativePath } from "../api-path.js"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger, } from "../components/ui/dropdown-menu.js"; import { Popover, PopoverContent, PopoverTrigger, } from "../components/ui/popover.js"; import { PromptComposer } from "../composer/index.js"; import { useT } from "../i18n.js"; import { cn } from "../utils.js"; import { deleteOrHideExtension, invalidateExtensionRemoval, } from "./delete-extension.js"; import { TOOLS_ORDER_CHANGE_EVENT, applyToolsOrder, getToolsOrder, } from "./extension-order.js"; import { ExtensionQueryErrorState } from "./ExtensionQueryErrorState.js"; interface Extension { id: string; name: string; description?: string; icon?: string; canDelete?: boolean; globallyHidden?: boolean; source?: { mode?: "database" | "local-files"; entryPath?: string; }; } let lastCreateSubmission: { prompt: string; at: number } | null = null; function submitCreateTool( prompt: string, messageForPrompt: (prompt: string) => string, ) { const trimmed = prompt.trim(); if (!trimmed) return; const now = Date.now(); if ( lastCreateSubmission && lastCreateSubmission.prompt === trimmed && now - lastCreateSubmission.at < 2_000 ) { return; } lastCreateSubmission = { prompt: trimmed, at: now }; sendToAgentChat({ message: messageForPrompt(trimmed), submit: true, openSidebar: true, newTab: true, }); } function CreateToolInput({ className }: { className?: string }) { const t = useT(); return (

{t("extensions.whatShouldItDo")}

submitCreateTool(text, (prompt) => t("extensions.createPrompt", { prompt }), ) } />
); } export interface ExtensionsListPageProps { /** Skip the standalone extensions navigation state when embedded in Settings. */ embedded?: boolean; } export function ExtensionsListPage({ embedded = false, }: ExtensionsListPageProps = {}) { const t = useT(); const [showCreate, setShowCreate] = useState(false); const [confirmDeleteId, setConfirmDeleteId] = useState(null); const [deletingId, setDeletingId] = useState(null); const [showGloballyHidden, setShowGloballyHidden] = useState(false); const queryClient = useQueryClient(); const [toolOrderState, setToolOrderState] = useState(() => typeof window !== "undefined" ? getToolsOrder() : [], ); useEffect(() => { if (embedded) return; fetch(agentNativePath("/_agent-native/application-state/navigation"), { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ view: "extensions" }), }).catch(() => {}); }, [embedded]); useEffect(() => { if (typeof window === "undefined") return; const syncOrder = () => setToolOrderState(getToolsOrder()); window.addEventListener(TOOLS_ORDER_CHANGE_EVENT, syncOrder); window.addEventListener("storage", syncOrder); return () => { window.removeEventListener(TOOLS_ORDER_CHANGE_EVENT, syncOrder); window.removeEventListener("storage", syncOrder); }; }, []); const extensionsQuery = useQuery({ queryKey: ["extensions", { includeGloballyHidden: showGloballyHidden }], queryFn: async () => { const res = await fetch( agentNativePath( showGloballyHidden ? "/_agent-native/extensions?includeGloballyHidden=true" : "/_agent-native/extensions", ), ); if (!res.ok) throw new Error(`Failed to load extensions (${res.status})`); return res.json(); }, }); const extensions = extensionsQuery.data; const toolList = toolOrderState.length > 0 ? applyToolsOrder(extensions ?? [], toolOrderState) : (extensions ?? []); const handleCreate = (text: string) => { submitCreateTool(text, (prompt) => t("extensions.createPrompt", { prompt }), ); setShowCreate(false); }; const handleDelete = async (extension: Extension) => { setDeletingId(extension.id); const previous = queryClient.getQueryData(["extensions"]); queryClient.setQueryData(["extensions"], (old) => (old ?? []).filter((item) => item.id !== extension.id), ); try { await deleteOrHideExtension(extension); invalidateExtensionRemoval(queryClient, extension.id); } catch { if (previous) queryClient.setQueryData(["extensions"], previous); } finally { setDeletingId(null); setConfirmDeleteId(null); queryClient.invalidateQueries({ queryKey: ["extensions"] }); } }; const handleGlobalHideToggle = async (extension: Extension) => { setConfirmDeleteId(null); const action = extension.globallyHidden ? "global-unhide" : "global-hide"; try { await fetch( agentNativePath(`/_agent-native/extensions/${extension.id}/${action}`), { method: "POST" }, ); } finally { queryClient.invalidateQueries({ queryKey: ["extensions"] }); } }; return (
{!embedded ? (

{t("extensions.title")}

) : null}

{t("extensions.newExtensionTitle")}

setShowGloballyHidden(Boolean(checked)) } > {t("extensions.showHidden")} {!embedded ? : null}
{extensionsQuery.isLoading ? (
{Array.from({ length: 6 }).map((_, i) => (
))}
) : extensionsQuery.isError ? ( void extensionsQuery.refetch()} retrying={extensionsQuery.isFetching} /> ) : toolList.length === 0 ? (

{t("extensions.emptyTitle")}

{t("extensions.emptyDescription")}

) : (
{toolList.map((extension) => { const isLocalExtension = extension.source?.mode === "local-files"; return (

{extension.globallyHidden && ( )} {extension.name}

{extension.description && (

{extension.description}

)} {isLocalExtension && (

{t("extensions.localFile")}

)} setConfirmDeleteId(open ? extension.id : null) } > {!isLocalExtension && extension.canDelete !== false && (
)} {isLocalExtension ? (
{t("extensions.localFileDescription", { entryPath: extension.source?.entryPath ?? "local files", })}
) : (

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

)}
); })}
)}
); }