diff --git a/apps/web/src/components/plugins/PluginToolbarMenu.tsx b/apps/web/src/components/plugins/PluginToolbarMenu.tsx index 6e6e51ba..f99b9beb 100644 --- a/apps/web/src/components/plugins/PluginToolbarMenu.tsx +++ b/apps/web/src/components/plugins/PluginToolbarMenu.tsx @@ -1,7 +1,6 @@ import { useEffect, useState, useSyncExternalStore } from "react"; -import { Clock3, LoaderCircle, PanelRightOpen, Play, Puzzle, Settings2 } from "lucide-react"; +import { LoaderCircle, PanelRightOpen, Play, Puzzle, Settings2 } from "lucide-react"; import { useTranslation } from "react-i18next"; -import { Link } from "react-router"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -14,7 +13,7 @@ import { import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { PluginPanelDialog } from "@/components/plugins/PluginPanelDialog"; import { cn } from "@/lib/utils"; -import { getPluginDetailPath, getPluginToolbarGroups } from "@/lib/plugins/plugin-navigation"; +import { getPluginToolbarGroups } from "@/lib/plugins/plugin-navigation"; import type { EdgeEverPluginHost, RegisteredPluginAction, @@ -40,7 +39,6 @@ export const PluginToolbarMenu = ({ host, onManage, align = "end", className }: const groups = getPluginToolbarGroups(snapshot); const hasActions = groups.some((group) => group.actions.length > 0); - const pluginNames = new Map(snapshot.extensions.map((extension) => [extension.manifest.id, extension.manifest.name])); const activePanelRegistered = Boolean(activePanelPluginId && activePanelId && snapshot.panels.some( (panel) => panel.pluginId === activePanelPluginId && panel.id === activePanelId )); @@ -74,7 +72,7 @@ export const PluginToolbarMenu = ({ host, onManage, align = "end", className }: } }; - const renderAction = (action: RegisteredPluginAction, prefix: string, pluginName?: string) => { + const renderAction = (action: RegisteredPluginAction, prefix: string) => { const key = actionKey(action); return ( )} {action.title} - {pluginName ? {pluginName} : null} ); }; @@ -121,30 +118,11 @@ export const PluginToolbarMenu = ({ host, onManage, align = "end", className }: - {snapshot.recentActions.length ? ( - <> - - - {t("plugins.toolbar.recent")} - - {snapshot.recentActions.map((action) => renderAction(action, "recent", pluginNames.get(action.pluginId)))} - - - ) : null} - {groups.length > 0 ? groups.map((group, index) => (
{index > 0 ? : null} {group.name} {group.actions.map((action) => renderAction(action, group.pluginId))} - {group.hasSettings ? ( - setOpen(false)}> - - - {t("plugins.settings.title")} - - - ) : null}
)) : (
{t("plugins.toolbar.empty")}
diff --git a/apps/web/src/lib/plugins/plugin-host.test.mjs b/apps/web/src/lib/plugins/plugin-host.test.mjs index 65b5a774..e44a1d1d 100644 --- a/apps/web/src/lib/plugins/plugin-host.test.mjs +++ b/apps/web/src/lib/plugins/plugin-host.test.mjs @@ -227,7 +227,6 @@ describe("EdgeEverPluginHost", () => { expect(host.getSnapshot().commands).toHaveLength(7); expect(host.getSnapshot().panels).toEqual([{ pluginId: "org.edgeever.test-plugin", id: "fixture", title: "Fixture panel", presentation: "dialog" }]); expect(notices).toEqual(["hello from plugin"]); - expect(host.getSnapshot().recentActions[0]).toMatchObject({ id: "hello", type: "command" }); await host.runCommand("org.edgeever.test-plugin", "read-without-permission"); await host.runCommand("org.edgeever.test-plugin", "subscribe-without-read-permission"); await host.runCommand("org.edgeever.test-plugin", "replace-selection"); @@ -245,7 +244,6 @@ describe("EdgeEverPluginHost", () => { const container = {}; const disposePanel = await host.mountPanel("org.edgeever.test-plugin", "fixture", container); expect(container.mountedByFixture).toBe(true); - expect(host.getSnapshot().recentActions[0]).toMatchObject({ id: "fixture", type: "panel" }); disposePanel(); expect(container.mountedByFixture).toBe(false); const mountedDuringDisable = {}; @@ -253,7 +251,6 @@ describe("EdgeEverPluginHost", () => { await host.setEnabled("org.edgeever.test-plugin", false); expect(mountedDuringDisable.mountedByFixture).toBe(false); expect(host.getSnapshot().panels).toHaveLength(0); - expect(host.getSnapshot().recentActions).toHaveLength(0); await host.uninstall("org.edgeever.test-plugin"); expect(secrets.has("test:org.edgeever.test-plugin:token")).toBe(false); expect(secrets.has("test:org.edgeever.test-plugin:setting:token")).toBe(false); diff --git a/apps/web/src/lib/plugins/plugin-host.ts b/apps/web/src/lib/plugins/plugin-host.ts index 50f518ca..479b042d 100644 --- a/apps/web/src/lib/plugins/plugin-host.ts +++ b/apps/web/src/lib/plugins/plugin-host.ts @@ -43,7 +43,6 @@ const INSTALLED_EXTENSIONS_STORAGE_KEY = "edgeever.extensions.installed.v1"; const ACTIVE_THEME_STORAGE_KEY = "edgeever.extensions.active-theme.v1"; const STORAGE_PREFIX = "edgeever.plugin-data.v1"; const SETTINGS_STORAGE_PREFIX = "edgeever.plugin-settings.v1"; -const RECENT_ACTIONS_STORAGE_PREFIX = "edgeever.extensions.recent-actions.v1"; const readStorageItem = (key: string) => { try { @@ -172,7 +171,6 @@ export interface PluginHostSnapshot { commands: RegisteredPluginCommand[]; panels: RegisteredPluginPanel[]; embeds: RegisteredPluginEmbed[]; - recentActions: RegisteredPluginAction[]; activeThemeId: string | null; } @@ -425,8 +423,7 @@ export class EdgeEverPluginHost { private readonly eventListeners = new Map void }>>(); private extensions = readInstalledExtensions(); private activeThemeId = readStorageItem(ACTIVE_THEME_STORAGE_KEY); - private snapshot: PluginHostSnapshot = { extensions: [], commands: [], panels: [], embeds: [], recentActions: [], activeThemeId: null }; - private recentActions: RegisteredPluginAction[]; + private snapshot: PluginHostSnapshot = { extensions: [], commands: [], panels: [], embeds: [], activeThemeId: null }; private editorAdapter: PluginEditorAdapter | null = null; private navigationAdapter: PluginNavigationAdapter | null = null; private panelAdapter: PluginPanelAdapter | null = null; @@ -452,7 +449,6 @@ export class EdgeEverPluginHost { this.secretStorage = options.secretStorage ?? new WebPluginSecretStore(); this.packageStorage = options.packageStorage ?? new WebPluginPackageStore(); this.scheduleAdapter = options.scheduleAdapter; - this.recentActions = this.readRecentActions(); this.refreshSnapshot(); } @@ -647,8 +643,6 @@ export class EdgeEverPluginHost { const extension = this.requireExtension(extensionId); if (extension.manifest.type === "plugin") await this.deactivatePlugin(extensionId); this.extensions = this.extensions.filter((item) => item.manifest.id !== extensionId); - this.recentActions = this.recentActions.filter((action) => action.pluginId !== extensionId); - this.persistRecentActions(); if (this.activeThemeId === extensionId) { this.activeThemeId = null; removeStorageItem(ACTIVE_THEME_STORAGE_KEY); @@ -715,7 +709,6 @@ export class EdgeEverPluginHost { const command = this.commands.get(`${pluginId}:${commandId}`); if (!command) throw new Error("Plugin command is not registered."); await command.run(); - this.recordRecentAction({ pluginId, id: commandId, title: command.title, type: "command" }); } async mountPanel( @@ -749,7 +742,6 @@ export class EdgeEverPluginHost { if (typeof pluginDispose === "function") pluginDispose(); }; mounted.add(dispose); - this.recordRecentAction({ pluginId, id: panelId, title: panel.title, type: "panel" }); return dispose; } @@ -1517,54 +1509,7 @@ export class EdgeEverPluginHost { this.refreshSnapshot(); } - private readRecentActions(): RegisteredPluginAction[] { - try { - const parsed = JSON.parse(readStorageItem(`${RECENT_ACTIONS_STORAGE_PREFIX}:${this.scope}`) ?? "[]") as unknown; - if (!Array.isArray(parsed)) return []; - return parsed.flatMap((item) => { - if (!item || typeof item !== "object") return []; - const action = item as Partial; - if ( - typeof action.pluginId !== "string" || - typeof action.id !== "string" || - typeof action.title !== "string" || - (action.type !== "command" && action.type !== "panel") - ) return []; - return [action as RegisteredPluginAction]; - }).slice(0, 5); - } catch { - return []; - } - } - - private recordRecentAction(action: RegisteredPluginAction) { - this.recentActions = [ - action, - ...this.recentActions.filter((item) => item.pluginId !== action.pluginId || item.id !== action.id || item.type !== action.type), - ].slice(0, 5); - this.persistRecentActions(); - this.refreshSnapshot(); - } - - private persistRecentActions() { - try { - writeStorageItem(`${RECENT_ACTIONS_STORAGE_PREFIX}:${this.scope}`, JSON.stringify(this.recentActions)); - } catch { - // Recent actions are a convenience and must never make a successful plugin action fail. - } - } - private refreshSnapshot() { - const registeredActions = new Map([ - ...[...this.commands.values()].map(({ pluginId, id, title }) => [ - `command:${pluginId}:${id}`, - { pluginId, id, title, type: "command" as const }, - ] as const), - ...[...this.panels.values()].map(({ pluginId, id, title }) => [ - `panel:${pluginId}:${id}`, - { pluginId, id, title, type: "panel" as const }, - ] as const), - ]); this.snapshot = { extensions: this.extensions.map((item) => ({ ...item, manifest: { ...item.manifest } })), commands: [...this.commands.values()].map(({ pluginId, id, title }) => ({ pluginId, id, title })), @@ -1575,10 +1520,6 @@ export class EdgeEverPluginHost { presentation: presentation === "fullscreen" ? "fullscreen" : "dialog", })), embeds: [...this.embeds.values()].map(({ pluginId, type }) => ({ pluginId, type })), - recentActions: this.recentActions.flatMap((action) => { - const registered = registeredActions.get(`${action.type}:${action.pluginId}:${action.id}`); - return registered ? [registered] : []; - }), activeThemeId: this.activeThemeId, }; for (const listener of this.listeners) listener(); diff --git a/apps/web/src/lib/plugins/plugin-navigation.test.mjs b/apps/web/src/lib/plugins/plugin-navigation.test.mjs index e7f2f2d5..f86c64e9 100644 --- a/apps/web/src/lib/plugins/plugin-navigation.test.mjs +++ b/apps/web/src/lib/plugins/plugin-navigation.test.mjs @@ -17,18 +17,16 @@ describe("plugin settings navigation", () => { expect(hasPluginSettings({ type: "theme" })).toBe(false); }); - test("disabled plugins retain settings but never expose executable actions", () => { + test("does not expose disabled plugins through the command menu", () => { const groups = getPluginToolbarGroups({ extensions: [{ manifest: plugin, enabled: false }], commands: [{ pluginId: "example", id: "run" }], panels: [{ pluginId: "example", id: "panel" }], }); - expect(groups).toHaveLength(1); - expect(groups[0].hasSettings).toBe(true); - expect(groups[0].actions).toEqual([]); + expect(groups).toEqual([]); }); - test("keeps commands and panels while hiding settings-free inactive plugins and themes", () => { + test("keeps commands and panels while hiding settings-only plugins, inactive plugins, and themes", () => { const groups = getPluginToolbarGroups({ extensions: [ { manifest: { type: "plugin", id: "actions" }, enabled: true }, @@ -39,11 +37,8 @@ describe("plugin settings navigation", () => { commands: [{ pluginId: "actions", id: "run" }, { pluginId: "inactive", id: "hidden" }], panels: [{ pluginId: "actions", id: "panel" }], }); - expect(groups.map((group) => group.pluginId)).toEqual(["actions", "example"]); - expect(groups[0].hasSettings).toBe(false); + expect(groups.map((group) => group.pluginId)).toEqual(["actions"]); expect(groups[0].actions.map((action) => action.type)).toEqual(["command", "panel"]); - expect(groups[1].hasSettings).toBe(true); - expect(groups[1].actions).toEqual([]); }); test("resolves deep links and safely falls back for unsupported settings pages", () => { diff --git a/apps/web/src/lib/plugins/plugin-navigation.ts b/apps/web/src/lib/plugins/plugin-navigation.ts index 187f87a0..0bca814c 100644 --- a/apps/web/src/lib/plugins/plugin-navigation.ts +++ b/apps/web/src/lib/plugins/plugin-navigation.ts @@ -15,12 +15,11 @@ export const getPluginDetailPage = (manifest: ExtensionManifest, requestedPage: export const getPluginToolbarGroups = (snapshot: Pick) => snapshot.extensions.flatMap((extension) => { if (extension.manifest.type !== "plugin") return []; - const hasSettings = hasPluginSettings(extension.manifest); const actions: RegisteredPluginAction[] = extension.enabled ? [ ...snapshot.commands.filter((command) => command.pluginId === extension.manifest.id).map((command) => ({ ...command, type: "command" as const })), ...snapshot.panels.filter((panel) => panel.pluginId === extension.manifest.id).map((panel) => ({ ...panel, type: "panel" as const })), ] : []; - return actions.length || hasSettings - ? [{ pluginId: extension.manifest.id, name: extension.manifest.name, actions, hasSettings }] + return actions.length + ? [{ pluginId: extension.manifest.id, name: extension.manifest.name, actions }] : []; }); diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index 4e281445..2ac1fbc2 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -759,7 +759,6 @@ export const enUS = { }, toolbar: { open: "Plugins", - recent: "Recently used", empty: "Enabled plugins have not provided any commands or panels yet.", manage: "Manage plugins and themes", }, diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index 4805cbaf..3f5b3483 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -759,7 +759,6 @@ export const zhCN = { }, toolbar: { open: "插件", - recent: "最近使用", empty: "已启用的插件还没有提供命令或面板。", manage: "管理插件与主题", },