feat(desktop): window translucency slider in Appearance settings (#45086)

A see-through-window control (0–100, off by default) that maps to the
native window opacity via setOpacity — the desktop shows through the whole
window, the same effect as the Windows shift-scroll trick. macOS + Windows;
a no-op on Linux (no runtime window opacity).

Renderer owns the value (persisted, nanostore) and mirrors it to the main
process over IPC; main persists it to translucency.json so a cold launch
applies it at window creation before the renderer reports in.
This commit is contained in:
brooklyn!
2026-06-12 12:02:38 -05:00
committed by GitHub
parent 7d4e60e44a
commit 46d758bb3e
11 changed files with 151 additions and 0 deletions
@@ -9,6 +9,7 @@ import { Check, Download, Loader2, Palette, Trash2 } from '@/lib/icons'
import { cn } from '@/lib/utils'
import { $activeGatewayProfile, $profiles, normalizeProfileKey } from '@/store/profile'
import { $toolViewMode, setToolViewMode } from '@/store/tool-view'
import { $translucency, setTranslucency } from '@/store/translucency'
import { useTheme } from '@/themes/context'
import { installVscodeThemeFromMarketplace } from '@/themes/install'
import { isUserTheme, removeUserTheme, resolveTheme } from '@/themes/user-themes'
@@ -135,6 +136,7 @@ export function AppearanceSettings() {
const { t, isSavingLocale } = useI18n()
const { themeName, mode, availableThemes, setTheme, setMode } = useTheme()
const toolViewMode = useStore($toolViewMode)
const translucency = useStore($translucency)
const profiles = useStore($profiles)
const activeProfileKey = normalizeProfileKey(useStore($activeGatewayProfile))
const a = t.settings.appearance
@@ -183,6 +185,32 @@ export function AppearanceSettings() {
title={a.colorMode}
/>
<ListRow
action={
<div className="flex items-center gap-3">
<input
aria-label={a.translucencyTitle}
className="h-1 w-40 cursor-pointer appearance-none rounded-full bg-(--ui-stroke-tertiary)"
max={100}
min={0}
onChange={event => {
triggerHaptic('selection')
setTranslucency(Number(event.target.value))
}}
step={5}
style={{ accentColor: 'var(--dt-primary)' }}
type="range"
value={translucency}
/>
<span className="w-9 text-right text-[length:var(--conversation-caption-font-size)] tabular-nums text-(--ui-text-tertiary)">
{translucency}%
</span>
</div>
}
description={a.translucencyDesc}
title={a.translucencyTitle}
/>
<ListRow
below={
<>
+1
View File
@@ -55,6 +55,7 @@ declare global {
stopPreviewFileWatch: (id: string) => Promise<boolean>
setTitleBarTheme?: (payload: HermesTitleBarTheme) => void
setNativeTheme?: (mode: 'dark' | 'light' | 'system') => void
setTranslucency?: (payload: { intensity: number }) => void
setPreviewShortcutActive?: (active: boolean) => void
openExternal: (url: string) => Promise<void>
fetchLinkTitle: (url: string) => Promise<string>
+2
View File
@@ -296,6 +296,8 @@ export const en: Translations = {
colorModeDesc: 'Pick a fixed mode or let Hermes follow your system setting.',
toolViewTitle: 'Tool Call Display',
toolViewDesc: 'Product hides raw tool payloads; Technical shows full input/output.',
translucencyTitle: 'Window Translucency',
translucencyDesc: 'See your desktop through the whole window. macOS and Windows only.',
product: 'Product',
productDesc: 'Human-friendly tool activity with concise summaries.',
technical: 'Technical',
+2
View File
@@ -210,6 +210,8 @@ export const ja = defineLocale({
colorModeDesc: '固定モードを選ぶか、Hermes をシステム設定に合わせます。',
toolViewTitle: 'ツール呼び出しの表示',
toolViewDesc: 'プロダクト表示は生のツールペイロードを隠し、テクニカル表示は入出力をすべて表示します。',
translucencyTitle: 'ウィンドウの透過',
translucencyDesc: 'ウィンドウ全体を透過させてデスクトップを表示します。macOS と Windows のみ。',
product: 'プロダクト',
productDesc: '読みやすいツール活動と簡潔な要約を表示します。',
technical: 'テクニカル',
+2
View File
@@ -213,6 +213,8 @@ export interface Translations {
colorModeDesc: string
toolViewTitle: string
toolViewDesc: string
translucencyTitle: string
translucencyDesc: string
product: string
productDesc: string
technical: string
+2
View File
@@ -204,6 +204,8 @@ export const zhHant = defineLocale({
colorModeDesc: '選擇固定模式,或讓 Hermes 跟隨系統設定。',
toolViewTitle: '工具呼叫顯示',
toolViewDesc: '產品模式會隱藏原始工具 payload;技術模式會顯示完整輸入/輸出。',
translucencyTitle: '視窗透明',
translucencyDesc: '讓整個視窗透出桌面。僅支援 macOS 與 Windows。',
product: '產品',
productDesc: '易讀的工具活動與精簡摘要。',
technical: '技術',
+2
View File
@@ -291,6 +291,8 @@ export const zh: Translations = {
colorModeDesc: '选择固定模式,或让 Hermes 跟随系统设置。',
toolViewTitle: '工具调用显示',
toolViewDesc: '产品模式隐藏原始工具数据;技术模式显示完整输入/输出。',
translucencyTitle: '窗口透明',
translucencyDesc: '让整个窗口透出桌面。仅支持 macOS 和 Windows。',
product: '产品',
productDesc: '易读的工具活动与简洁摘要。',
technical: '技术',
+2
View File
@@ -1,4 +1,6 @@
import './styles.css'
// Side-effect: applies the persisted window translucency on load.
import './store/translucency'
import { QueryClientProvider } from '@tanstack/react-query'
import { StrictMode } from 'react'
+38
View File
@@ -0,0 +1,38 @@
/**
* Window translucency (see-through window).
*
* One lever, 0100. 0 = off (fully opaque, the default). Higher = more of the
* desktop shows through the whole window — the main process maps it to the
* native window opacity (`setOpacity`), the same effect as the Windows
* shift-scroll trick. macOS + Windows only; Linux has no runtime window
* opacity, so it's a no-op there.
*
* The renderer owns the value and mirrors it to the main process over IPC.
*/
import { atom } from 'nanostores'
import { persistString, storedString } from '@/lib/storage'
const KEY = 'hermes.desktop.translucency.v1'
const clamp = (n: number): number => Math.min(100, Math.max(0, Math.round(n)))
const read = (): number => {
const n = Number(storedString(KEY))
return Number.isFinite(n) ? clamp(n) : 0
}
export const $translucency = atom<number>(typeof window === 'undefined' ? 0 : read())
export function setTranslucency(intensity: number): void {
$translucency.set(clamp(intensity))
}
if (typeof window !== 'undefined') {
$translucency.subscribe(intensity => {
persistString(KEY, String(intensity))
window.hermesDesktop?.setTranslucency?.({ intensity })
})
}