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 → - + 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 + }) +})