{"repo":"NousResearch/hermes-agent","public_id":"47500","display_label":"Desktop app auto-previews links causing unwanted side effects","summary":"These issues concern the desktop app automatically previewing or loading external links in chat, which can trigger unintended behaviors such as audio autoplay and custom protocol handler activation.","subsystem":"apps/desktop","counts":{"members":3,"open_issues":1,"open_issues_distinct":1,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":0},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":49515,"merged_at":"2026-06-20T09:23:06+00:00"},"resolution":{"state":"partially_resolved","by":"pr-49515"},"freshness":{"state":"verified","last_verified_at":"2026-06-20T09:23:06+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.4533333333333333},"fix_edges":[{"issue_number":49505,"pr_number":49515,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-20T09:23:06+00:00"}],"members":[{"kind":"issue","number":47500,"title":"[Bug]: Desktop app auto-previews external links in chat, triggering custom protocol handlers (e.g. bitbrowser://)","url":null,"state":"open"},{"kind":"issue","number":49505,"title":"[Bug]: Desktop app auto-plays audio from YouTube links","url":null,"state":"closed"},{"kind":"pr","number":49515,"title":"fix(desktop): mute hidden link-title window so historical links don't autoplay audio","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/electron/link-title-window.cjs","apps/desktop/electron/link-title-window.test.cjs","apps/desktop/electron/main.cjs","apps/desktop/package.json"],"fix_hunks":[{"file":"apps/desktop/electron/link-title-window.cjs","hunks":["@@ -0,0 +1,42 @@\n+'use strict'\n+\n+// Hidden BrowserWindow used by tier-2 link-title resolution: when curl can't\n+// read a page <title> (bot walls, JS-rendered pages), we briefly load the URL\n+// in an offscreen window and read its title. That window loads arbitrary\n+// user-linked pages — including YouTube/`watch` URLs that autoplay — so it must\n+// never be allowed to emit sound.\n+\n+function linkTitleWindowOptions(partitionSession) {\n+  return {\n+    show: false,\n+    width: 1280,\n+    height: 800,\n+    webPreferences: {\n+      backgroundThrottling: false,\n+      contextIsolation: true,\n+      javascript: true,\n+      nodeIntegration: false,\n+      sandbox: true,\n+      session: partitionSession,\n+      webSecurity: true\n+    }\n+  }\n+}\n+\n+// Create the offscreen title-fetch window and immediately mute it. Without the\n+// mute, autoplaying media on the loaded page (e.g. a YouTube link) leaks ~2s of\n+// audio every time a session containing such links is re-rendered. See #49505.\n+function createLinkTitleWindow(BrowserWindow, partitionSession) {\n+  const window = new BrowserWindow(linkTitleWindowOptions(partitionSession))\n+\n+  try {\n+    window.webContents.setAudioMuted(true)\n+  } catch {\n+    // webContents may be unavailable in degraded/headless environments; muting\n+    // is best-effort and the window is destroyed within a few seconds anyway.\n+  }\n+\n+  return window\n+}\n+\n+module.exports = { createLinkTitleWindow, linkTitleWindowOptions }"]},{"file":"apps/desktop/electron/link-title-window.test.cjs","hunks":["@@ -0,0 +1,56 @@\n+const assert = require('node:assert/strict')\n+const test = require('node:test')\n+\n+const { createLinkTitleWindow, linkTitleWindowOptions } = require('./link-title-window.cjs')\n+\n+function makeFakeBrowserWindow() {\n+  const calls = { audioMuted: [] }\n+  const FakeBrowserWindow = function (options) {\n+    this.options = options\n+    this.webContents = {\n+      setAudioMuted(value) {\n+        calls.audioMuted.push(value)\n+      }\n+    }\n+  }\n+\n+  return { FakeBrowserWindow, calls }\n+}\n+\n+test('linkTitleWindowOptions keeps the offscreen, hardened defaults', () => {\n+  const session = { id: 'link-titles' }\n+  const options = linkTitleWindowOptions(session)\n+\n+  assert.equal(options.show, false)\n+  assert.equal(options.webPreferences.session, session)\n+  assert.equal(options.webPreferences.contextIsolation, true)\n+  assert.equal(options.webPreferences.sandbox, true)\n+  assert.equal(options.webPreferences.nodeIntegration, false)\n+})\n+\n+test('createLinkTitleWindow mutes audio so historical links never autoplay sound', () => {\n+  // Regression for #49505: the hidden title-fetch window loaded YouTube/watch\n+  // URLs (to read their <title>) without muting, leaking ~2s of audio on every\n+  // history re-render.\n+  const { FakeBrowserWindow, calls } = makeFakeBrowserWindow()\n+\n+  const window = createLinkTitleWindow(FakeBrowserWindow, { id: 'link-titles' })\n+\n+  assert.ok(window instanceof FakeBrowserWindow)\n+  assert.deepEqual(calls.audioMuted, [true])\n+})\n+\n+test('createLinkTitleWindow still returns the window if muting throws', () => {\n+  const ThrowingBrowserWindow = function (options) {\n+    this.options = options\n+    this.webContents = {\n+      setAudioMuted() {\n+        throw new Error('webContents unavailable')\n+      }\n+    }\n+  }\n+\n+  const window = createLinkTitleWindow(ThrowingBrowserWindow, { id: 'link-titles' })\n+\n+  assert.ok(window instanceof ThrowingBrowserWindow)\n+})"]},{"file":"apps/desktop/electron/main.cjs","hunks":["@@ -34,6 +34,7 @@ const {\n   SESSION_WINDOW_MIN_WIDTH\n } = require('./session-windows.cjs')\n const { canImportHermesCli, verifyHermesCli } = require('./backend-probes.cjs')\n+const { createLinkTitleWindow } = require('./link-title-window.cjs')\n const { probeGatewayWebSocket } = require('./gateway-ws-probe.cjs')\n const { adoptServedDashboardToken } = require('./dashboard-token.cjs')\n const { waitForDashboardPort } = require('./backend-ready.cjs')","@@ -2980,20 +2981,7 @@ function runRenderTitleJob(rawUrl) {\n     }\n \n     try {\n-      window = new BrowserWindow({\n-        show: false,\n-        width: 1280,\n-        height: 800,\n-        webPreferences: {\n-          backgroundThrottling: false,\n-          contextIsolation: true,\n-          javascript: true,\n-          nodeIntegration: false,\n-          sandbox: true,\n-          session: partitionSession,\n-          webSecurity: true\n-        }\n-      })\n+      window = createLinkTitleWindow(BrowserWindow, partitionSession)\n     } catch {\n       return finish('')\n     }"]},{"file":"apps/desktop/package.json","hunks":["@@ -37,7 +37,7 @@\n     \"test:desktop:nsis\": \"node scripts/test-desktop.mjs nsis\",\n     \"test:desktop:existing\": \"node scripts/test-desktop.mjs existing\",\n     \"test:desktop:fresh\": \"node scripts/test-desktop.mjs fresh\",\n-    \"test:desktop:platforms\": \"node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-env.test.cjs electron/backend-probes.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs electron/dashboard-token.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs electron/workspace-cwd.test.cjs electron/fs-read-dir.test.cjs electron/git-root.test.cjs electron/windows-child-process.test.cjs electron/update-remote.test.cjs electron/update-rebuild.test.cjs electron/windows-user-env.test.cjs\",\n+    \"test:desktop:platforms\": \"node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-env.test.cjs electron/backend-probes.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs electron/dashboard-token.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs electron/link-title-window.test.cjs electron/workspace-cwd.test.cjs electron/fs-read-dir.test.cjs electron/git-root.test.cjs electron/windows-child-process.test.cjs electron/update-remote.test.cjs electron/update-rebuild.test.cjs electron/windows-user-env.test.cjs\",\n     \"typecheck\": \"tsc -p . --noEmit\",\n     \"lint\": \"eslint src/ electron/\",\n     \"lint:fix\": \"eslint src/ electron/ --fix\",\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}