From 8ebe37f6ad2de723ea87b3568a1ba6698f85eca4 Mon Sep 17 00:00:00 2001 From: sprmn24 Date: Sat, 20 Jun 2026 00:50:51 +0300 Subject: [PATCH] feat(desktop): notify renderer when GPU acceleration is disabled due to remote display Remote displays (RDP/SSH/X11) silently disable GPU hardware acceleration with only a console.log, leaving the user unaware that software rendering is active. Expose the detected reason over IPC and surface a dismissible banner in the renderer. --- apps/desktop/electron/main.cjs | 2 + apps/desktop/electron/preload.cjs | 1 + apps/desktop/src/app/desktop-controller.tsx | 2 + .../src/components/remote-display-banner.tsx | 42 +++++++++++++++++++ apps/desktop/src/global.d.ts | 1 + apps/desktop/src/i18n/en.ts | 6 +++ apps/desktop/src/i18n/ja.ts | 6 +++ apps/desktop/src/i18n/types.ts | 5 +++ apps/desktop/src/i18n/zh-hant.ts | 5 +++ apps/desktop/src/i18n/zh.ts | 5 +++ 10 files changed, 75 insertions(+) create mode 100644 apps/desktop/src/components/remote-display-banner.tsx diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index db573a1e0d2..0a4f8eec8ad 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -150,6 +150,8 @@ if (REMOTE_DISPLAY_REASON) { ) } +ipcMain.handle('hermes:get-remote-display-reason', () => REMOTE_DISPLAY_REASON) + // Keep the renderer running at full speed while the window is in the background // or occluded. The chat transcript streams to screen through a // requestAnimationFrame-gated flush; Chromium pauses rAF (and clamps timers) diff --git a/apps/desktop/electron/preload.cjs b/apps/desktop/electron/preload.cjs index 413abd77b32..f033475c544 100644 --- a/apps/desktop/electron/preload.cjs +++ b/apps/desktop/electron/preload.cjs @@ -140,6 +140,7 @@ contextBridge.exposeInMainWorld('hermesDesktop', { return () => ipcRenderer.removeListener('hermes:bootstrap:event', listener) }, getVersion: () => ipcRenderer.invoke('hermes:version'), + getRemoteDisplayReason: () => ipcRenderer.invoke('hermes:get-remote-display-reason'), uninstall: { summary: () => ipcRenderer.invoke('hermes:uninstall:summary'), run: mode => ipcRenderer.invoke('hermes:uninstall:run', { mode }) diff --git a/apps/desktop/src/app/desktop-controller.tsx b/apps/desktop/src/app/desktop-controller.tsx index 5ca73061135..c8cb9facc13 100644 --- a/apps/desktop/src/app/desktop-controller.tsx +++ b/apps/desktop/src/app/desktop-controller.tsx @@ -8,6 +8,7 @@ import { DesktopInstallOverlay } from '@/components/desktop-install-overlay' import { DesktopOnboardingOverlay } from '@/components/desktop-onboarding-overlay' import { GatewayConnectingOverlay } from '@/components/gateway-connecting-overlay' import { Pane, PaneMain } from '@/components/pane-shell' +import { RemoteDisplayBanner } from '@/components/remote-display-banner' import { useMediaQuery } from '@/hooks/use-media-query' import { useSkinCommand } from '@/themes/use-skin-command' @@ -956,6 +957,7 @@ export function DesktopController() { const overlays = ( <> + {!isSecondaryWindow() && } {!isSecondaryWindow() && ( (null) + const [dismissed, setDismissed] = useState(false) + + useEffect(() => { + void window.hermesDesktop?.getRemoteDisplayReason?.().then(result => setReason(result)) + }, []) + + if (!reason || dismissed) { + return null + } + + return ( +
+ + + +

{t.remoteDisplayBanner.message(reason)}

+
+ +
+
+ ) +} diff --git a/apps/desktop/src/global.d.ts b/apps/desktop/src/global.d.ts index c615ad2d61a..26ab49fea51 100644 --- a/apps/desktop/src/global.d.ts +++ b/apps/desktop/src/global.d.ts @@ -102,6 +102,7 @@ declare global { cancelBootstrap: () => Promise<{ ok: boolean; cancelled: boolean }> onBootstrapEvent: (callback: (payload: DesktopBootstrapEvent) => void) => () => void getVersion: () => Promise + getRemoteDisplayReason?: () => Promise updates: { check: () => Promise apply: (opts?: DesktopUpdateApplyOptions) => Promise diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index afe1e0117a2..704ed5f8e56 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -146,6 +146,12 @@ export const en: Translations = { } }, + remoteDisplayBanner: { + message: reason => + `Software rendering active — remote display detected (${reason}). GPU acceleration is disabled to prevent flickering.`, + dismiss: 'Dismiss' + }, + titlebar: { hideSidebar: 'Hide sidebar', showSidebar: 'Show sidebar', diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index 03fd9b4354b..a3109b94ffa 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -147,6 +147,12 @@ export const ja = defineLocale({ } }, + remoteDisplayBanner: { + message: reason => + `ソフトウェアレンダリングが有効です — リモートディスプレイを検出しました(${reason})。ちらつきを防ぐため GPU アクセラレーションは無効化されています。`, + dismiss: '閉じる' + }, + titlebar: { hideSidebar: 'サイドバーを非表示', showSidebar: 'サイドバーを表示', diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts index da025767fff..7cb915b6ac3 100644 --- a/apps/desktop/src/i18n/types.ts +++ b/apps/desktop/src/i18n/types.ts @@ -159,6 +159,11 @@ export interface Translations { } } + remoteDisplayBanner: { + message: (reason: string) => string + dismiss: string + } + titlebar: { hideSidebar: string showSidebar: string diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index b60fe5d423d..23fc6027b42 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -142,6 +142,11 @@ export const zhHant = defineLocale({ } }, + remoteDisplayBanner: { + message: reason => `軟體繪圖已啟用 — 偵測到遠端顯示(${reason})。為防止畫面閃爍,已停用 GPU 加速。`, + dismiss: '關閉' + }, + titlebar: { hideSidebar: '隱藏側邊欄', showSidebar: '顯示側邊欄', diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index bc0b828b955..271ca9e4899 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -142,6 +142,11 @@ export const zh: Translations = { } }, + remoteDisplayBanner: { + message: reason => `软件渲染已启用 — 检测到远程显示(${reason})。为防止画面闪烁,已禁用 GPU 加速。`, + dismiss: '关闭' + }, + titlebar: { hideSidebar: '隐藏侧边栏', showSidebar: '显示侧边栏',