diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index cae06656ca..8a2d66a997 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -71,6 +71,7 @@ import { rememberExternalSqlFileTarget, resolveExternalSqlFileTarget, unassociat import { externalSqlFileOpenErrorMessage, externalSqlEditorMaxBytes, isSqlFilePath, readBrowserSqlFile, sqlFileTitleFromPath } from "@/lib/sql/sqlFileOpen"; import type { ConnectionConfig, DatabaseType, ObjectBrowserFilter, ObjectSourceKind, QueryTab, TabOutputView, TreeNode } from "@/types/database"; import type { PluginCenterFocus } from "@/lib/plugins/pluginCenterNavigation"; +import { parsePluginInstallDeepLink } from "@/lib/plugins/pluginInstallDeepLink"; import { parseConnectionDeepLink, type ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink"; import { parseAiConfigDeepLink, type AiConfigDeepLinkDraft } from "@/lib/ai/aiConfigDeepLink"; import { activeDesktopAiRuns, blockingDesktopAiRunsForQuit } from "@/lib/ai/desktopAiRunRegistry"; @@ -999,6 +1000,7 @@ const { setupTauriListeners, cleanupTauriListeners } = useTauriEvents({ openConnectionDeepLink, closeActiveSurface, openAiConfigDeepLink, + openPluginInstallDeepLink, }); const { showCloseActionPrompt, chooseQuit, chooseMinimize, cancelCloseActionPrompt, performCloseAction, setupCloseActionPromptListener, cleanupCloseActionPromptListener } = useCloseActionPrompt({ requestClose: requestAppClose }); useVisibilityChange(); @@ -2372,6 +2374,37 @@ async function openPendingAiConfigLinks() { } } +const pluginCenterInstallRequest = ref<{ id: number; url: string } | null>(null); +let pluginInstallDeepLinkId = 0; + +async function openPluginInstallDeepLink(url: string) { + try { + const draft = parsePluginInstallDeepLink(url); + if (!draft) return; + pluginCenterInstallRequest.value = { id: ++pluginInstallDeepLinkId, url: draft.url }; + openPluginCenterPage(); + } catch (e: any) { + toast( + t("pluginPlatform.deepLinkInvalid", { + message: e?.message || String(e), + }), + 5000, + ); + } +} + +async function openPendingPluginInstallLinks() { + if (!isTauriRuntime()) return; + try { + const links = await api.pendingOpenPluginInstallLinks(); + for (const link of links) { + await openPluginInstallDeepLink(link); + } + } catch { + /* ignore startup deep-link probing errors */ + } +} + function setConnectionDialogOpen(value: boolean) { showConnectionDialog.value = value; if (!value) { @@ -3579,6 +3612,7 @@ onMounted(async () => { void openPendingDbFiles(); void openPendingConnectionLinks(); void openPendingAiConfigLinks(); + void openPendingPluginInstallLinks(); console.log(`[STARTUP] onMounted sync done: ${(performance.now() - mountStart).toFixed(0)}ms`); }); @@ -3707,7 +3741,7 @@ onUnmounted(() => { @detach-tab="detachTab" > - + (); const emit = defineEmits<{ @@ -534,6 +535,21 @@ watch( }, { deep: true }, ); +let lastHandledInstallRequestId = 0; +watch( + () => props.installUrlRequest, + async (request) => { + // immediate so a deep link that just opened the Plugin Center is consumed on + // mount; the id guard keeps already-handled requests from replaying. + if (!request || request.id <= lastHandledInstallRequestId) return; + lastHandledInstallRequestId = request.id; + installUrl.value = request.url; + if (window.confirm(t("pluginPlatform.deepLinkInstallConfirm", { url: request.url }))) { + await installPluginFromUrl(); + } + }, + { immediate: true }, +); watch(allowUnsigned, (value) => { try { localStorage.setItem(PLUGIN_ALLOW_UNSIGNED_STORAGE_KEY, value ? "1" : "0"); diff --git a/apps/desktop/src/composables/useTauriEvents.ts b/apps/desktop/src/composables/useTauriEvents.ts index 9f7c2b5359..55e5b83d61 100644 --- a/apps/desktop/src/composables/useTauriEvents.ts +++ b/apps/desktop/src/composables/useTauriEvents.ts @@ -9,6 +9,7 @@ export function useTauriEvents(deps: { openDbFilePath: (path: string) => Promise; openConnectionDeepLink: (url: string) => Promise; openAiConfigDeepLink: (url: string) => Promise; + openPluginInstallDeepLink: (url: string) => Promise; closeActiveSurface: () => void; }) { const connectionStore = useConnectionStore(); @@ -153,6 +154,17 @@ export function useTauriEvents(deps: { } }).then((unlisten) => unlistenHandles.push(unlisten)); + listen("dbx-open-plugin-install-links", async (event) => { + try { + for (const url of event.payload) { + await deps.openPluginInstallDeepLink(url); + } + focusCurrentWindow(); + } catch (e) { + console.error("[DBX] dbx-open-plugin-install-links error:", e); + } + }).then((unlisten) => unlistenHandles.push(unlisten)); + listen("dbx-close-active-tab", () => { deps.closeActiveSurface(); }).then((unlisten) => unlistenHandles.push(unlisten)); diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index ec7ee4db08..85c0897cea 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -230,6 +230,8 @@ export default { installPackage: "Install .dbxp", installFromUrl: "Install from URL", installUrlPlaceholder: "https://example.com/plugin.dbxp", + deepLinkInstallConfirm: "Install this plugin from the link below?", + deepLinkInvalid: "Invalid plugin install link: {message}", invalidPackageUrl: "Enter a valid http:// or https:// plugin package URL", downloadProgress: "Downloading {downloaded} of {total}", downloadProgressUnknown: "Downloading {downloaded}", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 42e3440103..27c48089a2 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -223,6 +223,8 @@ export default withEnglishFallback({ installPackage: "Instalar .dbxp", installFromUrl: "Instalar desde URL", installUrlPlaceholder: "https://example.com/plugin.dbxp", + deepLinkInstallConfirm: "¿Instalar este complemento desde el siguiente enlace?", + deepLinkInvalid: "Enlace de instalación de complemento no válido: {message}", invalidPackageUrl: "Introduce una URL de paquete de plugin http(s) válida", downloadProgress: "Descargando {downloaded} de {total}", downloadProgressUnknown: "Descargando {downloaded}", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index c9ee7584f6..aa8a5c3ed0 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -222,6 +222,8 @@ export default withEnglishFallback({ installPackage: "Installa .dbxp", installFromUrl: "Installa da URL", installUrlPlaceholder: "https://example.com/plugin.dbxp", + deepLinkInstallConfirm: "Installare questo plugin dal seguente link?", + deepLinkInvalid: "Link di installazione plugin non valido: {message}", invalidPackageUrl: "Inserisci un URL http(s) valido per il pacchetto del plugin", downloadProgress: "Download di {downloaded} su {total}", downloadProgressUnknown: "Download di {downloaded}", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 0ca2dc4db0..ee1f5130b7 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -223,6 +223,8 @@ export default withEnglishFallback({ installPackage: ".dbxp をインストール", installFromUrl: "URL からインストール", installUrlPlaceholder: "https://example.com/plugin.dbxp", + deepLinkInstallConfirm: "以下のリンクからプラグインをインストールしますか?", + deepLinkInvalid: "無効なプラグインインストールリンクです: {message}", invalidPackageUrl: "有効な http(s) プラグインパッケージ URL を入力してください", downloadProgress: "ダウンロード中 {downloaded} / {total}", downloadProgressUnknown: "ダウンロード中 {downloaded}", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 6ea97f7620..93eeb985ce 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -231,6 +231,8 @@ export default withEnglishFallback({ installPackage: ".dbxp 설치", installFromUrl: "URL에서 설치", installUrlPlaceholder: "https://example.com/plugin.dbxp", + deepLinkInstallConfirm: "아래 링크에서 플러그인을 설치하시겠습니까?", + deepLinkInvalid: "잘못된 플러그인 설치 링크입니다: {message}", invalidPackageUrl: "유효한 http(s) 플러그인 패키지 URL을 입력하세요", downloadProgress: "다운로드 중 {downloaded} / {total}", downloadProgressUnknown: "다운로드 중 {downloaded}", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 26518cee57..7127826014 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -223,6 +223,8 @@ export default withEnglishFallback({ installPackage: "Instalar .dbxp", installFromUrl: "Instalar de URL", installUrlPlaceholder: "https://example.com/plugin.dbxp", + deepLinkInstallConfirm: "Instalar este plugin pelo link abaixo?", + deepLinkInvalid: "Link de instalação de plugin inválido: {message}", invalidPackageUrl: "Insira uma URL http(s) válida do pacote do plugin", downloadProgress: "Baixando {downloaded} de {total}", downloadProgressUnknown: "Baixando {downloaded}", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index cb3fde243a..6f13c43f02 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -146,6 +146,8 @@ export default withEnglishFallback({ installPackage: "安装 .dbxp", installFromUrl: "从 URL 安装", installUrlPlaceholder: "https://example.com/plugin.dbxp", + deepLinkInstallConfirm: "是否从以下链接安装插件?", + deepLinkInvalid: "无效的插件安装链接:{message}", invalidPackageUrl: "请输入有效的 http(s) 插件包链接", downloadProgress: "正在下载 {downloaded} / {total}", downloadProgressUnknown: "正在下载 {downloaded}", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 583f3b6a43..2823cf6432 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -223,6 +223,8 @@ export default withEnglishFallback({ installPackage: "安裝 .dbxp", installFromUrl: "從 URL 安裝", installUrlPlaceholder: "https://example.com/plugin.dbxp", + deepLinkInstallConfirm: "要從以下連結安裝外掛程式嗎?", + deepLinkInvalid: "無效的外掛程式安裝連結:{message}", invalidPackageUrl: "請輸入有效的 http(s) 外掛套件連結", downloadProgress: "正在下載 {downloaded} / {total}", downloadProgressUnknown: "正在下載 {downloaded}", diff --git a/apps/desktop/src/lib/backend/api.ts b/apps/desktop/src/lib/backend/api.ts index 475e5eb35a..dad796f5eb 100644 --- a/apps/desktop/src/lib/backend/api.ts +++ b/apps/desktop/src/lib/backend/api.ts @@ -420,6 +420,7 @@ export const pendingOpenSqlFiles = forward("pendingOpenSqlFiles"); export const pendingOpenDbFiles = forward("pendingOpenDbFiles"); export const pendingOpenConnectionLinks = forward("pendingOpenConnectionLinks"); export const pendingOpenAiConfigLinks = forward("pendingOpenAiConfigLinks"); +export const pendingOpenPluginInstallLinks = forward("pendingOpenPluginInstallLinks"); export const readExternalSqlFile = forward("readExternalSqlFile"); export const readExternalSqlFileSnapshot = forward("readExternalSqlFileSnapshot"); export const inspectExternalSqlFile = forward("inspectExternalSqlFile"); diff --git a/apps/desktop/src/lib/backend/http.ts b/apps/desktop/src/lib/backend/http.ts index 44eaefd0fe..df1a316646 100644 --- a/apps/desktop/src/lib/backend/http.ts +++ b/apps/desktop/src/lib/backend/http.ts @@ -2491,6 +2491,10 @@ export async function pendingOpenAiConfigLinks(): Promise { return []; } +export async function pendingOpenPluginInstallLinks(): Promise { + return []; +} + export async function readExternalSqlFile(_path: string, _maxSizeBytes?: number): Promise { throw new Error("Opening external SQL file paths is only available in the desktop app"); } diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts index 4c5fee7b53..2a3552c4f7 100644 --- a/apps/desktop/src/lib/backend/tauri.ts +++ b/apps/desktop/src/lib/backend/tauri.ts @@ -1003,6 +1003,10 @@ export async function pendingOpenAiConfigLinks(): Promise { return invoke("pending_open_ai_config_links"); } +export async function pendingOpenPluginInstallLinks(): Promise { + return invoke("pending_open_plugin_install_links"); +} + export interface ExternalSqlFileSnapshot { content: string; version: ExternalSqlFileVersion; diff --git a/apps/desktop/src/lib/plugins/__tests__/pluginInstallDeepLink.spec.ts b/apps/desktop/src/lib/plugins/__tests__/pluginInstallDeepLink.spec.ts new file mode 100644 index 0000000000..10de4ca88a --- /dev/null +++ b/apps/desktop/src/lib/plugins/__tests__/pluginInstallDeepLink.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { parsePluginInstallDeepLink } from "../pluginInstallDeepLink"; + +const PACKAGE_URL = "https://dl.dbxio.com/plugins/io.dbx.ssh/0.4.73/io.dbx.ssh-0.4.73-darwin-arm64.dbxp"; + +describe("parsePluginInstallDeepLink", () => { + it("extracts the encoded package url", () => { + const draft = parsePluginInstallDeepLink(`dbx://plugins/install?url=${encodeURIComponent(PACKAGE_URL)}`); + expect(draft).toEqual({ url: PACKAGE_URL }); + }); + + it("returns null for non-plugin links and junk", () => { + expect(parsePluginInstallDeepLink("dbx://connection/new?type=mysql")).toBeNull(); + expect(parsePluginInstallDeepLink("dbx://plugins/installed?url=https://example.com/a.dbxp")).toBeNull(); + expect(parsePluginInstallDeepLink("dbx://plugins/installation?url=https://example.com/a.dbxp")).toBeNull(); + expect(parsePluginInstallDeepLink("")).toBeNull(); + expect(parsePluginInstallDeepLink("not a url")).toBeNull(); + }); + + it("throws for matching links without a usable url param", () => { + expect(() => parsePluginInstallDeepLink("dbx://plugins/install")).toThrow(/Missing url/); + expect(() => parsePluginInstallDeepLink("dbx://plugins/install?url=")).toThrow(/Missing url/); + expect(() => parsePluginInstallDeepLink(`dbx://plugins/install?url=${encodeURIComponent("ftp://example.com/a.dbxp")}`)).toThrow(/http/); + expect(() => parsePluginInstallDeepLink(`dbx://plugins/install?url=${encodeURIComponent(`https://example.com/${"a".repeat(2100)}`)}`)).toThrow(/too long/); + }); +}); diff --git a/apps/desktop/src/lib/plugins/pluginInstallDeepLink.ts b/apps/desktop/src/lib/plugins/pluginInstallDeepLink.ts new file mode 100644 index 0000000000..5fa1a15b00 --- /dev/null +++ b/apps/desktop/src/lib/plugins/pluginInstallDeepLink.ts @@ -0,0 +1,31 @@ +const PLUGIN_INSTALL_DEEP_LINK_HOST = "plugins"; +const PLUGIN_INSTALL_DEEP_LINK_PATH = "/install"; +const MAX_DEEP_LINK_LENGTH = 4096; +const MAX_PACKAGE_URL_LENGTH = 2048; + +export interface PluginInstallDeepLinkDraft { + url: string; +} + +/** + * Parses `dbx://plugins/install?url=` links. + * Returns null for non-matching links and throws for malformed ones, + * mirroring the AI config deep-link parser contract. + */ +export function parsePluginInstallDeepLink(value: string): PluginInstallDeepLinkDraft | null { + const trimmed = value.trim(); + if (!trimmed || trimmed.length > MAX_DEEP_LINK_LENGTH) return null; + if (!trimmed.startsWith("dbx://")) return null; + + const url = new URL(trimmed); + if (url.host !== PLUGIN_INSTALL_DEEP_LINK_HOST || url.pathname !== PLUGIN_INSTALL_DEEP_LINK_PATH) return null; + + const packageUrl = url.searchParams.get("url")?.trim() ?? ""; + if (!packageUrl) throw new Error("Missing url"); + if (packageUrl.length > MAX_PACKAGE_URL_LENGTH) throw new Error("Package url is too long"); + + const protocol = new URL(packageUrl).protocol; + if (protocol !== "http:" && protocol !== "https:") throw new Error("Package url must be http(s)"); + + return { url: packageUrl }; +} diff --git a/src-tauri/src/commands/deep_link.rs b/src-tauri/src/commands/deep_link.rs index c4f388d4e8..fd632039bd 100644 --- a/src-tauri/src/commands/deep_link.rs +++ b/src-tauri/src/commands/deep_link.rs @@ -3,6 +3,7 @@ use std::sync::Mutex; const CONNECTION_DEEP_LINK_PREFIX: &str = "dbx://connection/new"; const AI_CONFIG_DEEP_LINK_PREFIX: &str = "dbx://settings/ai/new"; +const PLUGIN_INSTALL_DEEP_LINK_PREFIX: &str = "dbx://plugins/install"; const APP_OPEN_DEEP_LINK_PREFIX: &str = "dbx://open"; #[tauri::command] @@ -15,10 +16,16 @@ pub fn pending_open_ai_config_links(state: tauri::State<'_, DeepLinkOpenState>) dedupe_links(state.drain_ai_config_links()) } +#[tauri::command] +pub fn pending_open_plugin_install_links(state: tauri::State<'_, DeepLinkOpenState>) -> Vec { + dedupe_links(state.drain_plugin_install_links()) +} + #[derive(Default)] pub struct DeepLinkOpenState { pending_connection_links: Mutex>, pending_ai_config_links: Mutex>, + pending_plugin_install_links: Mutex>, } impl DeepLinkOpenState { @@ -40,6 +47,15 @@ impl DeepLinkOpenState { } } + pub fn push_plugin_install_links(&self, links: Vec) { + if links.is_empty() { + return; + } + if let Ok(mut pending) = self.pending_plugin_install_links.lock() { + pending.extend(links); + } + } + fn drain_connection_links(&self) -> Vec { self.pending_connection_links.lock().map(|mut pending| pending.drain(..).collect()).unwrap_or_default() } @@ -47,6 +63,10 @@ impl DeepLinkOpenState { fn drain_ai_config_links(&self) -> Vec { self.pending_ai_config_links.lock().map(|mut pending| pending.drain(..).collect()).unwrap_or_default() } + + fn drain_plugin_install_links(&self) -> Vec { + self.pending_plugin_install_links.lock().map(|mut pending| pending.drain(..).collect()).unwrap_or_default() + } } pub fn connection_deep_links_from_args(args: I) -> Vec @@ -75,6 +95,19 @@ pub fn ai_config_deep_link_from_arg(arg: &str) -> Option { matches_deep_link_target(trimmed, AI_CONFIG_DEEP_LINK_PREFIX).then(|| trimmed.to_string()) } +pub fn plugin_install_deep_links_from_args(args: I) -> Vec +where + I: IntoIterator, + S: AsRef, +{ + args.into_iter().filter_map(|arg| plugin_install_deep_link_from_arg(arg.as_ref())).collect() +} + +pub fn plugin_install_deep_link_from_arg(arg: &str) -> Option { + let trimmed = arg.trim(); + matches_deep_link_target(trimmed, PLUGIN_INSTALL_DEEP_LINK_PREFIX).then(|| trimmed.to_string()) +} + pub fn is_app_open_deep_link(arg: &str) -> bool { matches_deep_link_target(arg.trim(), APP_OPEN_DEEP_LINK_PREFIX) } @@ -133,6 +166,22 @@ mod tests { assert_eq!(links, vec!["dbx://settings/ai/new?v=1&provider=openai-compatible".to_string()]); } + #[test] + fn filters_plugin_install_deep_links() { + let links = plugin_install_deep_links_from_args([ + "dbx://plugins/install?url=https%3A%2F%2Fdl.dbxio.com%2Fplugins%2Fio.dbx.ssh%2F0.4.73%2Fio.dbx.ssh-0.4.73-darwin-arm64.dbxp", + "--flag", + "dbx://plugins/installed?url=https://example.com/plugin.dbxp", + "dbx://plugins/installation?url=https://example.com/plugin.dbxp", + "dbx://connection/new?type=mysql", + ]); + + assert_eq!( + links, + vec!["dbx://plugins/install?url=https%3A%2F%2Fdl.dbxio.com%2Fplugins%2Fio.dbx.ssh%2F0.4.73%2Fio.dbx.ssh-0.4.73-darwin-arm64.dbxp".to_string()] + ); + } + #[test] fn recognizes_app_open_deep_links() { assert!(is_app_open_deep_link("dbx://open")); @@ -148,11 +197,17 @@ mod tests { let state = DeepLinkOpenState::default(); state.push_connection_links(vec!["dbx://connection/new?type=mysql".to_string()]); state.push_ai_config_links(vec!["dbx://settings/ai/new?provider=openai-compatible".to_string()]); + state.push_plugin_install_links(vec!["dbx://plugins/install?url=https://example.com/plugin.dbxp".to_string()]); assert_eq!(state.drain_connection_links(), vec!["dbx://connection/new?type=mysql"]); assert_eq!(state.drain_ai_config_links(), vec!["dbx://settings/ai/new?provider=openai-compatible"]); + assert_eq!( + state.drain_plugin_install_links(), + vec!["dbx://plugins/install?url=https://example.com/plugin.dbxp"] + ); assert!(state.drain_connection_links().is_empty()); assert!(state.drain_ai_config_links().is_empty()); + assert!(state.drain_plugin_install_links().is_empty()); } #[test] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9a31d16009..20b0d9837d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -700,6 +700,17 @@ fn open_ai_config_deep_links(app: &tauri::AppHandle, links: Vec) { show_main_window(app); } +fn open_plugin_install_deep_links(app: &tauri::AppHandle, links: Vec) { + if links.is_empty() { + return; + } + if let Some(state) = app.try_state::() { + state.push_plugin_install_links(links.clone()); + } + let _ = app.emit("dbx-open-plugin-install-links", links); + show_main_window(app); +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum LocaleFamily { Azerbaijani, @@ -1446,6 +1457,8 @@ pub fn run() { open_connection_deep_links(app, links); let ai_config_links = commands::deep_link::ai_config_deep_links_from_args(args.clone()); open_ai_config_deep_links(app, ai_config_links); + let plugin_install_links = commands::deep_link::plugin_install_deep_links_from_args(args.clone()); + open_plugin_install_deep_links(app, plugin_install_links); let paths = commands::external_sql::sql_file_paths_from_args(args.clone(), std::path::Path::new(&cwd)); if !paths.is_empty() { @@ -1660,6 +1673,8 @@ pub fn run() { open_connection_deep_links(app.handle(), startup_links); let startup_ai_config_links = commands::deep_link::ai_config_deep_links_from_args(&startup_args); open_ai_config_deep_links(app.handle(), startup_ai_config_links); + let startup_plugin_install_links = commands::deep_link::plugin_install_deep_links_from_args(&startup_args); + open_plugin_install_deep_links(app.handle(), startup_plugin_install_links); let app_handle = app.handle().clone(); commands::mcp_bridge::start(app_handle, state, data_dir); @@ -2036,6 +2051,7 @@ pub fn run() { commands::keychain::read_keychain_passwords, commands::deep_link::pending_open_connection_links, commands::deep_link::pending_open_ai_config_links, + commands::deep_link::pending_open_plugin_install_links, commands::table_import::preview_table_import_file, commands::table_import::import_table_file, commands::table_import::cancel_table_import, @@ -2654,6 +2670,13 @@ pub fn run() { .collect(); open_ai_config_deep_links(app_handle, ai_config_links); + let plugin_install_links: Vec = urls + .iter() + .map(|url| url.to_string()) + .filter_map(|url| commands::deep_link::plugin_install_deep_link_from_arg(&url)) + .collect(); + open_plugin_install_deep_links(app_handle, plugin_install_links); + let paths: Vec = urls .iter() .filter_map(|url| url.to_file_path().ok())