diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ada0d505..b28ebf320 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Fix color picker dragging flooding the undo stack — fill/stroke/effect color and opacity drags now collapse into a single undo entry per interaction via debounced batching in `PropertyListRoot` - Fix .fig import crash on alias variables without a GUID +- Fix external links in AI panel blocked by Tauri ACL — use opener plugin instead of shell ## 0.11.6 — 2026-04-08 diff --git a/src/components/chat/ProviderSettings.vue b/src/components/chat/ProviderSettings.vue index 34718bcc7..24dd5fee2 100644 --- a/src/components/chat/ProviderSettings.vue +++ b/src/components/chat/ProviderSettings.vue @@ -16,6 +16,7 @@ import ProviderSelectField from '@/components/chat/ProviderSelectField.vue' import { useInputUI } from '@/components/ui/input' import { usePopoverUI } from '@/components/ui/popover' import { useAIChat } from '@/composables/use-chat' +import { openExternalLink } from '@/composables/use-external-link' const cls = usePopoverUI({ content: 'isolate z-[51] w-64 p-3' }) @@ -167,13 +168,13 @@ function clearUnsplashKey() { :class="useInputUI({ size: 'sm' }).base" @change="save" /> - Get free Pexels API key → - + @@ -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 + }) +})