diff --git a/packages/plugin-sdk/src/frontend.ts b/packages/plugin-sdk/src/frontend.ts index dd76de797..b22e3f9d1 100644 --- a/packages/plugin-sdk/src/frontend.ts +++ b/packages/plugin-sdk/src/frontend.ts @@ -608,21 +608,18 @@ export interface DashboardCardContribution { component: ComponentType; } -export interface HomepageWidgetContribution> { +/** + * One item in a named list another plugin reads (app.registerExtension). The + * reading plugin owns the point id and the item shape. Components go under + * `components` so core can scope them to the registering plugin. + */ +export interface ExtensionContribution { id: string; /** Set by core to the registering plugin's id; a plugin never sets this itself. */ pluginId?: string; - name: string; - description: string; - category: "links" | "info" | "system" | "monitoring"; - icon: ReactNode; - defaultConfig: C; - defaultSize: { w: number; h: number }; - minSize: { w: number; h: number }; // eslint-disable-next-line @typescript-eslint/no-explicit-any - component: ComponentType; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - editFormComponent?: ComponentType; + components?: Record>; + [key: string]: unknown; } export interface SettingsComponentProps { @@ -818,7 +815,10 @@ export interface TermixApp extends TermixAppInfo { binding: KeybindingDefaultContribution, ) => Disposer; registerDashboardCard: (card: DashboardCardContribution) => Disposer; - registerHomepageWidget: (widget: HomepageWidgetContribution) => Disposer; + registerExtension: ( + pointId: string, + extension: ExtensionContribution, + ) => Disposer; registerSettingsComponent: ( componentId: string, component: ComponentType, @@ -986,8 +986,11 @@ export interface PluginHostBridge { useHostActions: () => HostActionContribution[]; useActivityTypes: () => string[]; activityTarget: (type: string) => ActivityTargetInfo | undefined; - useHomepageWidgetTypes: () => HomepageWidgetContribution[]; - homepageWidgetType: (id: string) => HomepageWidgetContribution | undefined; + useExtensions: (pointId: string) => ExtensionContribution[]; + getExtension: ( + pointId: string, + id: string, + ) => ExtensionContribution | undefined; core: PluginCoreApi; usePluginUiPreferences: (pluginId: string) => { values: Record; @@ -1333,19 +1336,21 @@ export function activityTarget(type: string): ActivityTargetInfo | undefined { } /** - * Every homepage widget type any running plugin registered with - * registerHomepageWidget, for the homepage plugin's own canvas and add-widget - * menu. Reactive: it re-renders as plugins enable and disable. + * Every item any running plugin added to `pointId` with registerExtension. + * Reactive: it re-renders as plugins enable and disable. */ -export function useHomepageWidgetTypes(): HomepageWidgetContribution[] { - return requireHost().useHomepageWidgetTypes(); +export function useExtensions( + pointId: string, +): T[] { + return requireHost().useExtensions(pointId) as T[]; } -/** A single registered homepage widget type, by id, or undefined if none. */ -export function homepageWidgetType( +/** A single item added to `pointId`, by id, or undefined if none. */ +export function getExtension( + pointId: string, id: string, -): HomepageWidgetContribution | undefined { - return requireHost().homepageWidgetType(id); +): T | undefined { + return requireHost().getExtension(pointId, id) as T | undefined; } /** diff --git a/packages/plugin-sdk/src/testing/setup.ts b/packages/plugin-sdk/src/testing/setup.ts index bc898a3c5..5cb11dc78 100644 --- a/packages/plugin-sdk/src/testing/setup.ts +++ b/packages/plugin-sdk/src/testing/setup.ts @@ -129,8 +129,8 @@ const offline = () => useHostActions: () => [], useActivityTypes: () => [], activityTarget: () => undefined, - useHomepageWidgetTypes: () => [], - homepageWidgetType: () => undefined, + useExtensions: () => [], + getExtension: () => undefined, usePluginUiPreferences: () => ({ values: {}, set: noop }), core: { logActivity: async () => {}, diff --git a/plugins/docker/src/frontend/DockerWidget.tsx b/plugins/docker/src/frontend/DockerWidget.tsx index 0098d6a8a..eeb112027 100644 --- a/plugins/docker/src/frontend/DockerWidget.tsx +++ b/plugins/docker/src/frontend/DockerWidget.tsx @@ -57,6 +57,5 @@ export const dockerWidget: WidgetDefinition = { defaultConfig: { hostId: 0 }, defaultSize: { w: GRID_SIZE * 20, h: GRID_SIZE * 14 }, minSize: { w: GRID_SIZE * 10, h: GRID_SIZE * 8 }, - component: DockerWidget, - editFormComponent: DockerWidgetEditForm, + components: { view: DockerWidget, editForm: DockerWidgetEditForm }, }; diff --git a/plugins/docker/src/frontend/homepage.ts b/plugins/docker/src/frontend/homepage.ts index e90a3e231..2028c6aa9 100644 --- a/plugins/docker/src/frontend/homepage.ts +++ b/plugins/docker/src/frontend/homepage.ts @@ -1,4 +1,5 @@ -import type { HomepageWidgetContribution } from "@termix/plugin-sdk/frontend"; +import type { ComponentType, ReactNode } from "react"; +import type { ExtensionContribution } from "@termix/plugin-sdk/frontend"; /** The homepage's grid step, in pixels. */ export const GRID_SIZE = 30; @@ -18,7 +19,18 @@ export interface WidgetEditFormProps { onChange: (config: C) => void; } -export type WidgetDefinition = Omit< - HomepageWidgetContribution, - "defaultConfig" -> & { defaultConfig: C }; +/** What app.registerExtension("homepage.widgets", ...) takes. */ +export interface WidgetDefinition extends ExtensionContribution { + id: string; + name: string; + description: string; + category: "links" | "info" | "system" | "monitoring"; + icon: ReactNode; + defaultConfig: C; + defaultSize: { w: number; h: number }; + minSize: { w: number; h: number }; + components: { + view: ComponentType>; + editForm?: ComponentType>; + }; +} diff --git a/plugins/docker/src/frontend/index.tsx b/plugins/docker/src/frontend/index.tsx index a74c64eb2..588833656 100644 --- a/plugins/docker/src/frontend/index.tsx +++ b/plugins/docker/src/frontend/index.tsx @@ -1,7 +1,6 @@ import type { ComponentType } from "react"; import { Box } from "lucide-react"; import type { - HomepageWidgetContribution, StandaloneViewProps, TabProps, TermixApp, @@ -57,9 +56,7 @@ export function activate(app: TermixApp): void { when: (host) => canUse && !!host.enableSsh && dockerEnabled(host), }); - app.registerHomepageWidget( - dockerWidget as unknown as HomepageWidgetContribution, - ); + app.registerExtension("homepage.widgets", dockerWidget); app.registerSlotContribution("onboarding.features", { actionId: "docker.feature", diff --git a/plugins/file-manager/src/frontend/homepage/homepage.ts b/plugins/file-manager/src/frontend/homepage/homepage.ts index 11074f341..cfb8fcf6a 100644 --- a/plugins/file-manager/src/frontend/homepage/homepage.ts +++ b/plugins/file-manager/src/frontend/homepage/homepage.ts @@ -1,4 +1,5 @@ -import type { HomepageWidgetContribution } from "@termix/plugin-sdk/frontend"; +import type { ComponentType, ReactNode } from "react"; +import type { ExtensionContribution } from "@termix/plugin-sdk/frontend"; /** The homepage's grid step, in pixels. */ export const GRID_SIZE = 30; @@ -18,7 +19,18 @@ export interface WidgetEditFormProps { onChange: (config: C) => void; } -export type WidgetDefinition = Omit< - HomepageWidgetContribution, - "defaultConfig" -> & { defaultConfig: C }; +/** What app.registerExtension("homepage.widgets", ...) takes. */ +export interface WidgetDefinition extends ExtensionContribution { + id: string; + name: string; + description: string; + category: "links" | "info" | "system" | "monitoring"; + icon: ReactNode; + defaultConfig: C; + defaultSize: { w: number; h: number }; + minSize: { w: number; h: number }; + components: { + view: ComponentType>; + editForm?: ComponentType>; + }; +} diff --git a/plugins/file-manager/src/frontend/index.tsx b/plugins/file-manager/src/frontend/index.tsx index 4cd2c77f3..fa34ce924 100644 --- a/plugins/file-manager/src/frontend/index.tsx +++ b/plugins/file-manager/src/frontend/index.tsx @@ -8,7 +8,11 @@ import { type TabProps, type TermixApp, } from "@termix/plugin-sdk/frontend"; -import { GRID_SIZE } from "./homepage/homepage.js"; +import { + GRID_SIZE, + type FileManagerWidgetConfig, + type WidgetDefinition, +} from "./homepage/homepage.js"; import { FileManager } from "./FileManager.tsx"; import FileManagerApp from "./FileManagerApp.tsx"; import { SftpTransferTab } from "./SftpTransferTab.tsx"; @@ -113,7 +117,7 @@ export function activate(app: TermixApp): void { fileManagerHostSetting(host, "enableFileManager", true), }); - app.registerHomepageWidget({ + app.registerExtension("homepage.widgets", { id: "file_manager_widget", name: "File Manager", description: "Embedded SFTP file manager for a configured host", @@ -122,9 +126,11 @@ export function activate(app: TermixApp): void { defaultConfig: { hostId: 0 }, defaultSize: { w: GRID_SIZE * 20, h: GRID_SIZE * 14 }, minSize: { w: GRID_SIZE * 10, h: GRID_SIZE * 8 }, - component: FileManagerWidget as never, - editFormComponent: FileManagerWidgetEditForm as never, - }); + components: { + view: FileManagerWidget, + editForm: FileManagerWidgetEditForm, + }, + } satisfies WidgetDefinition); const openHost = ((host: PluginHostRecord | null, path?: string) => openHostAction(app, host, path)) as never; diff --git a/plugins/homepage/src/frontend/index.tsx b/plugins/homepage/src/frontend/index.tsx index 314a10171..59de3d4c0 100644 --- a/plugins/homepage/src/frontend/index.tsx +++ b/plugins/homepage/src/frontend/index.tsx @@ -10,7 +10,11 @@ import { HomepagePreviewCard } from "./HomepagePreviewCard.js"; import { ServiceLinksCard } from "./ServiceLinksCard.js"; import { DashboardHomepageView } from "./DashboardHomepageView.js"; import { setHomepageApi } from "./api.js"; -import { drainQueuedWidgets } from "./widgets/WidgetRegistry.js"; +import { + WIDGET_POINT, + drainQueuedWidgets, + toExtension, +} from "./widgets/WidgetRegistry.js"; // Side-effect imports so widgets register themselves. import "./widgets/ServiceLinkWidget"; @@ -54,7 +58,7 @@ export function activate(app: TermixApp): void { app.onDispose(() => setHomepageApi(null)); for (const widget of drainQueuedWidgets()) { - app.registerHomepageWidget(widget as never); + app.registerExtension(WIDGET_POINT, toExtension(widget)); } app.registerTab("homepage", HomepageTab as ComponentType, { diff --git a/plugins/homepage/src/frontend/widgets/WidgetRegistry.ts b/plugins/homepage/src/frontend/widgets/WidgetRegistry.ts index f4affd582..033b263f0 100644 --- a/plugins/homepage/src/frontend/widgets/WidgetRegistry.ts +++ b/plugins/homepage/src/frontend/widgets/WidgetRegistry.ts @@ -1,16 +1,22 @@ import { - homepageWidgetType, - useHomepageWidgetTypes, - type HomepageWidgetContribution, + getExtension, + useExtensions, + type ExtensionContribution, } from "@termix/plugin-sdk/frontend"; +import { useMemo } from "react"; import type { WidgetTypeDefinition, WidgetTypeId } from "../types.js"; +/** + * Other plugins add widgets with app.registerExtension(WIDGET_POINT, ...), + * putting the widget under components.view and its edit form under + * components.editForm. Core only stores the list, so a plugin's widgets + * stay registered while this plugin is off. + */ +export const WIDGET_POINT = "homepage.widgets"; + /** * Each widget module calls this at import time (before activate(app) runs), - * so the definitions are queued here and app.registerHomepageWidget is - * called for each one from activate. Core owns the actual registry, the same - * one other plugins (docker, tunnels, host-metrics, file-manager) register - * their own widgets into. + * so the definitions are queued here and registered from activate. */ const queued: WidgetTypeDefinition[] = []; @@ -23,18 +29,42 @@ export function drainQueuedWidgets(): WidgetTypeDefinition[] { return queued.splice(0, queued.length); } +export function toExtension(def: WidgetTypeDefinition): ExtensionContribution { + const { component, editFormComponent, ...rest } = def; + return { + ...rest, + components: editFormComponent + ? { view: component, editForm: editFormComponent } + : { view: component }, + }; +} + function toDefinition( - widget: HomepageWidgetContribution | undefined, + extension: ExtensionContribution | undefined, ): WidgetTypeDefinition | undefined { - return widget as unknown as WidgetTypeDefinition | undefined; + const view = extension?.components?.view; + if (!extension || !view) return undefined; + const { components, ...rest } = extension; + return { + ...(rest as unknown as WidgetTypeDefinition), + component: view, + editFormComponent: components?.editForm, + }; } export function getWidgetType( id: WidgetTypeId, ): WidgetTypeDefinition | undefined { - return toDefinition(homepageWidgetType(id)); + return toDefinition(getExtension(WIDGET_POINT, id)); } export function useWidgetTypes(): WidgetTypeDefinition[] { - return useHomepageWidgetTypes() as unknown as WidgetTypeDefinition[]; + const extensions = useExtensions(WIDGET_POINT); + return useMemo( + () => + extensions + .map(toDefinition) + .filter((def): def is WidgetTypeDefinition => !!def), + [extensions], + ); } diff --git a/plugins/host-metrics/src/frontend/MetricsChartWidget.tsx b/plugins/host-metrics/src/frontend/MetricsChartWidget.tsx index b236fe0a0..815c25dde 100644 --- a/plugins/host-metrics/src/frontend/MetricsChartWidget.tsx +++ b/plugins/host-metrics/src/frontend/MetricsChartWidget.tsx @@ -236,8 +236,7 @@ export const metricsChartWidget: WidgetDefinition = { }, defaultSize: { w: GRID_SIZE * 12, h: GRID_SIZE * 6 }, minSize: { w: GRID_SIZE * 6, h: GRID_SIZE * 4 }, - component: MetricsChartWidget, - editFormComponent: MetricsChartEditForm, + components: { view: MetricsChartWidget, editForm: MetricsChartEditForm }, }; export { MetricsChartWidget }; diff --git a/plugins/host-metrics/src/frontend/index.tsx b/plugins/host-metrics/src/frontend/index.tsx index 13555a756..31f07b632 100644 --- a/plugins/host-metrics/src/frontend/index.tsx +++ b/plugins/host-metrics/src/frontend/index.tsx @@ -1,7 +1,6 @@ import type { ComponentType } from "react"; import { Activity, Server } from "lucide-react"; import type { - HomepageWidgetContribution, HostEditorSectionProps, PluginHostRecord, StandaloneViewProps, @@ -100,9 +99,7 @@ export async function activate(app: TermixApp): Promise { component: MetricsHostSection, }); - app.registerHomepageWidget( - metricsChartWidget as unknown as HomepageWidgetContribution, - ); + app.registerExtension("homepage.widgets", metricsChartWidget); // Live CPU, memory and disk bars in the terminal toolbar's expanded view. app.registerSlotContribution("terminal.toolbarStatus", { diff --git a/plugins/host-metrics/src/shared/homepage.ts b/plugins/host-metrics/src/shared/homepage.ts index 5468c024d..4a60ea77d 100644 --- a/plugins/host-metrics/src/shared/homepage.ts +++ b/plugins/host-metrics/src/shared/homepage.ts @@ -1,4 +1,5 @@ -import type { HomepageWidgetContribution } from "@termix/plugin-sdk/frontend"; +import type { ComponentType, ReactNode } from "react"; +import type { ExtensionContribution } from "@termix/plugin-sdk/frontend"; /** The homepage's grid step, in pixels. */ export const GRID_SIZE = 30; @@ -25,7 +26,18 @@ export interface WidgetEditFormProps { onChange: (config: C) => void; } -export type WidgetDefinition = Omit< - HomepageWidgetContribution, - "defaultConfig" -> & { defaultConfig: C }; +/** What app.registerExtension("homepage.widgets", ...) takes. */ +export interface WidgetDefinition extends ExtensionContribution { + id: string; + name: string; + description: string; + category: "links" | "info" | "system" | "monitoring"; + icon: ReactNode; + defaultConfig: C; + defaultSize: { w: number; h: number }; + minSize: { w: number; h: number }; + components: { + view: ComponentType>; + editForm?: ComponentType>; + }; +} diff --git a/plugins/tunnels/src/frontend/index.tsx b/plugins/tunnels/src/frontend/index.tsx index 4a71ddb22..66b2565e6 100644 --- a/plugins/tunnels/src/frontend/index.tsx +++ b/plugins/tunnels/src/frontend/index.tsx @@ -1,10 +1,6 @@ import type { ComponentType } from "react"; import { Network, Plug } from "lucide-react"; -import type { - HomepageWidgetContribution, - TabProps, - TermixApp, -} from "@termix/plugin-sdk/frontend"; +import type { TabProps, TermixApp } from "@termix/plugin-sdk/frontend"; import { TunnelTab } from "./TunnelTab"; import { HostTunnelsSection } from "./HostTunnelsSection"; import { @@ -84,7 +80,7 @@ export function activate(app: TermixApp): void { icon: Plug as ComponentType<{ className?: string }>, }); - app.registerHomepageWidget({ + app.registerExtension("homepage.widgets", { id: "tunnel_widget", name: "Tunnel Manager", description: "Embedded SSH tunnel manager for a configured host", @@ -93,9 +89,8 @@ export function activate(app: TermixApp): void { defaultConfig: { hostId: 0 }, defaultSize: { w: GRID_SIZE * 16, h: GRID_SIZE * 10 }, minSize: { w: GRID_SIZE * 8, h: GRID_SIZE * 6 }, - component: TunnelWidget, - editFormComponent: TunnelWidgetEditForm, - } as HomepageWidgetContribution); + components: { view: TunnelWidget, editForm: TunnelWidgetEditForm }, + }); // The dashboard's active tunnel counter. app.registerAction( diff --git a/src/ui/plugin-host/app.ts b/src/ui/plugin-host/app.ts index c7b1cfeaa..5af0267c6 100644 --- a/src/ui/plugin-host/app.ts +++ b/src/ui/plugin-host/app.ts @@ -35,7 +35,7 @@ import { registerDashboardCard, type DashboardCardRenderProps, } from "@/dashboard/dashboard-cards-registry"; -import { registerHomepageWidgetType } from "./homepage-widget-registry"; +import { registerExtension } from "./extension-registry"; import { registerSettingsComponent } from "@/settings/settings-components"; import { declareActionSlot, @@ -45,7 +45,6 @@ import { } from "@/shell/action-registry"; import { pluginApiFor, pluginFetch, pluginWsUrl } from "@/lib/plugin-transport"; import { registerPluginComponent } from "./component-registry"; -import type { RegisteredHomepageWidget } from "./homepage-widget-registry"; import type { LucideIcon } from "lucide-react"; import { registerLoginMethod, @@ -360,16 +359,17 @@ export function createPluginApp( ); }, - registerHomepageWidget(widget) { + registerExtension(pointId, extension) { + const components = extension.components + ? Object.fromEntries( + Object.entries(extension.components).map(([name, component]) => [ + name, + scoped(component), + ]), + ) + : undefined; return track( - registerHomepageWidgetType({ - ...widget, - pluginId, - component: scoped(widget.component), - editFormComponent: widget.editFormComponent - ? scoped(widget.editFormComponent) - : undefined, - } as unknown as RegisteredHomepageWidget), + registerExtension(pointId, { ...extension, components, pluginId }), ); }, diff --git a/src/ui/plugin-host/bridge.ts b/src/ui/plugin-host/bridge.ts index 67b5a688b..299695c93 100644 --- a/src/ui/plugin-host/bridge.ts +++ b/src/ui/plugin-host/bridge.ts @@ -2,15 +2,12 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { enabledHostProtocols } from "@/sidebar/host-protocols"; import { useHostActions } from "@/sidebar/host-contributions"; import { tabTypeForActivity, useTabTypes } from "@/shell/tab-registry"; -import { - getHomepageWidgetType, - useHomepageWidgetTypes, -} from "./homepage-widget-registry"; +import { getExtension, useExtensions } from "./extension-registry"; import { useTranslation as useI18nTranslation } from "react-i18next"; import { toast } from "sonner"; import { __setPluginHost, - type HomepageWidgetContribution, + type ExtensionContribution, type HostActionContribution, type PluginHostBridge, type PluginHostRecord, @@ -380,11 +377,9 @@ export const pluginHostBridge: PluginHostBridge = { return { icon: def.icon, tab: def.id, titleKey: def.titleKey }; }, - useHomepageWidgetTypes: () => - useHomepageWidgetTypes() as unknown as HomepageWidgetContribution[], - homepageWidgetType: (id) => - getHomepageWidgetType(id) as unknown as - HomepageWidgetContribution | undefined, + useExtensions: (pointId) => useExtensions(pointId) as ExtensionContribution[], + getExtension: (pointId, id) => + getExtension(pointId, id) as ExtensionContribution | undefined, usePluginUiPreferences: (pluginId) => { const ctx = useUiPreferencesContext(); diff --git a/src/ui/plugin-host/extension-registry.ts b/src/ui/plugin-host/extension-registry.ts new file mode 100644 index 000000000..b38c9ada2 --- /dev/null +++ b/src/ui/plugin-host/extension-registry.ts @@ -0,0 +1,50 @@ +import { createRegistry, type Registry } from "@/lib/registry"; + +/** + * Named lists any plugin can add to through app.registerExtension. The plugin + * that reads a point decides its id and item shape; core only stores them, so + * contributions survive the reader being off and core never learns what a + * point is for. + */ +export interface RegisteredExtension { + id: string; + pluginId?: string; + [key: string]: unknown; +} + +const points = new Map>(); + +function point(id: string): Registry { + let registry = points.get(id); + if (!registry) { + registry = createRegistry(); + points.set(id, registry); + } + return registry; +} + +export function registerExtension( + pointId: string, + extension: RegisteredExtension, +): () => void { + return point(pointId).register(extension); +} + +export function getExtension( + pointId: string, + id: string, +): RegisteredExtension | undefined { + return point(pointId).get(id); +} + +export function listExtensions(pointId: string): RegisteredExtension[] { + return point(pointId).list(); +} + +export function useExtensions(pointId: string): RegisteredExtension[] { + return point(pointId).useList(); +} + +export function resetExtensions(): void { + for (const registry of points.values()) registry.reset(); +} diff --git a/src/ui/plugin-host/homepage-widget-registry.ts b/src/ui/plugin-host/homepage-widget-registry.ts deleted file mode 100644 index 5dc2ab8b9..000000000 --- a/src/ui/plugin-host/homepage-widget-registry.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { ComponentType } from "react"; -import { createRegistry } from "@/lib/registry"; - -/** - * A homepage widget type any plugin contributes through - * app.registerHomepageWidget. Core owns this registry so the homepage - * plugin's canvas can read it without every widget-contributing plugin - * depending on homepage being enabled. - */ -export interface RegisteredHomepageWidget { - id: string; - pluginId?: string; - name: string; - description: string; - category: "links" | "info" | "system" | "monitoring"; - icon: React.ReactNode; - defaultConfig: Record; - defaultSize: { w: number; h: number }; - minSize: { w: number; h: number }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - component: ComponentType; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - editFormComponent?: ComponentType; -} - -const registry = createRegistry(); - -export const registerHomepageWidgetType = registry.register; -export const getHomepageWidgetType = registry.get; -export const useHomepageWidgetTypes = registry.useList; -export const resetHomepageWidgetTypes = registry.reset; diff --git a/src/ui/tests/plugin-host/registries.test.tsx b/src/ui/tests/plugin-host/registries.test.tsx index a8bed1dd3..20dd302e9 100644 --- a/src/ui/tests/plugin-host/registries.test.tsx +++ b/src/ui/tests/plugin-host/registries.test.tsx @@ -3,9 +3,9 @@ import { useState } from "react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { Puzzle } from "lucide-react"; import { - homepageWidgetType, + getExtension, useActivityTypes, - useHomepageWidgetTypes, + useExtensions, useHostActions, usePermission, useTranslation, @@ -39,7 +39,7 @@ import { resetRegisteredRailItems } from "@/sidebar/rail-items"; import { resetTabTypes } from "@/shell/tab-registry"; import { resetPanels } from "@/shell/panel-registry"; import { resetDashboardCards } from "@/dashboard/dashboard-cards-registry"; -import { resetHomepageWidgetTypes } from "@/plugin-host/homepage-widget-registry"; +import { resetExtensions } from "@/plugin-host/extension-registry"; import { resetSettingsComponents } from "@/settings/settings-components"; import { resetActionRegistry } from "@/shell/action-registry"; import { resetPluginStore } from "@/plugin-host/plugin-store"; @@ -132,16 +132,11 @@ function activate(app: TermixApp) { ), }); app.registerSettingsComponent("probe", PermissionProbe); - app.registerHomepageWidget({ + app.registerExtension("fixture.widgets", { id: "fixture-widget", name: "Fixture Widget", - description: "A fixture widget", - category: "info", icon: , - defaultConfig: {}, - defaultSize: { w: 4, h: 4 }, - minSize: { w: 2, h: 2 }, - component: () => fixture widget, + components: { view: () => fixture widget }, }); } @@ -168,7 +163,7 @@ afterEach(async () => { resetHostEditorSections(); resetPaletteEntries(); resetPluginStore(); - resetHomepageWidgetTypes(); + resetExtensions(); }); const host = (overrides: Partial = {}) => @@ -294,15 +289,33 @@ describe("registries through the app object", () => { expect(await screen.findByText(/fixture_activity/)).toBeTruthy(); }); - it("registers a homepage widget type readable through the SDK", async () => { + it("adds an extension other plugins read by point id", async () => { await mount(); - expect(homepageWidgetType("fixture-widget")?.name).toBe("Fixture Widget"); + const widget = getExtension("fixture.widgets", "fixture-widget"); + expect(widget?.name).toBe("Fixture Widget"); + expect(widget?.pluginId).toBe("fixture"); + expect(getExtension("other.point", "fixture-widget")).toBeUndefined(); + const View = widget?.components?.view; function Probe() { - const types = useHomepageWidgetTypes(); - return {types.map((w) => w.id).join(",")}; + const items = useExtensions("fixture.widgets"); + return {items.map((w) => w.id).join(",")}; } - render(); + render( + <> + + {View && } + , + ); expect(await screen.findByText("fixture-widget")).toBeTruthy(); + expect(await screen.findByText("fixture widget")).toBeTruthy(); + }); + + it("drops a plugin's extensions when it deactivates", async () => { + await mount(); + expect(getExtension("fixture.widgets", "fixture-widget")).toBeTruthy(); + await rendered?.deactivate(); + rendered = null; + expect(getExtension("fixture.widgets", "fixture-widget")).toBeUndefined(); }); it("offers a per-host palette entry that runs against the shell", async () => {