feat(app): show plugin display names and a Settings tooltip on the cog

Plugin rows on Settings → Plugins show the manifest name, falling back to
the id; when a name is set, the id stays visible beneath it. The row's
controls, logs sheet, confirmations and feedback use the same name.
The cog is a ToolbarButton labeled Settings, with a matching tooltip.
This commit is contained in:
Mohamed Boudra
2026-11-03 14:03:58 +01:00
parent ac099f4ff8
commit f3b0c003da
8 changed files with 167 additions and 131 deletions
@@ -22,6 +22,8 @@ import {
openTabbedSettings,
selectSettingsTab,
openTabbedSettingsLink,
expectSettingsTooltip,
expectNamedPluginRow,
} from "../support/helpers/plugin-settings";
test("display preferences survive a reload", async ({ page }) => {
@@ -57,6 +59,12 @@ test("Back to plugins returns from a plugin settings screen", async ({ page }) =
test("plugin settings open from the cog and show a tab per screen", async ({ page }) => {
await openPluginsPage(page);
await test.step("plugin rows show the display name", async () => {
await expectNamedPluginRow(page);
});
await test.step("the cog explains itself", async () => {
await expectSettingsTooltip(page);
});
await test.step("the actions menu keeps only plugin actions", async () => {
await expectActionsMenuWithoutSettings(page);
});
@@ -101,8 +101,15 @@ async function openPluginScreen(page: Page) {
await openHostSection(page, getServerId(), "plugins");
await openSettingsFromCog(page);
}
/** The cog on a plugin's row. Its label is "Settings"; the row tells plugins apart. */
function settingsButton(page: Page, pluginId: string) {
return page
.getByTestId(`plugin-row-${pluginId}`)
.filter({ visible: true })
.getByRole("button", { name: "Settings", exact: true });
}
async function openSettingsFromCog(page: Page) {
await page.getByRole("button", { name: "Settings for settings-example", exact: true }).click();
await settingsButton(page, "settings-example").click();
await expect(page.getByRole("switch", { name: "Show metadata" })).toBeVisible();
// A plugin with one settings screen shows no tabs, and the header names only the plugin.
await expect(page.getByTestId("plugin-settings-tabs")).toHaveCount(0);
@@ -122,16 +129,29 @@ export async function expectActionsMenuWithoutSettings(page: Page) {
export async function expectNoSettingsCogWithoutScreens(page: Page) {
await page.getByRole("switch", { name: "settings-example: Disable" }).click();
await expect(page.getByRole("switch", { name: "settings-example: Enable" })).toBeVisible();
await expect(
page.getByRole("button", { name: "Settings for settings-example", exact: true }),
).toHaveCount(0);
await expect(settingsButton(page, "settings-example")).toHaveCount(0);
await page.getByRole("switch", { name: "settings-example: Enable" }).click();
await expect(settingsButton(page, "settings-example")).toBeVisible();
}
export async function expectSettingsTooltip(page: Page) {
await settingsButton(page, "settings-example").hover();
await expect(page.getByRole("tooltip")).toHaveText("Settings");
await page.mouse.move(0, 0);
}
/** A manifest `name` titles the row and its controls; the id stays visible beneath it. */
export async function expectNamedPluginRow(page: Page) {
const row = page.getByTestId(`plugin-row-${TABBED_PLUGIN_ID}`).filter({ visible: true });
await expect(row.getByText(TABBED_PLUGIN_NAME, { exact: true })).toBeVisible();
await expect(row.getByText(TABBED_PLUGIN_ID, { exact: true })).toBeVisible();
await expect(
page.getByRole("button", { name: "Settings for settings-example", exact: true }),
page.getByRole("switch", { name: `${TABBED_PLUGIN_NAME}: Disable`, exact: true }),
).toBeVisible();
await expect(
page.getByRole("button", { name: `Actions for ${TABBED_PLUGIN_NAME}`, exact: true }),
).toBeVisible();
}
export async function openTabbedSettings(page: Page) {
await page.getByRole("button", { name: `Settings for ${TABBED_PLUGIN_ID}`, exact: true }).click();
await settingsButton(page, TABBED_PLUGIN_ID).click();
await expectSettingsTab(page, "General");
// The Advanced icon reads the plugin's translations; the General icon throws and is dropped.
await expect(page.getByTestId("advanced-tab-icon")).toBeVisible();
+10 -10
View File
@@ -2384,7 +2384,7 @@ export const en = {
installing: "Installing…",
logs: {
action: "Logs",
title: "Logs: {{id}}",
title: "Logs: {{name}}",
loading: "Loading logs…",
refresh: "Refresh",
refreshing: "Refreshing…",
@@ -2393,8 +2393,8 @@ export const en = {
},
status: { running: "running", disabled: "disabled", failed: "failed" },
actions: {
menu: "Actions for {{id}}",
settings: "Settings for {{id}}",
menu: "Actions for {{name}}",
settings: "Settings",
reload: "Reload",
reloading: "Reloading…",
enable: "Enable",
@@ -2405,16 +2405,16 @@ export const en = {
removing: "Removing…",
},
feedback: {
installed: "Installed {{id}}",
reload: "Reloaded {{id}}",
enable: "Enabled {{id}}",
disable: "Disabled {{id}}",
remove: "Removed {{id}}",
kept: "Kept {{id}}",
installed: "Installed {{name}}",
reload: "Reloaded {{name}}",
enable: "Enabled {{name}}",
disable: "Disabled {{name}}",
remove: "Removed {{name}}",
kept: "Kept {{name}}",
globalEnabled: "Plugins enabled",
globalDisabled: "Plugins disabled",
},
removeConfirmTitle: "Remove {{id}}?",
removeConfirmTitle: "Remove {{name}}?",
removeConfirmMessage: "This removes the configuration. The source directory is not deleted.",
states: {
offlineTitle: "Plugin host is offline",
@@ -1,7 +1,7 @@
const shared = {
logs: {
action: "Logs",
title: "Logs: {{id}}",
title: "Logs: {{name}}",
loading: "Loading logs…",
refresh: "Refresh",
refreshing: "Refreshing…",
@@ -33,8 +33,8 @@ export const pluginSettings = {
installing: "جارٍ التثبيت…",
status: { running: "قيد التشغيل", disabled: "معطّلة", failed: "فشلت" },
actions: {
menu: "إجراءات {{id}}",
settings: "إعدادات {{id}}",
menu: "إجراءات {{name}}",
settings: "الإعدادات",
reload: "إعادة تحميل",
reloading: "جارٍ إعادة التحميل…",
enable: "تفعيل",
@@ -45,16 +45,16 @@ export const pluginSettings = {
removing: "جارٍ الإزالة…",
},
feedback: {
installed: "تم تثبيت {{id}}",
reload: "تمت إعادة تحميل {{id}}",
enable: "تم تفعيل {{id}}",
disable: "تم تعطيل {{id}}",
remove: "تمت إزالة {{id}}",
kept: "تم الإبقاء على {{id}}",
installed: "تم تثبيت {{name}}",
reload: "تمت إعادة تحميل {{name}}",
enable: "تم تفعيل {{name}}",
disable: "تم تعطيل {{name}}",
remove: "تمت إزالة {{name}}",
kept: "تم الإبقاء على {{name}}",
globalEnabled: "تم تفعيل الإضافات",
globalDisabled: "تم تعطيل الإضافات",
},
removeConfirmTitle: "إزالة {{id}}؟",
removeConfirmTitle: "إزالة {{name}}؟",
removeConfirmMessage: "يؤدي هذا إلى إزالة الإعداد فقط. لن يُحذف مجلد المصدر.",
states: {
offlineTitle: "مضيف الإضافات غير متصل",
@@ -89,8 +89,8 @@ export const pluginSettings = {
installing: "Instalando…",
status: { running: "en ejecución", disabled: "desactivado", failed: "fallido" },
actions: {
menu: "Acciones para {{id}}",
settings: "Configuración de {{id}}",
menu: "Acciones para {{name}}",
settings: "Configuración",
reload: "Recargar",
reloading: "Recargando…",
enable: "Activar",
@@ -101,16 +101,16 @@ export const pluginSettings = {
removing: "Eliminando…",
},
feedback: {
installed: "{{id}} instalado",
reload: "{{id}} recargado",
enable: "{{id}} activado",
disable: "{{id}} desactivado",
remove: "{{id}} eliminado",
kept: "Se conservó {{id}}",
installed: "{{name}} instalado",
reload: "{{name}} recargado",
enable: "{{name}} activado",
disable: "{{name}} desactivado",
remove: "{{name}} eliminado",
kept: "Se conservó {{name}}",
globalEnabled: "Plugins activados",
globalDisabled: "Plugins desactivados",
},
removeConfirmTitle: "¿Eliminar {{id}}?",
removeConfirmTitle: "¿Eliminar {{name}}?",
removeConfirmMessage: "Esto elimina la configuración. El directorio de origen no se borra.",
states: {
offlineTitle: "El host de plugins está sin conexión",
@@ -145,8 +145,8 @@ export const pluginSettings = {
installing: "Installation…",
status: { running: "actif", disabled: "désactivé", failed: "échec" },
actions: {
menu: "Actions pour {{id}}",
settings: "Paramètres de {{id}}",
menu: "Actions pour {{name}}",
settings: "Paramètres",
reload: "Recharger",
reloading: "Rechargement…",
enable: "Activer",
@@ -157,16 +157,16 @@ export const pluginSettings = {
removing: "Suppression…",
},
feedback: {
installed: "{{id}} installé",
reload: "{{id}} rechargé",
enable: "{{id}} activé",
disable: "{{id}} désactivé",
remove: "{{id}} supprimé",
kept: "{{id}} conservé",
installed: "{{name}} installé",
reload: "{{name}} rechargé",
enable: "{{name}} activé",
disable: "{{name}} désactivé",
remove: "{{name}} supprimé",
kept: "{{name}} conservé",
globalEnabled: "Plugins activés",
globalDisabled: "Plugins désactivés",
},
removeConfirmTitle: "Supprimer {{id}} ?",
removeConfirmTitle: "Supprimer {{name}} ?",
removeConfirmMessage: "La configuration sera supprimée. Le dossier source ne sera pas effacé.",
states: {
offlineTitle: "L’hôte des plugins est hors ligne",
@@ -201,8 +201,8 @@ export const pluginSettings = {
installing: "インストール中…",
status: { running: "実行中", disabled: "無効", failed: "失敗" },
actions: {
menu: "{{id}} のアクション",
settings: "{{id}} の設定",
menu: "{{name}} のアクション",
settings: "設定",
reload: "再読み込み",
reloading: "再読み込み中…",
enable: "有効化",
@@ -213,16 +213,16 @@ export const pluginSettings = {
removing: "削除中…",
},
feedback: {
installed: "{{id}} をインストールしました",
reload: "{{id}} を再読み込みしました",
enable: "{{id}} を有効にしました",
disable: "{{id}} を無効にしました",
remove: "{{id}} を削除しました",
kept: "{{id}} を保持しました",
installed: "{{name}} をインストールしました",
reload: "{{name}} を再読み込みしました",
enable: "{{name}} を有効にしました",
disable: "{{name}} を無効にしました",
remove: "{{name}} を削除しました",
kept: "{{name}} を保持しました",
globalEnabled: "プラグインを有効にしました",
globalDisabled: "プラグインを無効にしました",
},
removeConfirmTitle: "{{id}} を削除しますか?",
removeConfirmTitle: "{{name}} を削除しますか?",
removeConfirmMessage: "設定のみを削除します。ソースディレクトリは削除されません。",
states: {
offlineTitle: "プラグインホストはオフラインです",
@@ -256,8 +256,8 @@ export const pluginSettings = {
installing: "설치 중…",
status: { running: "실행 중", disabled: "비활성화", failed: "실패" },
actions: {
menu: "{{id}} 작업",
settings: "{{id}} 설정",
menu: "{{name}} 작업",
settings: "설정",
reload: "다시 로드",
reloading: "다시 로드 중…",
enable: "활성화",
@@ -268,16 +268,16 @@ export const pluginSettings = {
removing: "제거 중…",
},
feedback: {
installed: "{{id}} 설치됨",
reload: "{{id}} 다시 로드됨",
enable: "{{id}} 활성화됨",
disable: "{{id}} 비활성화됨",
remove: "{{id}} 제거됨",
kept: "{{id}} 유지됨",
installed: "{{name}} 설치됨",
reload: "{{name}} 다시 로드됨",
enable: "{{name}} 활성화됨",
disable: "{{name}} 비활성화됨",
remove: "{{name}} 제거됨",
kept: "{{name}} 유지됨",
globalEnabled: "플러그인 활성화됨",
globalDisabled: "플러그인 비활성화됨",
},
removeConfirmTitle: "{{id}}을(를) 제거할까요?",
removeConfirmTitle: "{{name}}을(를) 제거할까요?",
removeConfirmMessage: "설정만 제거합니다. 소스 디렉터리는 삭제되지 않습니다.",
states: {
offlineTitle: "플러그인 호스트가 오프라인입니다",
@@ -312,8 +312,8 @@ export const pluginSettings = {
installing: "Instalando…",
status: { running: "em execução", disabled: "desativado", failed: "falhou" },
actions: {
menu: "Ações para {{id}}",
settings: "Configurações de {{id}}",
menu: "Ações para {{name}}",
settings: "Configurações",
reload: "Recarregar",
reloading: "Recarregando…",
enable: "Ativar",
@@ -324,16 +324,16 @@ export const pluginSettings = {
removing: "Removendo…",
},
feedback: {
installed: "{{id}} instalado",
reload: "{{id}} recarregado",
enable: "{{id}} ativado",
disable: "{{id}} desativado",
remove: "{{id}} removido",
kept: "{{id}} mantido",
installed: "{{name}} instalado",
reload: "{{name}} recarregado",
enable: "{{name}} ativado",
disable: "{{name}} desativado",
remove: "{{name}} removido",
kept: "{{name}} mantido",
globalEnabled: "Plugins ativados",
globalDisabled: "Plugins desativados",
},
removeConfirmTitle: "Remover {{id}}?",
removeConfirmTitle: "Remover {{name}}?",
removeConfirmMessage: "Isso remove a configuração. O diretório de origem não será excluído.",
states: {
offlineTitle: "O host de plugins está offline",
@@ -368,8 +368,8 @@ export const pluginSettings = {
installing: "Установка…",
status: { running: "работает", disabled: "отключён", failed: "ошибка" },
actions: {
menu: "Действия для {{id}}",
settings: "Настройки {{id}}",
menu: "Действия для {{name}}",
settings: "Настройки",
reload: "Перезагрузить",
reloading: "Перезагрузка…",
enable: "Включить",
@@ -380,16 +380,16 @@ export const pluginSettings = {
removing: "Удаление…",
},
feedback: {
installed: "{{id}} установлен",
reload: "{{id}} перезагружен",
enable: "{{id}} включён",
disable: "{{id}} отключён",
remove: "{{id}} удалён",
kept: "{{id}} сохранён",
installed: "{{name}} установлен",
reload: "{{name}} перезагружен",
enable: "{{name}} включён",
disable: "{{name}} отключён",
remove: "{{name}} удалён",
kept: "{{name}} сохранён",
globalEnabled: "Плагины включены",
globalDisabled: "Плагины отключены",
},
removeConfirmTitle: "Удалить {{id}}?",
removeConfirmTitle: "Удалить {{name}}?",
removeConfirmMessage: "Конфигурация будет удалена. Исходный каталог останется на месте.",
states: {
offlineTitle: "Хост плагинов не в сети",
@@ -423,8 +423,8 @@ export const pluginSettings = {
installing: "正在安装…",
status: { running: "运行中", disabled: "已禁用", failed: "失败" },
actions: {
menu: "{{id}} 的操作",
settings: "{{id}} 的设置",
menu: "{{name}} 的操作",
settings: "设置",
reload: "重新加载",
reloading: "正在重新加载…",
enable: "启用",
@@ -435,16 +435,16 @@ export const pluginSettings = {
removing: "正在移除…",
},
feedback: {
installed: "已安装 {{id}}",
reload: "已重新加载 {{id}}",
enable: "已启用 {{id}}",
disable: "已禁用 {{id}}",
remove: "已移除 {{id}}",
kept: "已保留 {{id}}",
installed: "已安装 {{name}}",
reload: "已重新加载 {{name}}",
enable: "已启用 {{name}}",
disable: "已禁用 {{name}}",
remove: "已移除 {{name}}",
kept: "已保留 {{name}}",
globalEnabled: "插件已启用",
globalDisabled: "插件已禁用",
},
removeConfirmTitle: "移除 {{id}}?",
removeConfirmTitle: "移除 {{name}}?",
removeConfirmMessage: "这会移除配置,但不会删除源目录。",
states: {
offlineTitle: "插件主机离线",
+9 -7
View File
@@ -6,11 +6,12 @@ import { router } from "expo-router";
import { ArrowLeft, Settings } from "lucide-react-native";
import type { PluginHostProps } from "@getpaseo/plugin/client";
import { Button } from "@/components/ui/button";
import { ToolbarButton, paneContentToolbarIconSize } from "@/components/ui/pane-content-toolbar";
import { SegmentedControl, type SegmentedControlOption } from "@/components/ui/segmented-control";
import { useIsCompactFormFactor } from "@/constants/layout";
import { useHostFeature } from "@/runtime/host-features";
import { useHostRuntimeClient, useHostRuntimeIsConnected, useHosts } from "@/runtime/host-runtime";
import { ICON_SIZE, type Theme } from "@/styles/theme";
import type { Theme } from "@/styles/theme";
import { PluginIconView } from "../icons";
import { useInstalledPlugin } from "../registry";
import type { InstalledPlugin } from "../types";
@@ -27,7 +28,6 @@ interface SettingsIdentity {
const ThemedSettingsIcon = withUnistyles(Settings);
const mutedIconMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
const settingsIcon = <ThemedSettingsIcon size={ICON_SIZE.md} uniProps={mutedIconMapping} />;
/** Opens the plugin's settings at its first screen. Renders nothing when it has none. */
export function PluginSettingsButton({ serverId, pluginId }: Omit<SettingsIdentity, "screenId">) {
@@ -38,15 +38,17 @@ export function PluginSettingsButton({ serverId, pluginId }: Omit<SettingsIdenti
const open = useCallback(() => {
if (firstScreenId) router.push(buildPluginSettingsRoute(serverId, pluginId, firstScreenId));
}, [serverId, pluginId, firstScreenId]);
const compact = useIsCompactFormFactor();
if (!supported || !firstScreenId) return null;
return (
<Button
variant="ghost"
leftIcon={settingsIcon}
<ToolbarButton
label={t("settings.plugins.actions.settings")}
compact={compact}
onPress={open}
accessibilityLabel={t("settings.plugins.actions.settings", { id: pluginId })}
testID={`plugin-settings-${pluginId}`}
/>
>
<ThemedSettingsIcon size={paneContentToolbarIconSize(compact)} uniProps={mutedIconMapping} />
</ToolbarButton>
);
}
@@ -29,7 +29,7 @@ import { SettingsSection } from "@/components/settings/headings/settings-section
import { resolvePluginPageState } from "@/screens/settings/plugins-page-state";
import { openPluginInstallForm } from "@/screens/settings/plugin-install-form-model";
import { pluginRegistry, useInstalledPlugins } from "@/plugins/registry";
import { usePluginList } from "@/plugins/plugin-list";
import { pluginDisplayName, usePluginList } from "@/plugins/plugin-list";
import { settingsStyles } from "@/styles/settings";
import { confirmDialog } from "@/utils/confirm-dialog";
@@ -59,15 +59,15 @@ function PluginActionsMenu({
pending: boolean;
pendingAction?: PluginRowAction;
onAction(action: PluginRowAction, plugin: PluginListItem): void;
onOpenLogs(pluginId: string): void;
onOpenLogs(plugin: PluginListItem): void;
supportsLogs: boolean;
}) {
const { t } = useTranslation();
const reload = useCallback(() => onAction("reload", plugin), [onAction, plugin]);
const remove = useCallback(() => onAction("remove", plugin), [onAction, plugin]);
const openLogs = useCallback(() => onOpenLogs(plugin.id), [onOpenLogs, plugin.id]);
const openLogs = useCallback(() => onOpenLogs(plugin), [onOpenLogs, plugin]);
const removeIcon = useMemo(() => <Trash2 size={16} color={styles.dangerIcon.color} />, []);
const menuLabel = t("settings.plugins.actions.menu", { id: plugin.id });
const menuLabel = t("settings.plugins.actions.menu", { name: pluginDisplayName(plugin) });
return (
<DropdownMenu compactMode="sheet">
@@ -125,7 +125,7 @@ function PluginRow({
pending: boolean;
pendingAction?: PluginRowAction;
onAction(action: PluginRowAction, plugin: PluginListItem): void;
onOpenLogs(pluginId: string): void;
onOpenLogs(plugin: PluginListItem): void;
supportsLogs: boolean;
}) {
const { t } = useTranslation();
@@ -142,10 +142,14 @@ function PluginRow({
() => <StatusBadge label={statusLabel} variant={badgeVariant} />,
[badgeVariant, statusLabel],
);
const name = pluginDisplayName(plugin);
// A display name hides the id that the CLI and plugin routes use, so show it beneath.
const shownId = name === plugin.id ? null : plugin.id;
const hint = useMemo(
() =>
plugin.description || plugin.installation ? (
shownId || plugin.description || plugin.installation ? (
<View>
{shownId ? <Text style={styles.pluginSource}>{shownId}</Text> : null}
{plugin.description ? (
<Text style={styles.pluginDescription}>{plugin.description}</Text>
) : null}
@@ -154,20 +158,20 @@ function PluginRow({
) : null}
</View>
) : undefined,
[plugin.description, plugin.installation],
[shownId, plugin.description, plugin.installation],
);
const toggleLabel = `${plugin.id}: ${t(
const toggleLabel = `${name}: ${t(
plugin.enabled ? "settings.plugins.actions.disable" : "settings.plugins.actions.enable",
)}`;
return (
<SettingsRow
label={plugin.id}
label={name}
labelAccessory={statusBadge}
hint={hint}
error={clientError ?? plugin.error}
testID={`plugin-row-${plugin.id}`}
>
<View style={styles.pluginControls} accessibilityLabel={`${plugin.id} ${statusLabel}`}>
<View style={styles.pluginControls} accessibilityLabel={`${name} ${statusLabel}`}>
<PluginSettingsButton serverId={serverId} pluginId={plugin.id} />
<Switch
value={plugin.enabled}
@@ -190,15 +194,16 @@ function PluginRow({
function PluginLogsSheet({
client,
pluginId,
plugin,
serverId,
onClose,
}: {
client: NonNullable<ReturnType<typeof useHostRuntimeClient>>;
pluginId: string;
plugin: PluginListItem;
serverId: string;
onClose(): void;
}) {
const pluginId = plugin.id;
const { t } = useTranslation();
const logs = useFetchQuery({
queryKey: ["plugin-logs", serverId, pluginId],
@@ -211,7 +216,7 @@ function PluginLogsSheet({
}, [logs]);
const header = useMemo<SheetHeader>(
() => ({
title: t("settings.plugins.logs.title", { id: pluginId }),
title: t("settings.plugins.logs.title", { name: pluginDisplayName(plugin) }),
actions: (
<Button variant="outline" onPress={refresh} disabled={logs.isFetching}>
{logs.isFetching
@@ -220,7 +225,7 @@ function PluginLogsSheet({
</Button>
),
}),
[logs.isFetching, pluginId, refresh, t],
[logs.isFetching, plugin, refresh, t],
);
let content = <Text style={styles.logsState}>{t("settings.plugins.logs.loading")}</Text>;
@@ -289,7 +294,7 @@ export function HostPluginsPage({ serverId }: { serverId: string }) {
),
[t],
);
const [logsPluginId, setLogsPluginId] = useState<string | null>(null);
const [logsPlugin, setLogsPlugin] = useState<PluginListItem | null>(null);
const [feedback, setFeedback] = useState<{ kind: "success" | "error"; message: string } | null>(
null,
);
@@ -343,27 +348,28 @@ export function HostPluginsPage({ serverId }: { serverId: string }) {
run: async () => {
const installed = await client.installPluginSource(input);
installForm.reset();
return t("settings.plugins.feedback.installed", { id: installed.id });
return t("settings.plugins.feedback.installed", { name: pluginDisplayName(installed) });
},
});
}, [client, installForm, installState.canSubmit, mutation, sourceInstallSupported, t]);
const action = useCallback(
(name: PluginRowAction, plugin: PluginListItem) => {
if (!client) return;
const pluginName = pluginDisplayName(plugin);
const run = async () => {
if (name === "remove") {
const confirmed = await confirmDialog({
title: t("settings.plugins.removeConfirmTitle", { id: plugin.id }),
title: t("settings.plugins.removeConfirmTitle", { name: pluginName }),
message: t("settings.plugins.removeConfirmMessage"),
confirmLabel: t("settings.plugins.actions.remove"),
destructive: true,
});
if (!confirmed) return t("settings.plugins.feedback.kept", { id: plugin.id });
if (!confirmed) return t("settings.plugins.feedback.kept", { name: pluginName });
await client.removePlugin(plugin.id);
} else {
await client[`${name}Plugin`](plugin.id);
}
return t(`settings.plugins.feedback.${name}`, { id: plugin.id });
return t(`settings.plugins.feedback.${name}`, { name: pluginName });
};
setFeedback(null);
mutation.mutate({ action: name, pluginId: plugin.id, run });
@@ -395,7 +401,7 @@ export function HostPluginsPage({ serverId }: { serverId: string }) {
const retry = useCallback(() => {
void refetchPlugins();
}, [refetchPlugins]);
const closeLogs = useCallback(() => setLogsPluginId(null), []);
const closeLogs = useCallback(() => setLogsPlugin(null), []);
if (pageState === "offline") {
return (
@@ -441,7 +447,7 @@ export function HostPluginsPage({ serverId }: { serverId: string }) {
pending={mutation.isPending}
pendingAction={pending ? pluginRowAction(mutation.variables?.action) : undefined}
onAction={action}
onOpenLogs={setLogsPluginId}
onOpenLogs={setLogsPlugin}
supportsLogs={logsSupported}
/>
);
@@ -515,10 +521,10 @@ export function HostPluginsPage({ serverId }: { serverId: string }) {
) : null}
{catalogContent}
</SettingsSection>
{client && logsPluginId ? (
{client && logsPlugin ? (
<PluginLogsSheet
client={client}
pluginId={logsPluginId}
plugin={logsPlugin}
serverId={serverId}
onClose={closeLogs}
/>
+10 -10
View File
@@ -73,16 +73,16 @@ The required root manifest is `paseo-plugin.json`:
}
```
| Field | Required | Behavior |
| -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | Yes | Default installation ID. |
| `name` | No | Non-empty display name for the registry, the website, and your plugin settings header; independent of the installation ID. |
| `icon` | No | Relative path to a PNG inside the plugin package. |
| `media` | No | Array of image or video paths inside the package, or HTTPS URLs. An empty array is valid. |
| `description` | No | Non-empty summary shown below the plugin ID in **Settings → Plugins**. |
| `translations` | No | Listing text in other languages, read by the registry and ignored by Paseo. See [other languages](/docs/plugins/publishing#other-languages). |
| `requirements` | No | Supported Paseo versions, described below. |
| `build` | No | Preparation commands, described in the CLI reference. |
| Field | Required | Behavior |
| -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | Yes | Default installation ID. |
| `name` | No | Non-empty display name for the registry, the website, your row in **Settings → Plugins**, and your settings header. Without it, Paseo shows the ID. |
| `icon` | No | Relative path to a PNG inside the plugin package. |
| `media` | No | Array of image or video paths inside the package, or HTTPS URLs. An empty array is valid. |
| `description` | No | Non-empty summary shown below the plugin name in **Settings → Plugins**. |
| `translations` | No | Listing text in other languages, read by the registry and ignored by Paseo. See [other languages](/docs/plugins/publishing#other-languages). |
| `requirements` | No | Supported Paseo versions, described below. |
| `build` | No | Preparation commands, described in the CLI reference. |
Paths are relative to `paseo-plugin.json`, use forward slashes, and cannot contain `..`
segments. Registry listings for visual plugins require an interface image even though `media` is
+2 -2
View File
@@ -73,10 +73,10 @@ my-plugin/
| 字段 | 是否必填 | 行为 |
| -------------- | -------- | ---------------------------------------------------------------------------------------------------------- |
| `id` | 是 | 默认的安装 ID。 |
| `name` | 否 | 注册表、网站和插件设置页标题使用的非空显示名称;与安装 ID 无关。 |
| `name` | 否 | 注册表、网站、**设置 → 插件** 中的插件行以及插件设置页标题使用的非空显示名称。没有 `name` 时显示插件 ID。 |
| `icon` | 否 | 插件包内 PNG 文件的相对路径。 |
| `media` | 否 | 包内图片或视频路径的数组,或 HTTPS URL。空数组是有效的。 |
| `description` | 否 | 在 **设置 → 插件** 中显示在插件 ID 下方的非空摘要。 |
| `description` | 否 | 在 **设置 → 插件** 中显示在插件名称下方的非空摘要。 |
| `translations` | 否 | 其他语言的展示文本,由注册表读取,Paseo 会忽略它。见[其他语言](/docs/plugins/publishing#other-languages)。 |
| `requirements` | 否 | 支持的 Paseo 版本,见下文。 |
| `build` | 否 | 准备命令,见 CLI 参考。 |