{"record":{"id":"beb8b217b6b604bf","repo":"NousResearch/hermes-agent","slug":"desktop-bridge-unavailable","errorCode":null,"errorMessage":"Desktop bridge unavailable","messagePattern":"Desktop bridge unavailable","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/desktop/src/app/chat/right-rail/preview-artifact.tsx","lineNumber":53,"sourceCode":"  }\n\n  return [\n    '<!doctype html>',\n    '<html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">',\n    '<style>body{margin:0;font-family:system-ui,sans-serif}</style></head><body>',\n    content,\n    '</body></html>'\n  ].join('\\n')\n}\n\n/** Write the composed document to a real temp file through the existing\n *  buffer-save IPC, then hand it to the OS browser. A blob/data URL can't\n *  cross into the OS default browser, so a file on disk is the honest path. */\nasync function openHtmlInBrowser(content: string): Promise<void> {\n  const bridge = window.hermesDesktop\n\n  if (!bridge?.saveImageBuffer || !bridge.openExternal) {\n    throw new Error('Desktop bridge unavailable')\n  }\n\n  const bytes = new TextEncoder().encode(composeArtifactHtml(content))\n  const path = await bridge.saveImageBuffer(bytes, '.html')\n\n  if (!path) {\n    throw new Error('Could not write artifact file')\n  }\n\n  const fileUrl = `file://${path.startsWith('/') ? '' : '/'}${path.replace(/\\\\/g, '/')}`\n\n  if (bridge.openPreviewInBrowser) {\n    await bridge.openPreviewInBrowser(fileUrl)\n\n    return\n  }\n\n  await bridge.openExternal(fileUrl)","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/src/app/chat/right-rail/preview-artifact.tsx#L35-L71","documentation":"Thrown by openHtmlInBrowser in the desktop right-rail artifact preview. It composes an HTML document and needs the Electron preload bridge (window.hermesDesktop) to write a temp file and hand a file:// URL to the OS browser — a blob/data URL cannot cross into the default browser. The error means the bridge object or one of its required methods (saveImageBuffer, openExternal) is missing, i.e. the code is running outside the expected Electron preload context or against an older preload that lacks those methods.","triggerScenarios":"Rendering the artifact 'open in browser' action in a non-Electron host (plain browser, web dashboard, tests/jsdom) where window.hermesDesktop is undefined; an Electron runtime whose preload script predates the saveImageBuffer bridge method; contextIsolation/sandbox settings preventing the bridge from being exposed on window.","commonSituations":"Reusing desktop components in the web dashboard where the preload bridge does not exist; upgrading the renderer without upgrading the Electron main/preload bundle; running component tests without mocking window.hermesDesktop.","solutions":["Ensure the renderer runs inside the Electron app with the current preload script that installs window.hermesDesktop (saveImageBuffer + openExternal).","If running in a plain browser context, gate the 'open in browser' button behind a bridge availability check instead of calling openHtmlInBrowser.","Rebuild/reinstall the desktop app so preload and renderer versions match.","In tests, stub window.hermesDesktop with the required methods."],"exampleFix":"// before\nconst bridge = window.hermesDesktop\nif (!bridge?.saveImageBuffer || !bridge.openExternal) {\n  throw new Error('Desktop bridge unavailable')\n}\n\n// after — feature-detect at the UI layer and disable the action\nconst bridgeAvailable = Boolean(\n  window.hermesDesktop?.saveImageBuffer && window.hermesDesktop?.openExternal\n)\n// render the button only when bridgeAvailable; keep the throw as an invariant guard","handlingStrategy":"type-guard","validationCode":"const bridge = window.hermesDesktop\nconst canOpenInBrowser = Boolean(bridge?.saveImageBuffer && bridge?.openExternal)\nif (!canOpenInBrowser) {\n  // hide/disable the 'open in browser' action instead of throwing at click time\n}","typeGuard":"interface DesktopBridge {\n  saveImageBuffer(bytes: Uint8Array, ext: string): Promise<string>\n  openExternal(url: string): Promise<void>\n  openPreviewInBrowser?(url: string): Promise<void>\n}\nconst isDesktopBridge = (b: unknown): b is DesktopBridge =>\n  typeof b === 'object' && b !== null &&\n  typeof (b as DesktopBridge).saveImageBuffer === 'function' &&\n  typeof (b as DesktopBridge).openExternal === 'function'","tryCatchPattern":"try {\n  await openHtmlInBrowser(content)\n} catch (error) {\n  if (error instanceof Error && error.message === 'Desktop bridge unavailable') {\n    // degrade gracefully: fall back to in-pane HTML rendering\n    renderInline(content)\n  } else {\n    throw error\n  }\n}","preventionTips":["Feature-detect window.hermesDesktop before rendering OS-browser actions","Keep preload and renderer bundles versioned/deployed together","Mock the bridge in tests so components assume its presence only inside Electron"],"tags":["desktop","electron","ipc-bridge","preview"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}