"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useIsMobile } from "@/hooks/useIsMobile"; import { useI18n } from "@/hooks/useI18n"; import type { ExtensionInfo, ExtensionsResponse } from "@/lib/api-types"; function shortenPath(path: string): string { return path.replace(/^\/(?:Users|home)\/[^/]+/, "~"); } function statusColor(status: ExtensionInfo["status"]): string { if (status === "enabled") return "var(--accent)"; if (status === "blocked") return "#d97706"; return "var(--text-dim)"; } function statusLabel(status: ExtensionInfo["status"], t: ReturnType["t"]): string { if (status === "enabled") return t("extensions.enabled"); if (status === "blocked") return t("extensions.blocked"); return t("extensions.disabled"); } function scopeLabel(scope: ExtensionInfo["scope"], t: ReturnType["t"]): string { if (scope === "project") return t("extensions.project"); if (scope === "builtin") return t("extensions.builtin"); return t("extensions.app"); } function extensionKey(extension: ExtensionInfo): string { return `${extension.scope}:${extension.path}`; } export function ExtensionsConfig({ cwd, onCloseAction }: { cwd: string; onCloseAction: () => void }) { const isMobile = useIsMobile(); const { t } = useI18n(); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selected, setSelected] = useState(null); const loadExtensions = useCallback(async () => { setLoading(true); setError(null); try { const response = await fetch(`/api/extensions?cwd=${encodeURIComponent(cwd)}`); const next = (await response.json()) as ExtensionsResponse & { error?: string }; if (!response.ok || next.error) throw new Error(next.error ?? `HTTP ${response.status}`); setData(next); setSelected((current) => ( current && next.extensions.some((extension) => extensionKey(extension) === current) ? current : next.extensions[0] ? extensionKey(next.extensions[0]) : null )); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } }, [cwd]); useEffect(() => { void loadExtensions(); }, [loadExtensions]); const extensions = useMemo(() => data?.extensions ?? [], [data?.extensions]); const selectedExtension = extensions.find((extension) => extensionKey(extension) === selected) ?? null; const enabledCount = extensions.filter((extension) => extension.status === "enabled").length; const blockedCount = extensions.filter((extension) => extension.status === "blocked").length; const disabledCount = extensions.filter((extension) => extension.status === "disabled").length; const hasBlockedProjectExtensions = blockedCount > 0 && !data?.projectResourcesLoaded; const groupedExtensions = useMemo(() => { const groups: Array<{ scope: ExtensionInfo["scope"]; extensions: ExtensionInfo[] }> = []; for (const scope of ["project", "builtin", "global"] as const) { const scoped = extensions.filter((extension) => extension.scope === scope); if (scoped.length > 0) groups.push({ scope, extensions: scoped }); } return groups; }, [extensions]); return (
{ if (event.target === event.currentTarget) onCloseAction(); }} >
{t("common.extensions")} {shortenPath(cwd)}
{hasBlockedProjectExtensions && (
{t("extensions.projectResourcesBlocked")}
)}
{loading ? (
{t("i18n.loading")}
) : error ? (
{error}
) : groupedExtensions.length === 0 ? (
{t("extensions.noExtensions")}
) : ( groupedExtensions.map((group) => (
{scopeLabel(group.scope, t)}
{group.extensions.map((extension) => { const key = extensionKey(extension); const isSelected = selected === key; return (
setSelected(key)} style={{ display: "flex", alignItems: "center", gap: 7, padding: "8px", borderRadius: 5, cursor: "pointer", background: isSelected ? "var(--bg-selected)" : "none" }} onMouseEnter={(event) => { if (!isSelected) event.currentTarget.style.background = "var(--bg-hover)"; }} onMouseLeave={(event) => { if (!isSelected) event.currentTarget.style.background = "none"; }} > {extension.name}
); })}
)) )}
{selectedExtension ? (
{selectedExtension.name} {scopeLabel(selectedExtension.scope, t)}
{t("extensions.status")}
{statusLabel(selectedExtension.status, t)}
{t("extensions.source")}
{selectedExtension.source}
{t("extensions.path")}
{shortenPath(selectedExtension.path)}
) : !loading && !error ? (
{t("extensions.noExtensions")}
) : null}
{data ? `${extensions.length} ${t("extensions.total")} · ${enabledCount} ${t("extensions.enabled")} · ${disabledCount} ${t("extensions.disabled")} · ${blockedCount} ${t("extensions.blocked")}${data.diagnostics.length ? ` · ${data.diagnostics.length} ${t("extensions.diagnostics")}` : ""}` : ""}
); }