feat(browser): persist default browser zoom#4765
Conversation
WalkthroughThis PR implements persistent per-user default browser zoom configuration. It introduces shared zoom utilities to normalize and convert zoom levels, extends the persistence layer and RPC schema to handle the new field, adds store state management with normalization and hydration, updates the browser pane to apply the default zoom to newly opened webviews, and provides a settings UI component for users to configure it. ChangesDefault browser zoom persistence
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Reviewed changes — persists one app-wide default browser zoom so users who need larger text can set it once and have future local browser tabs open at that zoom across repos and worktrees. Zoom shortcuts update the same persisted default; Reset returns to 100%; existing open tabs are left alone.
- Add
src/shared/browser-page-zoom.ts— centralized zoom constants, normalization, level-to-percent conversion, and next-level stepping shared across main and renderer. - Refactor
browser-page-zoom.tsrenderer module — re-exports shared functions, keeps webview-specificapplyBrowserPageZoom(with optionalresetLevel) and newsetBrowserPageZoomLevel. - Add
browserDefaultZoomLevelto store slice — persisted state with normalization at hydration, initial value, and setter boundaries. - Normalize in
Persistence—browserDefaultZoomLevelis clamped to Electron-supported steps on read and write. - Accept
browserDefaultZoomLevelin RPCui.set— validates asz.number().finite().optional(). - New
BrowserDefaultZoomSettingcomponent — a select dropdown listing all supported zoom levels by percentage. - Wire into
BrowserPanesettings andbrowser-search— setting is searchable; insertion shifts existing search-entry indices. - Apply default zoom to new browser tabs —
browserZoomPercentinitializes from default; new/reparented webviews receive default zoom ondom-ready. - Zoom shortcuts update the default — in/out/reset on any tab persist the new zoom level as the new default.
- Zoom indicator compares against default — indicator visibility uses
browserDefaultZoomPercentinstead of hardcoded 100.
⚠️ Zoom indicator opacity check still uses hardcoded 100 instead of default
The showBrowserZoomIndicator computed on line 4263 correctly compares against browserDefaultZoomPercent, but the opacity CSS class on line 4796 still checks browserZoomPercent === 100. When the configured default is non-100% and the tab is at that default, the indicator is rendered with opacity-80 (since 150 !== 100) despite aria-hidden being true. The user sees a ghost indicator.
Technical details
# Zoom indicator opacity still uses hardcoded 100
## Affected sites
- `src/renderer/src/components/browser-pane/BrowserPane.tsx:4796` — `browserZoomPercent === 100 ? 'opacity-0'` should use `browserDefaultZoomPercent` instead of `100`
## Required outcome
- The opacity ternary should compare `browserZoomPercent === browserDefaultZoomPercent` so the indicator is fully hidden when the tab's zoom equals the configured default, regardless of what that default is.
- The feedback path (`browserZoomFeedbackVisible`) already works correctly and does not need changes.
## Suggested approach
Change line 4796 from:
```
browserZoomPercent === 100
```
to:
```
browserZoomPercent === browserDefaultZoomPercent
```Note: 1 inline comment(s) dropped because they did not anchor to lines inside the PR diff:
src/renderer/src/components/browser-pane/BrowserPane.tsx:4796(RIGHT) — line 4796 (RIGHT) is not inside a diff hunk
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/main/persistence.test.ts (1)
2862-2869: ⚡ Quick winConsider testing additional normalization cases.
The test verifies one normalization case (1.26 → 1.5) but does not cover:
- Values already at valid zoom steps (should not change)
- Boundary cases (very small or very large values)
- Other normalization examples (e.g., what does 0.9 normalize to?)
Adding a few parameterized cases would strengthen confidence that the normalization logic handles the full range of inputs correctly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/persistence.test.ts` around lines 2862 - 2869, Add parameterized assertions around the normalization behavior of browserDefaultZoomLevel using the same createStore()/store.updateUI()/store.getUI() flow: include cases where input equals an exact valid step (e.g., 1.5 should remain 1.5), several boundary extremes (very small like 0.1 and very large like 3.0), and intermediate values (e.g., 0.9, 1.26, 2.74) to assert expected normalized outputs; implement this by replacing the single example test with a table-driven loop that calls store.updateUI({ browserDefaultZoomLevel: <input> }) and expects store.getUI().browserDefaultZoomLevel to equal the correct normalized value for each case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/main/persistence.test.ts`:
- Around line 2862-2869: The test for normalizing default browser zoom UI writes
should also verify persistence: after calling createStore(), store.updateUI({
browserDefaultZoomLevel: 1.26 }) and asserting
store.getUI().browserDefaultZoomLevel === 1.5, call await store.flush() (or the
test harness persistence flush) then create or load a fresh store (e.g., await
createStore() again) and assert the reloaded
store.getUI().browserDefaultZoomLevel is 1.5 to confirm normalization
round-trips to disk; update the test that uses createStore, store.updateUI,
store.getUI, and flush() accordingly.
In `@src/renderer/src/components/browser-pane/BrowserPane.tsx`:
- Around line 4262-4263: The zoom indicator visibility uses
browserDefaultZoomPercent, but the opacity check still compares against the
literal 100; update any opacity/visibility comparisons (the
showBrowserZoomIndicator calculation and the opacity branch that currently
compares to 100) to use browserDefaultZoomPercent instead so the badge is hidden
when zoom equals the persisted default; search for occurrences in
BrowserPane.tsx (the showBrowserZoomIndicator declaration and the opacity branch
around the zoom badge rendering, including the similar block at the later
region) and replace the hardcoded 100 comparison with browserDefaultZoomPercent.
---
Nitpick comments:
In `@src/main/persistence.test.ts`:
- Around line 2862-2869: Add parameterized assertions around the normalization
behavior of browserDefaultZoomLevel using the same
createStore()/store.updateUI()/store.getUI() flow: include cases where input
equals an exact valid step (e.g., 1.5 should remain 1.5), several boundary
extremes (very small like 0.1 and very large like 3.0), and intermediate values
(e.g., 0.9, 1.26, 2.74) to assert expected normalized outputs; implement this by
replacing the single example test with a table-driven loop that calls
store.updateUI({ browserDefaultZoomLevel: <input> }) and expects
store.getUI().browserDefaultZoomLevel to equal the correct normalized value for
each case.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 6dec557a-a0bd-4681-a7bd-aa5659d7efc3
📒 Files selected for processing (16)
src/main/persistence.test.tssrc/main/persistence.tssrc/main/runtime/rpc/methods/client-ui.test.tssrc/main/runtime/rpc/methods/client-ui.tssrc/renderer/src/components/browser-pane/BrowserPane.tsxsrc/renderer/src/components/browser-pane/browser-page-zoom.test.tssrc/renderer/src/components/browser-pane/browser-page-zoom.tssrc/renderer/src/components/settings/BrowserDefaultZoomSetting.tsxsrc/renderer/src/components/settings/BrowserPane.tsxsrc/renderer/src/components/settings/browser-search.test.tssrc/renderer/src/components/settings/browser-search.tssrc/renderer/src/store/slices/ui.test.tssrc/renderer/src/store/slices/ui.tssrc/shared/browser-page-zoom.tssrc/shared/constants.tssrc/shared/types.ts
|
|
||
| it('normalizes default browser zoom UI writes', async () => { | ||
| const store = await createStore() | ||
|
|
||
| store.updateUI({ browserDefaultZoomLevel: 1.26 }) | ||
|
|
||
| expect(store.getUI().browserDefaultZoomLevel).toBe(1.5) | ||
| }) |
There was a problem hiding this comment.
Verify that normalized zoom persists to disk.
The test verifies in-memory normalization but does not confirm the value round-trips through flush() and reload. Since this layer is responsible for persistence and other UI normalization tests in this file (e.g., lines 2662–2676) verify both in-memory and on-disk state, this test should do the same.
✅ Proposed fix to add persistence verification
it('normalizes default browser zoom UI writes', async () => {
const store = await createStore()
store.updateUI({ browserDefaultZoomLevel: 1.26 })
expect(store.getUI().browserDefaultZoomLevel).toBe(1.5)
+ store.flush()
+ const persisted = readDataFile() as { ui?: { browserDefaultZoomLevel?: number } }
+ expect(persisted.ui?.browserDefaultZoomLevel).toBe(1.5)
})🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/persistence.test.ts` around lines 2862 - 2869, The test for
normalizing default browser zoom UI writes should also verify persistence: after
calling createStore(), store.updateUI({ browserDefaultZoomLevel: 1.26 }) and
asserting store.getUI().browserDefaultZoomLevel === 1.5, call await
store.flush() (or the test harness persistence flush) then create or load a
fresh store (e.g., await createStore() again) and assert the reloaded
store.getUI().browserDefaultZoomLevel is 1.5 to confirm normalization
round-trips to disk; update the test that uses createStore, store.updateUI,
store.getUI, and flush() accordingly.
| const showBrowserZoomIndicator = | ||
| browserZoomFeedbackVisible || browserZoomPercent !== browserDefaultZoomPercent |
There was a problem hiding this comment.
Align zoom-indicator opacity with persisted default zoom.
The visibility predicate was updated to compare against browserDefaultZoomPercent, but the opacity branch still compares against 100, so non-100 defaults keep the badge visible even at the default zoom.
🔧 Proposed fix
- browserZoomFeedbackVisible
- ? 'opacity-100'
- : browserZoomPercent === 100
- ? 'opacity-0'
- : 'opacity-80'
+ browserZoomFeedbackVisible
+ ? 'opacity-100'
+ : browserZoomPercent === browserDefaultZoomPercent
+ ? 'opacity-0'
+ : 'opacity-80'Also applies to: 4794-4798
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/renderer/src/components/browser-pane/BrowserPane.tsx` around lines 4262 -
4263, The zoom indicator visibility uses browserDefaultZoomPercent, but the
opacity check still compares against the literal 100; update any
opacity/visibility comparisons (the showBrowserZoomIndicator calculation and the
opacity branch that currently compares to 100) to use browserDefaultZoomPercent
instead so the badge is hidden when zoom equals the persisted default; search
for occurrences in BrowserPane.tsx (the showBrowserZoomIndicator declaration and
the opacity branch around the zoom badge rendering, including the similar block
at the later region) and replace the hardcoded 100 comparison with
browserDefaultZoomPercent.
Jinwoo-H
left a comment
There was a problem hiding this comment.
Thanks, this looks good.
Verified:
- Default Zoom appears in Browser Settings and persists normalized zoom values.
- New browser tabs apply the saved default zoom.
- Existing tests cover renderer zoom helpers, UI store persistence, main persistence, settings search, and runtime UI RPC.
- Typecheck, lint, focused tests, and Electron validation pass locally.

Summary
Browser tabs now remember one app-wide default zoom, so users who need larger page text can set it once and have future local browser tabs open at that zoom across repos and worktrees. Browser Settings exposes the default directly, and existing browser zoom shortcuts update the same persisted preference while Reset returns future tabs to 100%.
The preference is normalized to Electron-supported zoom steps at the renderer store, main persistence, and runtime UI RPC boundaries. Existing open tabs keep their current zoom so changing the default does not unexpectedly resize pages the user is already reading or editing.
Closes #4752
Verification
pnpm vitest run --config config/vitest.config.ts src/renderer/src/components/browser-pane/browser-page-zoom.test.ts src/renderer/src/components/settings/browser-search.test.ts src/renderer/src/store/slices/ui.test.ts src/main/persistence.test.ts src/main/runtime/rpc/methods/client-ui.test.tspnpm run typecheck:nodepnpm run typecheck:webpnpm run lint(passes with existing unrelated hook warnings inChecksPanel,Settings, andStatusBar)Summary by CodeRabbit