diff --git a/apps/desktop/src/components/plugins/PluginWorkbenchHost.vue b/apps/desktop/src/components/plugins/PluginWorkbenchHost.vue
index 44b35797a5..e2bb5d71a9 100644
--- a/apps/desktop/src/components/plugins/PluginWorkbenchHost.vue
+++ b/apps/desktop/src/components/plugins/PluginWorkbenchHost.vue
@@ -21,6 +21,7 @@ import { buildPluginEditorAppearance } from "@/lib/plugins/pluginAppearance";
import { downloadPluginFile, cancelPluginDownload } from "@/lib/plugins/pluginFileDownload";
import type { InstalledPlugin, PluginUiContribution } from "@/types/database";
import { useI18n } from "vue-i18n";
+import { getCachedPluginUiHtml, setCachedPluginUiHtml } from "@/lib/plugins/pluginUiHtmlCache";
import { useTheme } from "@/composables/useTheme";
import { useSettingsStore } from "@/stores/settingsStore";
import { useConnectionStore } from "@/stores/connectionStore";
@@ -379,6 +380,20 @@ function createBridge() {
openWorkbench: async (pluginId, contributionId, context, options) => emit("openWorkbench", pluginId, contributionId, context, options),
openFilesystem: async (pluginId, providerId, context) => emit("openFilesystem", pluginId, providerId, context),
reopenConnection: (pluginId, connectionId) => useConnectionStore().reopenPluginConnection(connectionId, pluginId),
+ // PR-A4 generic extension point: a read-only, secret-free, plugin-scoped connection list (for in-panel connection switching).
+ listConnections: (ownerPluginId) => {
+ const providerIds = new Set((props.plugin.manifest.contributions || []).filter((candidate) => candidate.type === "connection-provider").map((candidate) => candidate.id));
+ if (ownerPluginId !== props.plugin.manifest.id) return [];
+ return useConnectionStore()
+ .connections.filter((connection) => providerIds.has(connection.plugin_connection_provider ?? ""))
+ .map((connection) => ({
+ id: connection.id,
+ name: connection.name,
+ providerId: connection.plugin_connection_provider ?? "",
+ connectionType: connection.plugin_connection_type,
+ readOnly: connection.read_only === true,
+ }));
+ },
// Both plan calls carry the plugin's declared `host.plans:read` gate in the
// bridge; the backend owns EXPLAIN generation, the timeout, and the plan cap.
getPlanCapabilities: (connectionId) => api.getPluginPlanCapabilities(connectionId),
@@ -468,19 +483,34 @@ function localUiAssetPath(source: string): string | undefined {
}
async function inlineLocalUiAssets(html: string, pluginId: string): Promise<{ html: string; entryDirectory: string }> {
+ // Shipped ui builds usually inline every asset into one HTML document.
+ // Parsing and re-serializing a multi-megabyte document is pure overhead when
+ // there is nothing local to inline — pre-check before touching DOMParser.
+ if (!/