diff --git a/apps/desktop/electron/link-title-window.cjs b/apps/desktop/electron/link-title-window.cjs
index 80b3af3976e..3aeabcfe611 100644
--- a/apps/desktop/electron/link-title-window.cjs
+++ b/apps/desktop/electron/link-title-window.cjs
@@ -3,8 +3,7 @@
// Hidden BrowserWindow used by tier-2 link-title resolution: when curl can't
// read a page
(bot walls, JS-rendered pages), we briefly load the URL
// in an offscreen window and read its title. That window loads arbitrary
-// user-linked pages — including YouTube/`watch` URLs that autoplay — so it must
-// never be allowed to emit sound.
+// user-linked pages, so it must never emit sound or trigger real downloads.
function linkTitleWindowOptions(partitionSession) {
return {
@@ -39,4 +38,15 @@ function createLinkTitleWindow(BrowserWindow, partitionSession) {
return window
}
-module.exports = { createLinkTitleWindow, linkTitleWindowOptions }
+// Cancel any download the title-fetch window triggers. Without this, a link
+// artifact URL served with Content-Disposition: attachment auto-downloads every
+// time the Artifacts page renders and fetchLinkTitle loads it.
+function guardLinkTitleSession(partitionSession) {
+ try {
+ partitionSession.on('will-download', (_event, item) => item.cancel())
+ } catch {
+ // best-effort; worst case is a spurious download
+ }
+}
+
+module.exports = { createLinkTitleWindow, guardLinkTitleSession, linkTitleWindowOptions }
diff --git a/apps/desktop/electron/link-title-window.test.cjs b/apps/desktop/electron/link-title-window.test.cjs
index 87333efb69d..64228ce4a8c 100644
--- a/apps/desktop/electron/link-title-window.test.cjs
+++ b/apps/desktop/electron/link-title-window.test.cjs
@@ -1,7 +1,7 @@
const assert = require('node:assert/strict')
const test = require('node:test')
-const { createLinkTitleWindow, linkTitleWindowOptions } = require('./link-title-window.cjs')
+const { createLinkTitleWindow, guardLinkTitleSession, linkTitleWindowOptions } = require('./link-title-window.cjs')
function makeFakeBrowserWindow() {
const calls = { audioMuted: [] }
@@ -54,3 +54,15 @@ test('createLinkTitleWindow still returns the window if muting throws', () => {
assert.ok(window instanceof ThrowingBrowserWindow)
})
+
+test('guardLinkTitleSession cancels downloads triggered by the title-fetch window', () => {
+ let cancelled = false
+ const handlers = {}
+ guardLinkTitleSession({ on: (e, h) => { handlers[e] = h } })
+ handlers['will-download'](null, { cancel: () => { cancelled = true } })
+ assert.ok(cancelled)
+})
+
+test('guardLinkTitleSession is a no-op when session.on throws', () => {
+ assert.doesNotThrow(() => guardLinkTitleSession({ on() { throw new Error() } }))
+})
diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs
index 64a8701c4a2..29b2891d7d5 100644
--- a/apps/desktop/electron/main.cjs
+++ b/apps/desktop/electron/main.cjs
@@ -36,7 +36,7 @@ const {
SESSION_WINDOW_MIN_WIDTH
} = require('./session-windows.cjs')
const { canImportHermesCli, verifyHermesCli } = require('./backend-probes.cjs')
-const { createLinkTitleWindow } = require('./link-title-window.cjs')
+const { createLinkTitleWindow, guardLinkTitleSession } = require('./link-title-window.cjs')
const { probeGatewayWebSocket } = require('./gateway-ws-probe.cjs')
const { adoptServedDashboardToken } = require('./dashboard-token.cjs')
const { waitForDashboardPortAnnouncement } = require('./backend-ready.cjs')
@@ -3509,6 +3509,7 @@ function getLinkTitleSession() {
linkTitleSession.webRequest.onBeforeRequest((details, callback) => {
callback({ cancel: RENDER_TITLE_BLOCKED_RESOURCES.has(details.resourceType) })
})
+ guardLinkTitleSession(linkTitleSession)
return linkTitleSession
}