Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions packages/app/e2e/external-links.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { test, expect } from "./fixtures"

test("external links are intercepted and don't navigate the app", async ({ page, context, slug, gotoSession }) => {
await gotoSession()

await page.evaluate(() => {
const link = document.createElement("a")
link.href = "https://opencode.ai"
link.id = "test-external-link"
link.textContent = "External"
link.style.display = "block"
document.body.appendChild(link)
})

const currentUrl = page.url()
const popupPromise = context.waitForEvent("page")
await page.click("#test-external-link")

const popup = await Promise.race([
popupPromise,
new Promise<never>((_, reject) => setTimeout(() => reject(new Error("Popup did not open")), 2000)),
]).catch(() => null)

await expect(page).toHaveURL(new RegExp(`/${slug}/session`))
expect(page.url()).toBe(currentUrl)

if (popup) {
expect(popup.url()).toContain("opencode.ai")
}
})

test("internal links navigate within the app", async ({ page, slug, gotoSession }) => {
await gotoSession()

await page.evaluate((slug) => {
const link = document.createElement("a")
link.href = `/${slug}`
link.id = "test-internal-link"
link.textContent = "Internal"
link.style.display = "block"
document.body.appendChild(link)
}, slug)

await page.click("#test-internal-link")

await expect(page).toHaveURL(new RegExp(`/${slug}($|/)`))
})

test("localhost links are treated as internal", async ({ page, gotoSession }) => {
await gotoSession()
const initialUrl = page.url()

await page.evaluate(() => {
const link = document.createElement("a")
link.href = `http://localhost:${window.location.port}/`
link.id = "test-localhost-link"
link.textContent = "Localhost"
link.style.display = "block"
document.body.appendChild(link)
})

await page.click("#test-localhost-link")
await page.waitForTimeout(100)

const newUrl = page.url()
expect(new URL(initialUrl).hostname).toBe(new URL(newUrl).hostname)
})
18 changes: 18 additions & 0 deletions packages/app/src/entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,24 @@ const platform: Platform = {
},
}

document.addEventListener(
"click",
(e) => {
const anchor = (e.target as HTMLElement).closest("a")
if (!anchor) return

const href = anchor.getAttribute("href")
if (!href) return

if (href.startsWith("http://") || href.startsWith("https://")) {
e.preventDefault()
e.stopPropagation()
platform.openLink(href)
}
},
true
)

render(
() => (
<PlatformProvider value={platform}>
Expand Down
30 changes: 22 additions & 8 deletions packages/desktop/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,18 +328,32 @@ render(() => {
const [serverPassword, setServerPassword] = createSignal<string | null>(null)
const platform = createPlatform(() => serverPassword())

function handleClick(e: MouseEvent) {
const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
if (link?.href) {
e.preventDefault()
platform.openLink(link.href)
onMount(() => {
// Handle external links - open in system browser instead of webview
const handleClick = (e: MouseEvent) => {
const target = e.target as HTMLElement
const link = target.closest("a") as HTMLAnchorElement | null

if (!link?.href) return

try {
const linkUrl = new URL(link.href, window.location.href)
const isExternal = linkUrl.origin !== window.location.origin

if (isExternal) {
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
void shellOpen(link.href).catch(() => undefined)
}
} catch {
// Invalid URL, let it through
}
}
}

onMount(() => {
document.addEventListener("click", handleClick)
onCleanup(() => {
document.removeEventListener("click", handleClick)
document.removeEventListener("click", handleClick, true)
})
})

Expand Down