/** * Client side of the first-party UI extension points: how each validated * declaration is projected onto the workbench, and how its items appear on * the plugin details page. One `defineUiPoint` per contribution type. */ import type { PluginInfo, ThemeDef } from "@/bindings"; import { registerAction } from "@/platform/actions"; import { tx } from "@/platform/i18n"; import { addContributedTheme, pluginThemeRef } from "@/platform/themes"; import { currentThemeChoice, setTheme, useThemeOverride } from "@/workbench/theme"; import { useProject } from "@/platform/project"; import { Button, Kbd, T } from "@/ui"; import { addIconTheme, iconThemeKey, selectIconTheme, useSelectedIconTheme } from "./iconThemes"; import { defineUiPoint, type UiItem } from "./projection"; import { SecretsDetails, type SecretDecl } from "./secrets"; import { defineViewUiPoints } from "./uiPoints"; import { isLive, runPluginCommand } from "./store"; import styles from "./PluginsPage.module.css"; export const pluginActionId = (pluginId: string, commandId: string) => `plugin.${pluginId}.${commandId}`; interface CommandDecl { id: string; title: string; category?: string | null; when?: string | null; } function CommandsDetails({ plugin, items }: { plugin: PluginInfo; items: UiItem[] }) { const runnable = plugin.status === "active" || plugin.status === "inactive"; return ( ); } function ThemesDetails({ plugin, items }: { plugin: PluginInfo; items: UiItem[] }) { // The selection is the Appearance setting; these buttons write it as the theme pickers do. useProject((s) => s.settings?.appearance); useThemeOverride((s) => s.theme); const selected = currentThemeChoice(); const live = isLive(plugin.status); return ( ); } interface IconThemeDecl { id: string; label: string; icons: number; } function IconThemesDetails({ plugin, items }: { plugin: PluginInfo; items: UiItem[] }) { const selected = useSelectedIconTheme(); return (
    {items.map(({ decl: t }) => { const key = iconThemeKey(plugin.id, t.id); return (
  • {t.label} {tx`${t.icons} icons`} {selected === key ? ( ) : ( )}
  • ); })}
); } /** Registers the client side of every first-party UI point. */ export function defineBuiltinUiPoints(): () => void { const disposers = [ defineUiPoint("commands", { title: "Commands", project: ({ pluginId, decl }, { plugin }) => registerAction({ id: pluginActionId(pluginId, decl.id), title: decl.title, category: decl.category ?? plugin.name, icon: "package", when: decl.when ?? undefined, keywords: ["plugin", plugin.name, pluginId], description: tx`From plugin ${plugin.name}`, run: async ({ arg }) => { await runPluginCommand(pluginId, decl.id, typeof arg === "string" ? arg : null); }, }), Details: CommandsDetails, }), // Offered by every theme picker while the plugin is live; `followThemeSetting` applies the selected one. defineUiPoint("themes", { title: "Themes", project: ({ pluginId, decl }, { plugin }) => addContributedTheme({ id: pluginThemeRef(pluginId, decl.id), label: decl.name, kind: decl.kind === "light" ? "light" : "dark", origin: plugin.name, }), Details: ThemesDetails, }), defineUiPoint("iconThemes", { title: "File Icon Themes", project: ({ pluginId, decl }, { plugin }) => addIconTheme({ pluginId, id: decl.id, label: decl.label, origin: plugin.name }), Details: IconThemesDetails, }), // Stored by the backend; listed here with a field per secret. defineUiPoint("secrets", { title: "Secrets", Details: SecretsDetails }), defineViewUiPoints(), ]; return () => disposers.reverse().forEach((d) => d()); }