What
A display scale chooser in Settings, like macOS Displays > Scaled, so smaller screens can fit more on screen. Discrete steps rather than a free slider, previewed the way macOS does it ("Larger Text ... More Space").
Requested by Jay.
Why it fits cleanly
theme-store.ts already establishes the exact pattern this needs: localStorage persistence plus document.documentElement.style.setProperty for CSS custom properties, applied at the root. A scale setting is the same shape, so it should mirror that store rather than invent a mechanism.
There is currently no UI scale handling anywhere in desktop/src (no zoom, no root font-size scaling, no density setting), so this is greenfield rather than a refactor.
Proposed approach
Root-level CSS zoom on :root, driven by a persisted uiScale value, with discrete steps (for example 0.8 / 0.9 / 1.0 / 1.1 / 1.25) surfaced in Settings next to Themes.
zoom is preferred over transform: scale() because transform breaks fixed positioning and pointer hit-testing, which a windowing UI depends on heavily.
The real risks, all found by reading the code
These are the reason this is not a one-line change.
1. 27 viewport reads across 18 files would desync. window.innerWidth / window.innerHeight are the layout viewport and are NOT affected by CSS zoom, while layout and CSS media queries ARE. Window.tsx:50-51 derives its bounds from exactly these. Every one of those reads needs to go through a single useEffectiveViewport() helper that divides by the active scale, or window clamping, maximize and snap maths will be wrong at any scale other than 1.0.
2. JS breakpoints and CSS breakpoints would diverge. use-is-mobile.ts and use-device-mode.ts decide mobile vs desktop from window.innerWidth in JS, while CSS media queries evaluate against the zoomed viewport. Under zoom those two disagree, so the OS could style itself as mobile while behaving as desktop, or scaling down could trip the mobile breakpoint and switch the whole shell unexpectedly. This needs one deliberate decision, recorded: does scaling affect the device-mode breakpoint or not? My view is it should NOT (scale is an appearance preference, device mode is a form-factor fact), which means device-mode reads must stay on the true viewport while layout reads use the effective one.
3. Agent desktop control reads bounds. use-desktop-control.ts is how an agent reads screen geometry and drives windows. If it reports unscaled bounds while windows are laid out scaled, agent-driven window placement lands in the wrong place. Whatever coordinate space is chosen has to be the one the control API reports, and the agent manual needs updating to say which.
Acceptance
Not in scope
Per-display scaling, fractional/free slider, and remembering scale per device.
What
A display scale chooser in Settings, like macOS Displays > Scaled, so smaller screens can fit more on screen. Discrete steps rather than a free slider, previewed the way macOS does it ("Larger Text ... More Space").
Requested by Jay.
Why it fits cleanly
theme-store.tsalready establishes the exact pattern this needs:localStoragepersistence plusdocument.documentElement.style.setPropertyfor CSS custom properties, applied at the root. A scale setting is the same shape, so it should mirror that store rather than invent a mechanism.There is currently no UI scale handling anywhere in
desktop/src(nozoom, no root font-size scaling, no density setting), so this is greenfield rather than a refactor.Proposed approach
Root-level CSS
zoomon:root, driven by a persisteduiScalevalue, with discrete steps (for example 0.8 / 0.9 / 1.0 / 1.1 / 1.25) surfaced in Settings next to Themes.zoomis preferred overtransform: scale()because transform breaks fixed positioning and pointer hit-testing, which a windowing UI depends on heavily.The real risks, all found by reading the code
These are the reason this is not a one-line change.
1. 27 viewport reads across 18 files would desync.
window.innerWidth/window.innerHeightare the layout viewport and are NOT affected by CSSzoom, while layout and CSS media queries ARE.Window.tsx:50-51derives its bounds from exactly these. Every one of those reads needs to go through a singleuseEffectiveViewport()helper that divides by the active scale, or window clamping, maximize and snap maths will be wrong at any scale other than 1.0.2. JS breakpoints and CSS breakpoints would diverge.
use-is-mobile.tsanduse-device-mode.tsdecide mobile vs desktop fromwindow.innerWidthin JS, while CSS media queries evaluate against the zoomed viewport. Under zoom those two disagree, so the OS could style itself as mobile while behaving as desktop, or scaling down could trip the mobile breakpoint and switch the whole shell unexpectedly. This needs one deliberate decision, recorded: does scaling affect the device-mode breakpoint or not? My view is it should NOT (scale is an appearance preference, device mode is a form-factor fact), which means device-mode reads must stay on the true viewport while layout reads use the effective one.3. Agent desktop control reads bounds.
use-desktop-control.tsis how an agent reads screen geometry and drives windows. If it reports unscaled bounds while windows are laid out scaled, agent-driven window placement lands in the wrong place. Whatever coordinate space is chosen has to be the one the control API reports, and the agent manual needs updating to say which.Acceptance
use-desktop-control.tsreports coordinates in the documented space, agent manual updatedNot in scope
Per-display scaling, fractional/free slider, and remembering scale per device.