mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:46:59 +00:00
Merge pull request #73161 from NousResearch/bb/default-zoom-out
feat(desktop): default UI zoom to Appearance 90% preset
This commit is contained in:
commit
7ddf3f1500
6 changed files with 71 additions and 35 deletions
|
|
@ -120,6 +120,17 @@ export function createSandbox(prefix: string): Sandbox {
|
|||
'utf8',
|
||||
)
|
||||
|
||||
// Pin Chromium actual-size zoom (level 0) for the suite. Fresh installs
|
||||
// ship DEFAULT_ZOOM_LEVEL at the Appearance 90% preset, but Playwright
|
||||
// click hit-testing and the committed visual baselines were calibrated at
|
||||
// 100%. Without this file every sandbox would inherit the product default
|
||||
// and fail pointer interception + snapshot diffs.
|
||||
fs.writeFileSync(
|
||||
path.join(userDataDir, 'zoom-state.json'),
|
||||
JSON.stringify({ zoomLevel: 0 }, null, 2),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
return {
|
||||
root,
|
||||
hermesHome,
|
||||
|
|
|
|||
|
|
@ -5187,7 +5187,7 @@ function buildApplicationMenu() {
|
|||
label: 'Actual Size',
|
||||
accelerator: 'CommandOrControl+0',
|
||||
click: () => {
|
||||
setAndPersistZoomLevel(mainWindow, 0)
|
||||
setAndPersistZoomLevel(mainWindow, DEFAULT_ZOOM_LEVEL)
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -5195,7 +5195,7 @@ function buildApplicationMenu() {
|
|||
accelerator: 'CommandOrControl+Plus',
|
||||
click: () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
setAndPersistZoomLevel(mainWindow, mainWindow.webContents.getZoomLevel() + 0.1)
|
||||
setAndPersistZoomLevel(mainWindow, mainWindow.webContents.getZoomLevel() + ZOOM_STEP)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -5204,7 +5204,7 @@ function buildApplicationMenu() {
|
|||
accelerator: 'CommandOrControl+-',
|
||||
click: () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
setAndPersistZoomLevel(mainWindow, mainWindow.webContents.getZoomLevel() - 0.1)
|
||||
setAndPersistZoomLevel(mainWindow, mainWindow.webContents.getZoomLevel() - ZOOM_STEP)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -5283,8 +5283,10 @@ function installPreviewShortcut(window) {
|
|||
// read it back on did-finish-load to re-apply after reloads or crash recovery.
|
||||
import {
|
||||
applyZoomLevel,
|
||||
DEFAULT_ZOOM_LEVEL,
|
||||
installZoomReassertOnWindowEvents,
|
||||
percentToZoomLevel,
|
||||
ZOOM_STEP,
|
||||
ZOOM_STORAGE_KEY,
|
||||
zoomLevelToPercent,
|
||||
zoomWiringForWindowKind
|
||||
|
|
@ -5328,31 +5330,33 @@ function restorePersistedZoomLevel(window) {
|
|||
return
|
||||
}
|
||||
|
||||
// Fall back to localStorage for installs that predate zoom-state.json,
|
||||
// migrating the value into the JSON store on first read.
|
||||
// No JSON yet: paint the shipped default immediately so a fresh install
|
||||
// doesn't flash Chromium 100%, then try localStorage for pre-JSON installs
|
||||
// and overwrite if a legacy value is there.
|
||||
applyZoomLevel(window.webContents, DEFAULT_ZOOM_LEVEL)
|
||||
|
||||
window.webContents
|
||||
.executeJavaScript(
|
||||
`(() => { try { return localStorage.getItem(${JSON.stringify(ZOOM_STORAGE_KEY)}) } catch { return null } })()`
|
||||
)
|
||||
.then(stored => {
|
||||
if (stored == null || !window || window.isDestroyed()) {
|
||||
if (!window || window.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Notify the renderer too — otherwise the Appearance UI Scale control
|
||||
// can stay stuck at 100% even though the window zoom was restored.
|
||||
const applied = applyZoomLevel(window.webContents, Number(stored))
|
||||
const level = stored == null ? DEFAULT_ZOOM_LEVEL : Number(stored)
|
||||
const applied = applyZoomLevel(window.webContents, level)
|
||||
writeZoomState(applied)
|
||||
})
|
||||
.catch(error => rememberLog(`[zoom] restore failed: ${error?.message || error}`))
|
||||
}
|
||||
|
||||
function installZoomShortcuts(window) {
|
||||
// Override Ctrl/Cmd + +/-/0 with half the default zoom step (0.1 vs 0.2).
|
||||
// The menu items handle this on macOS (where the menu is always present),
|
||||
// but on Linux/Windows the menu is null and Chromium's default handler
|
||||
// would use the full 0.2 step, so we intercept here for consistency.
|
||||
const ZOOM_STEP = 0.1
|
||||
// Override Ctrl/Cmd + +/-/0 with half Chromium's default zoom step (ZOOM_STEP
|
||||
// is 0.1 vs Chromium's 0.2). The menu items handle this on macOS (where the
|
||||
// menu is always present), but on Linux/Windows the menu is null and
|
||||
// Chromium's default handler would use the full 0.2 step, so we intercept
|
||||
// here for consistency. Ctrl/Cmd+0 resets to DEFAULT_ZOOM_LEVEL, not Chromium 0.
|
||||
window.webContents.on('before-input-event', (event, input) => {
|
||||
const mod = IS_MAC ? input.meta : input.control
|
||||
|
||||
|
|
@ -5368,7 +5372,7 @@ function installZoomShortcuts(window) {
|
|||
}
|
||||
|
||||
event.preventDefault()
|
||||
setAndPersistZoomLevel(window, 0)
|
||||
setAndPersistZoomLevel(window, DEFAULT_ZOOM_LEVEL)
|
||||
} else if (key === '=' || key === '+') {
|
||||
// Zoom-in must accept the shift modifier: on US layouts Plus is
|
||||
// physically Shift+=, so Cmd+Plus arrives as Cmd+Shift+'+' (or '='
|
||||
|
|
@ -9266,7 +9270,8 @@ ipcMain.handle('hermes:window:openInstance', async () => {
|
|||
// shortcuts and the View menu. Reads and writes target the asking window.
|
||||
ipcMain.handle('hermes:zoom:get', event => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender)
|
||||
const level = window && !window.isDestroyed() ? window.webContents.getZoomLevel() : 0
|
||||
const level =
|
||||
window && !window.isDestroyed() ? window.webContents.getZoomLevel() : DEFAULT_ZOOM_LEVEL
|
||||
|
||||
return { level, percent: zoomLevelToPercent(level) }
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ import { test, vi } from 'vitest'
|
|||
import {
|
||||
applyZoomLevel,
|
||||
clampZoomLevel,
|
||||
DEFAULT_ZOOM_LEVEL,
|
||||
installZoomReassertOnWindowEvents,
|
||||
percentToZoomLevel,
|
||||
ZOOM_RESIZE_REASSERT_DELAY_MS,
|
||||
ZOOM_STEP,
|
||||
ZOOM_STORAGE_KEY,
|
||||
zoomLevelToPercent,
|
||||
zoomReassertWindowEvents,
|
||||
|
|
@ -24,26 +26,32 @@ test('storage key stays stable so persisted zoom survives upgrades', () => {
|
|||
assert.equal(ZOOM_STORAGE_KEY, 'hermes:desktop:zoomLevel')
|
||||
})
|
||||
|
||||
test('default zoom matches the Appearance 90% preset', () => {
|
||||
assert.equal(ZOOM_STEP, 0.1)
|
||||
assert.equal(zoomLevelToPercent(DEFAULT_ZOOM_LEVEL), 90)
|
||||
assert.equal(DEFAULT_ZOOM_LEVEL, percentToZoomLevel(90))
|
||||
})
|
||||
|
||||
test('clampZoomLevel rejects garbage and enforces bounds', () => {
|
||||
assert.equal(clampZoomLevel(NaN), 0)
|
||||
assert.equal(clampZoomLevel(Infinity), 0)
|
||||
assert.equal(clampZoomLevel(undefined), 0)
|
||||
assert.equal(clampZoomLevel('2'), 0)
|
||||
assert.equal(clampZoomLevel(NaN), DEFAULT_ZOOM_LEVEL)
|
||||
assert.equal(clampZoomLevel(Infinity), DEFAULT_ZOOM_LEVEL)
|
||||
assert.equal(clampZoomLevel(undefined), DEFAULT_ZOOM_LEVEL)
|
||||
assert.equal(clampZoomLevel('2'), DEFAULT_ZOOM_LEVEL)
|
||||
assert.equal(clampZoomLevel(0.3), 0.3)
|
||||
assert.equal(clampZoomLevel(-42), -9)
|
||||
assert.equal(clampZoomLevel(42), 9)
|
||||
})
|
||||
|
||||
test('level 0 is exactly 100 percent', () => {
|
||||
test('level 0 is exactly 100 percent (Chromium actual-size baseline)', () => {
|
||||
assert.equal(zoomLevelToPercent(0), 100)
|
||||
assert.equal(percentToZoomLevel(100), 0)
|
||||
})
|
||||
|
||||
test('percentToZoomLevel rejects garbage', () => {
|
||||
assert.equal(percentToZoomLevel(NaN), 0)
|
||||
assert.equal(percentToZoomLevel(0), 0)
|
||||
assert.equal(percentToZoomLevel(-50), 0)
|
||||
assert.equal(percentToZoomLevel(undefined), 0)
|
||||
test('percentToZoomLevel rejects garbage by falling back to the shipped default', () => {
|
||||
assert.equal(percentToZoomLevel(NaN), DEFAULT_ZOOM_LEVEL)
|
||||
assert.equal(percentToZoomLevel(0), DEFAULT_ZOOM_LEVEL)
|
||||
assert.equal(percentToZoomLevel(-50), DEFAULT_ZOOM_LEVEL)
|
||||
assert.equal(percentToZoomLevel(undefined), DEFAULT_ZOOM_LEVEL)
|
||||
})
|
||||
|
||||
test('preset percentages roundtrip within rounding', () => {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@
|
|||
* Pure helpers for window zoom. The main process owns webContents.setZoomLevel,
|
||||
* so the menu items, the Ctrl/Cmd shortcuts, and the settings UI all funnel
|
||||
* through this one clamped scale. Percent is the user-facing unit (100 = the
|
||||
* default size); Chromium's internal unit is the zoom level, where
|
||||
* factor = 1.2 ^ level.
|
||||
* Chromium actual-size baseline); Chromium's internal unit is the zoom level,
|
||||
* where factor = 1.2 ^ level.
|
||||
*
|
||||
* Our shipped default is the Appearance 90% preset — tight enough to feel
|
||||
* denser than Chromium 100%, and selected in the UI Scale control on first run.
|
||||
*/
|
||||
|
||||
export const ZOOM_STORAGE_KEY = 'hermes:desktop:zoomLevel'
|
||||
|
|
@ -12,9 +15,15 @@ const ZOOM_FACTOR_BASE = 1.2
|
|||
const MIN_ZOOM_LEVEL = -9
|
||||
const MAX_ZOOM_LEVEL = 9
|
||||
|
||||
/** Half Chromium's default step; matching the shortcuts and View menu. */
|
||||
export const ZOOM_STEP = 0.1
|
||||
|
||||
/** Appearance 90% preset. Fresh installs + Actual Size / Ctrl+0. */
|
||||
export const DEFAULT_ZOOM_LEVEL = Math.log(0.9) / Math.log(ZOOM_FACTOR_BASE)
|
||||
|
||||
export function clampZoomLevel(value) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return 0
|
||||
return DEFAULT_ZOOM_LEVEL
|
||||
}
|
||||
|
||||
return Math.min(Math.max(value, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL)
|
||||
|
|
@ -26,7 +35,7 @@ export function zoomLevelToPercent(level) {
|
|||
|
||||
export function percentToZoomLevel(percent) {
|
||||
if (!Number.isFinite(percent) || percent <= 0) {
|
||||
return 0
|
||||
return DEFAULT_ZOOM_LEVEL
|
||||
}
|
||||
|
||||
return clampZoomLevel(Math.log(percent / 100) / Math.log(ZOOM_FACTOR_BASE))
|
||||
|
|
|
|||
|
|
@ -64,10 +64,11 @@ 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.
|
||||
// UI scale presets, as zoom percentages. 100 is Chromium's actual-size
|
||||
// baseline; the shipped default is the 90% preset. 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]
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@
|
|||
|
||||
import { atom } from 'nanostores'
|
||||
|
||||
export const $zoomPercent = atom<number>(100)
|
||||
// Mirror DEFAULT_ZOOM_LEVEL (90%) so Appearance doesn't flash 100% before
|
||||
// the main-process zoom.get() resolves. Keep in sync with electron/zoom.ts.
|
||||
export const $zoomPercent = atom<number>(90)
|
||||
|
||||
export function setZoomPercent(percent: number): void {
|
||||
window.hermesDesktop?.zoom?.setPercent(percent)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue