"use client"; import { IconArrowLeft } from "@tabler/icons-react"; import { Badge } from "@kandev/ui/badge"; import { Button } from "@kandev/ui/button"; import { CardContent, CardHeader, CardTitle } from "@kandev/ui/card"; import { Separator } from "@kandev/ui/separator"; import { PluginSlot } from "@/components/plugins/plugin-slot"; import Link from "@/components/routing/app-link"; import { useRouter } from "@/lib/routing/client-router"; import { usePlugins } from "@/hooks/domains/plugins/use-plugins"; import { SettingsCard } from "@/components/settings/settings-card"; import { useSettingsSaveContributor } from "@/components/settings/settings-save-provider"; import { PluginConfigForm } from "./plugin-config-form"; import { PluginManifestCard } from "./plugin-manifest-card"; import { PluginRepoLink } from "./plugin-repo-link"; import { PluginStatusBadge } from "./plugin-status-badge"; import { UninstallPluginDialog } from "./uninstall-plugin-dialog"; import { usePluginActions } from "./use-plugin-actions"; import { usePluginConfigForm } from "./use-plugin-config-form"; import type { PluginRecord } from "@/lib/types/plugins"; const PLUGINS_SETTINGS_HREF = "/settings/plugins"; /** * Per-plugin settings page (Settings > Plugins > ): manifest * overview plus the schema-driven settings form declared by the plugin * author via the manifest's config_schema (e.g. a GitHub plugin's PAT). * Saving restarts a running plugin so it re-reads config via the Host * GetConfig RPC. */ export function PluginDetail({ pluginId }: { pluginId: string }) { const { items, loaded } = usePlugins(); const router = useRouter(); const actions = usePluginActions(); const plugin = items.find((p) => p.id === pluginId) ?? null; const form = usePluginConfigForm(plugin); useSettingsSaveContributor({ id: `plugin-config:${pluginId}`, revision: form.revision, isDirty: form.isDirty, canSave: form.canSave, invalidReason: form.invalidReason, save: form.handleSave, discard: form.discard, }); if (!plugin) { return loaded ? : null; } return (
{/* Owner-scoped inline slot for the plugin's own settings UI, at the top (see PLUGIN-API.md). */} { await actions.confirmUninstall(); router.push(PLUGINS_SETTINGS_HREF); }} />
); } type PluginDetailHeaderProps = { plugin: PluginRecord; }; function PluginDetailHeader({ plugin }: PluginDetailHeaderProps) { return (
Plugins

{plugin.display_name}

{plugin.signed === false && ( unsigned )}
{plugin.id} ยท v{plugin.version}
{plugin.description && (

{plugin.description}

)}
); } type PluginSettingsCardProps = { plugin: PluginRecord; form: ReturnType; busy: boolean; }; function PluginSettingsCard({ plugin, form, busy }: PluginSettingsCardProps) { return ( Settings ); } function PluginSettingsBody({ plugin, form, busy }: PluginSettingsCardProps) { if (form.fields.length === 0) { return (

This plugin does not declare any settings (no config_schema in its manifest).

); } if (form.configError) { return

{form.configError}

; } if (form.configLoading) { return

Loading settings...

; } return (
{plugin.status === "active" && (

Saving restarts the plugin so the new settings take effect.

)}
); } type PluginDangerZoneProps = { plugin: PluginRecord; actions: ReturnType; }; function PluginDangerZone({ plugin, actions }: PluginDangerZoneProps) { const busy = actions.busyId === plugin.id; const canEnable = plugin.status === "disabled" || plugin.status === "registered"; const canDisable = plugin.status === "active" || plugin.status === "error"; return (
{canEnable && ( )} {canDisable && ( )}
); } function PluginNotFound({ pluginId }: { pluginId: string }) { return (
Plugins
No installed plugin with id {pluginId}.
); }