From 7b5a5bdeef7e72a01550c5a86df58334b0840789 Mon Sep 17 00:00:00 2001
From: Anton Soldatov
Date: Wed, 15 Apr 2026 11:02:00 +0300
Subject: [PATCH] fix(tauri): use opener plugin for external links instead of
shell (#197)
External links () in the AI panel triggered
"Command plugin:shell|open not allowed by ACL" on Tauri desktop.
Root cause: Tauri v2 intercepts via the shell plugin,
but only the opener plugin had ACL permissions. Replace all external links
with programmatic openUrl() from @tauri-apps/plugin-opener, which is already
whitelisted via "opener:default" in capabilities.
- Extract openExternalLink() composable for Tauri/browser switch
- Replace with
@@ -201,13 +202,13 @@ function clearUnsplashKey() {
:class="useInputUI({ size: 'sm' }).base"
@change="save"
/>
-
Get free Unsplash access key →
-
+
@@ -293,14 +294,14 @@ function clearUnsplashKey() {
:class="useInputUI({ size: 'sm' }).base"
@change="save"
/>
-
Get API key →
-
+
diff --git a/src/components/chat/ProviderSetup.vue b/src/components/chat/ProviderSetup.vue
index dacb6edf3..937413e28 100644
--- a/src/components/chat/ProviderSetup.vue
+++ b/src/components/chat/ProviderSetup.vue
@@ -5,6 +5,7 @@ import ProviderSelectField from '@/components/chat/ProviderSelectField.vue'
import { useInputUI } from '@/components/ui/input'
import { useAIChat } from '@/composables/use-chat'
import { ACP_AGENTS } from '@open-pencil/core'
+import { openExternalLink } from '@/composables/use-external-link'
import { useI18n } from '@open-pencil/vue'
const { providerID, providerDef, setAPIKey, customBaseURL, customModelID } = useAIChat()
@@ -104,15 +105,15 @@ function save() {
-
{{ dialogs.getAPIKey({ provider: providerDef.name }) }}
-
+
Window | null
}
diff --git a/tests/e2e/chat-panel.spec.ts b/tests/e2e/chat-panel.spec.ts
index 7d77a6b92..d9c7ed56c 100644
--- a/tests/e2e/chat-panel.spec.ts
+++ b/tests/e2e/chat-panel.spec.ts
@@ -217,3 +217,37 @@ test('transport errors show an actionable toast', async () => {
}),
).toBeVisible({ timeout: 5000 })
})
+
+test('"Get API key" link opens external URL via window.open', async () => {
+ // Clear the key to return to provider setup
+ await page.locator('[data-test-id="provider-settings-trigger"]').click()
+ await page.locator('[data-test-id="provider-settings-clear-key"]').click()
+ await page.locator('[data-test-id="provider-settings-done"]').click()
+
+ // Now we're back in ProviderSetup — the link should be visible
+ const link = page.locator('[data-test-id="api-key-get-link"]')
+ await expect(link).toBeVisible()
+
+ // Intercept window.open to verify it's called with the right URL
+ const openedUrls: string[] = []
+ await page.exposeFunction('mockWindowOpen', (url: string) => openedUrls.push(url))
+ await page.evaluate(() => {
+ window.__mocked_window_open = window.open
+ window.open = (url: string | URL) => {
+ ;(window as any).mockWindowOpen(String(url))
+ return null
+ }
+ })
+
+ await link.click()
+
+ await expect(() => {
+ expect(openedUrls.length).toBeGreaterThan(0)
+ expect(openedUrls[0]).toMatch(/^https:\/\//)
+ }).toPass({ timeout: 3000 })
+
+ // Restore
+ await page.evaluate(() => {
+ window.open = window.__mocked_window_open
+ })
+})