refactor(desktop): drop System settings section; keep-awake → Advanced

Revert the dedicated System section: Window Translucency + UI Scale move
back to Appearance, and Haptics returns to its titlebar-only home. Keep
computer awake now lives as a device-local toggle at the top of Advanced
(a ConfigSettings section-specific extra, like the Model block), keeping
the statusbar quick-toggle. Relocated i18n back to settings.appearance /
settings.config across all four locales.
This commit is contained in:
Brooklyn Nicholson 2026-07-20 13:40:16 -05:00
parent 9b513a3b8d
commit ac9a1014a6
10 changed files with 112 additions and 178 deletions

View file

@ -16,6 +16,8 @@ import { $backdrop, setBackdrop } from '@/store/backdrop'
import { $embedAllowed, $embedMode, clearEmbedAllowed, type EmbedMode, setEmbedMode } from '@/store/embed-consent'
import { $activeGatewayProfile, $profiles, normalizeProfileKey } from '@/store/profile'
import { $toolViewMode, setToolViewMode } from '@/store/tool-view'
import { $translucency, setTranslucency } from '@/store/translucency'
import { $zoomPercent, setZoomPercent } from '@/store/zoom'
import { getBaseColors, useTheme } from '@/themes/context'
import { installVscodeThemeFromMarketplace } from '@/themes/install'
import type { DesktopTheme } from '@/themes/types'
@ -62,6 +64,18 @@ function ThemePreview({ name, mode }: { name: string; mode: 'light' | 'dark' })
)
}
// UI scale presets, as zoom percentages. 100 is the browser-default size;
// the ids double as the percent values sent to the main process. A Cmd/Ctrl
// +/- step landing between presets highlights nothing, and the row
// description keeps showing the exact current percent.
const UI_SCALE_PRESETS = ['90', '100', '110', '125', '150', '175'] as const
type UiScalePreset = (typeof UI_SCALE_PRESETS)[number]
function matchUiScalePreset(percent: number): UiScalePreset | null {
return UI_SCALE_PRESETS.find(preset => Number(preset) === percent) ?? null
}
function useDebounced<T>(value: T, delayMs: number): T {
const [debounced, setDebounced] = useState(value)
@ -231,8 +245,10 @@ export function AppearanceSettings() {
const { t, isSavingLocale } = useI18n()
const { themeName, mode, resolvedMode, availableThemes, setTheme, setMode } = useTheme()
const toolViewMode = useStore($toolViewMode)
const zoomPercent = useStore($zoomPercent)
const embedMode = useStore($embedMode)
const embedAllowed = useStore($embedAllowed)
const translucency = useStore($translucency)
const backdrop = useStore($backdrop)
const installs = useStore($marketplaceInstalls)
const profiles = useStore($profiles)
@ -277,6 +293,10 @@ export function AppearanceSettings() {
{ id: 'off', label: a.embedsOff }
] as const satisfies readonly { id: EmbedMode; label: string }[]
const uiScaleOptions = UI_SCALE_PRESETS.map(preset => ({ id: preset, label: `${preset}%` }))
const matchedScalePreset = matchUiScalePreset(zoomPercent)
return (
<SettingsContent>
<div>
@ -392,6 +412,47 @@ export function AppearanceSettings() {
wide
/>
<ListRow
action={
<SegmentedControl
onChange={id => {
triggerHaptic('selection')
setZoomPercent(Number(id))
}}
options={uiScaleOptions}
value={matchedScalePreset ?? ('' as UiScalePreset)}
/>
}
description={a.uiScaleDesc(zoomPercent)}
title={a.uiScaleTitle}
/>
<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
action={
<SegmentedControl

View file

@ -1,3 +1,4 @@
import { useStore } from '@nanostores/react'
import { useQuery } from '@tanstack/react-query'
import type { ChangeEvent, ReactNode } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
@ -11,6 +12,7 @@ import { Textarea } from '@/components/ui/textarea'
import { getElevenLabsVoices, getHermesConfigSchema, saveHermesConfig } from '@/hermes'
import { useI18n } from '@/i18n'
import { cn } from '@/lib/utils'
import { $keepAwake, setKeepAwake } from '@/store/keep-awake'
import { notify, notifyError } from '@/store/notifications'
import type { ConfigFieldSchema, HermesConfigRecord } from '@/types/hermes'
@ -32,7 +34,7 @@ import {
import { MemoryConnect } from './memory/connect'
import { ProviderConfigPanel } from './memory/provider-config-panel'
import { ModelSettings, ModelSettingsSkeleton } from './model-settings'
import { EmptyState, ListRow, LoadingState, SettingsContent } from './primitives'
import { EmptyState, ListRow, LoadingState, SettingsContent, ToggleRow } from './primitives'
// On the Voice page, only surface the sub-fields of the *selected* TTS/STT
// provider — otherwise every provider's options render at once (the "totally
@ -242,6 +244,7 @@ export function ConfigSettings({
}) {
const { t } = useI18n()
const c = t.settings.config
const keepAwake = useStore($keepAwake)
// The editable draft is local (debounced autosave watches it), but it's seeded
// from — and saved back through — the shared config cache, so edits are visible
// in the MCP/model surfaces and reopening the page doesn't reload-flash.
@ -458,6 +461,11 @@ export function ConfigSettings({
<ModelSettings onMainModelChanged={onMainModelChanged} />
</div>
)}
{/* Device-local desktop pref (not config.yaml) lives here since keeping
the machine awake is a power-user knob. */}
{activeSectionId === 'advanced' && (
<ToggleRow checked={keepAwake} description={c.keepAwakeDesc} label={c.keepAwakeTitle} onChange={setKeepAwake} />
)}
{visibleFields.length === 0 ? (
<EmptyState description={c.emptyDesc} title={c.emptyTitle} />
) : (

View file

@ -10,7 +10,6 @@ import {
Archive,
BarChart3,
Bell,
Cpu,
Download,
Globe,
Info,
@ -43,7 +42,6 @@ import { NotificationsSettings } from './notifications-settings'
import { PluginsSettings } from './plugins-settings'
import { PROVIDER_VIEWS, ProvidersSettings, type ProviderView } from './providers-settings'
import { SessionsSettings } from './sessions-settings'
import { SystemSettings } from './system-settings'
import type { SettingsPageProps, SettingsView as SettingsViewId } from './types'
const SETTINGS_VIEWS: readonly SettingsViewId[] = [
@ -56,7 +54,6 @@ const SETTINGS_VIEWS: readonly SettingsViewId[] = [
'billing',
'plugins',
'sessions',
'system',
'about'
]
@ -156,13 +153,6 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
label: t.settings.nav.notifications,
onSelect: () => setActiveView('notifications')
},
{
active: activeView === 'system',
icon: Cpu,
id: 'system',
label: t.settings.nav.system,
onSelect: () => setActiveView('system')
},
{
active: activeView === 'billing',
icon: BarChart3,
@ -326,8 +316,6 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
<KeysSettings view={keysView} />
) : activeView === 'notifications' ? (
<NotificationsSettings />
) : activeView === 'system' ? (
<SystemSettings />
) : activeView === 'billing' ? (
<BillingSettings />
) : activeView === 'plugins' ? (

View file

@ -1,90 +0,0 @@
import { useStore } from '@nanostores/react'
import { SegmentedControl } from '@/components/ui/segmented-control'
import { useI18n } from '@/i18n'
import { triggerHaptic } from '@/lib/haptics'
import { Cpu } from '@/lib/icons'
import { $hapticsMuted, setHapticsMuted } from '@/store/haptics'
import { $keepAwake, setKeepAwake } from '@/store/keep-awake'
import { $translucency, setTranslucency } from '@/store/translucency'
import { $zoomPercent, setZoomPercent } from '@/store/zoom'
import { ListRow, SectionHeading, SettingsContent, ToggleRow } from './primitives'
// UI scale presets as zoom percentages (100 = browser default); the ids double
// as the percent sent to main. A Cmd/Ctrl +/- step between presets highlights
// nothing, and the row description keeps showing the exact current percent.
const UI_SCALE_PRESETS = ['90', '100', '110', '125', '150', '175'] as const
type UiScalePreset = (typeof UI_SCALE_PRESETS)[number]
export function SystemSettings() {
const { t } = useI18n()
const s = t.settings.system
const keepAwake = useStore($keepAwake)
const translucency = useStore($translucency)
const zoomPercent = useStore($zoomPercent)
const hapticsMuted = useStore($hapticsMuted)
const uiScaleOptions = UI_SCALE_PRESETS.map(preset => ({ id: preset, label: `${preset}%` }))
const matchedScale = UI_SCALE_PRESETS.find(preset => Number(preset) === zoomPercent) ?? ('' as UiScalePreset)
return (
<SettingsContent>
<SectionHeading icon={Cpu} title={s.title} />
<p className="mb-2 max-w-2xl text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">
{s.intro}
</p>
<ToggleRow checked={keepAwake} description={s.keepAwakeDesc} label={s.keepAwakeTitle} onChange={setKeepAwake} />
<ListRow
action={
<SegmentedControl
onChange={id => {
triggerHaptic('selection')
setZoomPercent(Number(id))
}}
options={uiScaleOptions}
value={matchedScale}
/>
}
description={s.uiScaleDesc(zoomPercent)}
title={s.uiScaleTitle}
/>
<ListRow
action={
<div className="flex items-center gap-3">
<input
aria-label={s.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={s.translucencyDesc}
title={s.translucencyTitle}
/>
<ToggleRow
checked={!hapticsMuted}
description={s.hapticsDesc}
label={s.hapticsTitle}
onChange={on => setHapticsMuted(!on)}
/>
</SettingsContent>
)
}

View file

@ -14,7 +14,6 @@ export type SettingsView =
| 'plugins'
| 'providers'
| 'sessions'
| 'system'
| `config:${string}`
export type EnvPatch = Partial<Pick<EnvVarInfo, 'is_set' | 'redacted_value'>>

View file

@ -325,8 +325,7 @@ export const en: Translations = {
about: 'About',
billing: 'Billing',
notifications: 'Notifications',
plugins: 'Plugins',
system: 'System'
plugins: 'Plugins'
},
plugins: {
title: 'Desktop plugins',
@ -380,19 +379,6 @@ export const en: Translations = {
completionSoundDesc: 'Plays when an agent turn finishes. Pick a preset and preview it here.',
completionSoundPreview: 'Preview'
},
system: {
title: 'System',
intro: 'How Hermes behaves on this machine. These are device-local — each computer keeps its own settings.',
keepAwakeTitle: 'Keep computer awake',
keepAwakeDesc: 'Stop this machine from sleeping so long or overnight runs keep going. The display can still dim.',
translucencyTitle: 'Window Translucency',
translucencyDesc: 'See your desktop through the whole window. macOS and Windows only.',
uiScaleTitle: 'UI Scale',
uiScaleDesc: (percent: number) =>
`Scales text and controls across the whole app. Cmd/Ctrl with +, - and 0 also works. Current: ${percent}%.`,
hapticsTitle: 'Haptic Feedback',
hapticsDesc: 'Trackpad taps on actions and toggles. Supported hardware only (macOS).'
},
sections: {
model: 'Model',
chat: 'Chat',
@ -424,6 +410,11 @@ 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.',
uiScaleTitle: 'UI Scale',
uiScaleDesc: (percent: number) =>
`Scales text and controls across the whole app. Cmd/Ctrl with +, - and 0 also works. Current: ${percent}%.`,
translucencyTitle: 'Window Translucency',
translucencyDesc: 'See your desktop through the whole window. macOS and Windows only.',
backdropTitle: 'Chat Backdrop',
backdropDesc: 'The faint statue image behind the conversation.',
embedsTitle: 'Inline Embeds',
@ -532,7 +523,9 @@ export const en: Translations = {
failedLoad: 'Settings failed to load',
autosaveFailed: 'Autosave failed',
imported: 'Config imported',
invalidJson: 'Invalid config JSON'
invalidJson: 'Invalid config JSON',
keepAwakeTitle: 'Keep computer awake',
keepAwakeDesc: 'Stop this machine from sleeping so long or overnight runs keep going. The display can still dim.'
},
credentials: {
pasteKey: 'Paste key',

View file

@ -225,8 +225,7 @@ export const ja = defineLocale({
archivedChats: 'アーカイブ済みチャット',
about: '情報',
billing: '請求',
notifications: '通知',
system: 'システム'
notifications: '通知'
},
notifications: {
title: '通知',
@ -266,18 +265,6 @@ export const ja = defineLocale({
completionSoundDesc: 'エージェントのターン終了時に再生されます。プリセットを選んでここで試聴できます。',
completionSoundPreview: '試聴'
},
system: {
title: 'システム',
intro: 'この端末での Hermes の動作。設定は端末ごとに保存されます。',
keepAwakeTitle: 'コンピューターをスリープさせない',
keepAwakeDesc: '本体のスリープを防ぎ、長時間や夜通しの実行を継続します。画面は暗転できます。',
translucencyTitle: 'ウィンドウの透過',
translucencyDesc: 'ウィンドウ全体を透過させてデスクトップを表示します。macOS と Windows のみ。',
uiScaleTitle: 'UI スケール',
uiScaleDesc: (percent: number) => `アプリ全体の文字と UI を拡大縮小します。Cmd/Ctrl と +、-、0 でも変更できます。現在: ${percent}%`,
hapticsTitle: '触覚フィードバック',
hapticsDesc: '操作やトグル時にトラックパッドの触覚を返します。対応ハードウェアのみmacOS。'
},
sections: {
model: 'モデル',
chat: 'チャット',
@ -309,6 +296,11 @@ export const ja = defineLocale({
colorModeDesc: '固定モードを選ぶか、Hermes をシステム設定に合わせます。',
toolViewTitle: 'ツール呼び出しの表示',
toolViewDesc: 'プロダクト表示は生のツールペイロードを隠し、テクニカル表示は入出力をすべて表示します。',
uiScaleTitle: 'UI スケール',
uiScaleDesc: (percent: number) =>
`アプリ全体の文字と UI を拡大縮小します。Cmd/Ctrl と +、-、0 でも変更できます。現在: ${percent}%`,
translucencyTitle: 'ウィンドウの透過',
translucencyDesc: 'ウィンドウ全体を透過させてデスクトップを表示します。macOS と Windows のみ。',
backdropTitle: 'チャット背景',
backdropDesc: '会話の背後に表示される淡い彫像の画像。',
embedsTitle: 'インライン埋め込み',
@ -629,7 +621,9 @@ export const ja = defineLocale({
failedLoad: '設定の読み込みに失敗しました',
autosaveFailed: '自動保存に失敗しました',
imported: '設定をインポートしました',
invalidJson: '設定 JSON が無効です'
invalidJson: '設定 JSON が無効です',
keepAwakeTitle: 'コンピューターをスリープさせない',
keepAwakeDesc: '本体のスリープを防ぎ、長時間や夜通しの実行を継続します。画面は暗転できます。'
},
credentials: {
pasteKey: 'キーを貼り付け',

View file

@ -284,7 +284,6 @@ export interface Translations {
billing: string
notifications: string
plugins: string
system: string
}
plugins: {
title: string
@ -318,18 +317,6 @@ export interface Translations {
completionSoundDesc: string
completionSoundPreview: string
}
system: {
title: string
intro: string
keepAwakeTitle: string
keepAwakeDesc: string
translucencyTitle: string
translucencyDesc: string
uiScaleTitle: string
uiScaleDesc: (percent: number) => string
hapticsTitle: string
hapticsDesc: string
}
sections: Record<string, string>
searchPlaceholder: Record<'about' | 'config' | 'gateway' | 'keys' | 'mcp' | 'sessions', string>
modeOptions: Record<'light' | 'dark' | 'system', ModeOptionCopy>
@ -340,6 +327,10 @@ export interface Translations {
colorModeDesc: string
toolViewTitle: string
toolViewDesc: string
uiScaleTitle: string
uiScaleDesc: (percent: number) => string
translucencyTitle: string
translucencyDesc: string
backdropTitle: string
backdropDesc: string
embedsTitle: string
@ -444,6 +435,8 @@ export interface Translations {
autosaveFailed: string
imported: string
invalidJson: string
keepAwakeTitle: string
keepAwakeDesc: string
}
credentials: {
pasteKey: string

View file

@ -219,8 +219,7 @@ export const zhHant = defineLocale({
archivedChats: '已封存聊天',
about: '關於',
billing: '帳單',
notifications: '通知',
system: '系統'
notifications: '通知'
},
notifications: {
title: '通知',
@ -259,18 +258,6 @@ export const zhHant = defineLocale({
completionSoundDesc: '代理回合結束時播放。可在此選擇預設並預覽。',
completionSoundPreview: '預覽'
},
system: {
title: '系統',
intro: 'Hermes 在這台電腦上的行為。設定會依裝置保存,每台電腦各自獨立。',
keepAwakeTitle: '保持電腦喚醒',
keepAwakeDesc: '阻止本機睡眠,讓長時間或整夜執行持續進行。螢幕仍可變暗。',
translucencyTitle: '視窗透明',
translucencyDesc: '讓整個視窗透出桌面。僅支援 macOS 與 Windows。',
uiScaleTitle: '介面縮放',
uiScaleDesc: (percent: number) => `縮放整個應用程式的文字與介面。也可使用 Cmd/Ctrl 加 +、- 或 0 調整。目前:${percent}%`,
hapticsTitle: '觸覺回饋',
hapticsDesc: '在操作與開關時觸發觸控板輕觸。僅支援對應硬體macOS。'
},
sections: {
model: '模型',
chat: '聊天',
@ -301,6 +288,11 @@ export const zhHant = defineLocale({
colorModeDesc: '選擇固定模式,或讓 Hermes 跟隨系統設定。',
toolViewTitle: '工具呼叫顯示',
toolViewDesc: '產品模式會隱藏原始工具 payload技術模式會顯示完整輸入/輸出。',
uiScaleTitle: '介面縮放',
uiScaleDesc: (percent: number) =>
`縮放整個應用程式的文字與介面。也可使用 Cmd/Ctrl 加 +、- 或 0 調整。目前:${percent}%`,
translucencyTitle: '視窗透明',
translucencyDesc: '讓整個視窗透出桌面。僅支援 macOS 與 Windows。',
backdropTitle: '聊天背景',
backdropDesc: '對話後方那張淡淡的雕像圖片。',
embedsTitle: '內嵌預覽',
@ -617,7 +609,9 @@ export const zhHant = defineLocale({
failedLoad: '設定載入失敗',
autosaveFailed: '自動儲存失敗',
imported: '設定已匯入',
invalidJson: '設定 JSON 無效'
invalidJson: '設定 JSON 無效',
keepAwakeTitle: '保持電腦喚醒',
keepAwakeDesc: '阻止本機睡眠,讓長時間或整夜執行持續進行。螢幕仍可變暗。'
},
credentials: {
pasteKey: '貼上金鑰',

View file

@ -316,8 +316,7 @@ export const zh: Translations = {
about: '关于',
billing: '账单',
notifications: '通知',
plugins: '插件',
system: '系统'
plugins: '插件'
},
plugins: {
title: '桌面插件',
@ -370,18 +369,6 @@ export const zh: Translations = {
completionSoundDesc: '智能体回合结束时播放。可在此选择预设并预览。',
completionSoundPreview: '预览'
},
system: {
title: '系统',
intro: 'Hermes 在这台电脑上的行为。设置按设备保存,每台电脑各自独立。',
keepAwakeTitle: '保持电脑唤醒',
keepAwakeDesc: '阻止本机休眠,让长时间或通宵运行继续进行。屏幕仍可变暗。',
translucencyTitle: '窗口透明',
translucencyDesc: '让整个窗口透出桌面。仅支持 macOS 和 Windows。',
uiScaleTitle: '界面缩放',
uiScaleDesc: (percent: number) => `缩放整个应用的文字和界面。也可使用 Cmd/Ctrl 加 +、- 或 0 调整。当前:${percent}%`,
hapticsTitle: '触感反馈',
hapticsDesc: '在操作和开关时触发触控板轻触。仅支持相应硬件macOS。'
},
sections: {
model: '模型',
chat: '对话',
@ -412,6 +399,11 @@ export const zh: Translations = {
colorModeDesc: '选择固定模式,或让 Hermes 跟随系统设置。',
toolViewTitle: '工具调用显示',
toolViewDesc: '产品模式隐藏原始工具数据;技术模式显示完整输入/输出。',
uiScaleTitle: '界面缩放',
uiScaleDesc: (percent: number) =>
`缩放整个应用的文字和界面。也可使用 Cmd/Ctrl 加 +、- 或 0 调整。当前:${percent}%`,
translucencyTitle: '窗口透明',
translucencyDesc: '让整个窗口透出桌面。仅支持 macOS 和 Windows。',
backdropTitle: '聊天背景',
backdropDesc: '对话后方那张淡淡的雕像图片。',
embedsTitle: '内嵌预览',
@ -728,7 +720,9 @@ export const zh: Translations = {
failedLoad: '设置加载失败',
autosaveFailed: '自动保存失败',
imported: '配置已导入',
invalidJson: '配置 JSON 无效'
invalidJson: '配置 JSON 无效',
keepAwakeTitle: '保持电脑唤醒',
keepAwakeDesc: '阻止本机休眠,让长时间或通宵运行继续进行。屏幕仍可变暗。'
},
credentials: {
pasteKey: '粘贴密钥',