Skip to content

feat(browser): persist default browser zoom#4765

Merged
Jinwoo-H merged 1 commit into
stablyai:mainfrom
tmchow:tmchow/browser-default-zoom
Jun 7, 2026
Merged

feat(browser): persist default browser zoom#4765
Jinwoo-H merged 1 commit into
stablyai:mainfrom
tmchow:tmchow/browser-default-zoom

Conversation

@tmchow
Copy link
Copy Markdown
Contributor

@tmchow tmchow commented Jun 7, 2026

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.ts
  • pnpm run typecheck:node
  • pnpm run typecheck:web
  • pnpm run lint (passes with existing unrelated hook warnings in ChecksPanel, Settings, and StatusBar)

Compound Engineering
GPT-5

Summary by CodeRabbit

  • New Features
    • Added a customizable default browser zoom level in settings that persists across sessions.
    • Browser tabs now initialize with your saved default zoom instead of a fixed base level.
    • Zoom levels are automatically normalized to supported browser zoom steps for consistency.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 7, 2026

Review Change Stack

Walkthrough

This 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.

Changes

Default browser zoom persistence

Layer / File(s) Summary
Zoom constants and utilities
src/shared/browser-page-zoom.ts
Defines BrowserPageZoomDirection type, step/min/max bounds, immutable BROWSER_PAGE_ZOOM_LEVELS array, and four utility functions: normalizeBrowserPageZoomLevel() (validates/clamps to step), browserPageZoomLevelToPercent() (converts to integer percent via exponential), nextBrowserPageZoomLevel() (increments/resets with normalization).
Shared types and defaults
src/shared/types.ts, src/shared/constants.ts
PersistedUIState adds optional browserDefaultZoomLevel?: number; getDefaultUIState() initializes the field from DEFAULT_BROWSER_PAGE_ZOOM_LEVEL.
Main process RPC and persistence
src/main/runtime/rpc/methods/client-ui.ts, src/main/persistence.ts, src/main/persistence.test.ts, src/main/runtime/rpc/methods/client-ui.test.ts
UiUpdate schema extended with optional browserDefaultZoomLevel field; Store.getUI() and Store.updateUI() normalize the field on read/write; RPC test and persistence test validate round-trip normalization (e.g., 1.261.5).
Renderer zoom helper refactor and tests
src/renderer/src/components/browser-pane/browser-page-zoom.ts, src/renderer/src/components/browser-pane/browser-page-zoom.test.ts
Refactors to re-export zoom math from shared module; applyBrowserPageZoom() now accepts optional resetLevel parameter; new setBrowserPageZoomLevel() helper normalizes and applies explicit zoom to webview; tests cover normalization, reset behavior, and setter operation.
Renderer store state and hydration
src/renderer/src/store/slices/ui.ts, src/renderer/src/store/slices/ui.test.ts
UISlice adds browserDefaultZoomLevel state and setBrowserDefaultZoomLevel() setter; hydration normalizes persisted value; setter normalizes input before persisting via window.api.ui.set(); tests verify normalization during hydration and persistence.
BrowserPane zoom behavior
src/renderer/src/components/browser-pane/BrowserPane.tsx
Reads browserDefaultZoomLevel from store, normalizes and converts to percent for initial state; applies normalized default to webview on dom-ready via setBrowserPageZoomLevel(); persists runtime zoom changes back to store; updates zoom-indicator visibility to compare against persisted default rather than hardcoded 100.
Settings UI and search
src/renderer/src/components/settings/BrowserDefaultZoomSetting.tsx, src/renderer/src/components/settings/BrowserPane.tsx, src/renderer/src/components/settings/browser-search.ts, src/renderer/src/components/settings/browser-search.test.ts
New BrowserDefaultZoomSetting component renders a normalized zoom level in a controlled select dropdown with options from BROWSER_PAGE_ZOOM_LEVELS; integrated into settings pane with store wiring; search entry added with zoom keyword; search indices updated for new entry.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The pull request comprehensively implements the requested feature to set a default browser zoom that applies to newly opened tabs, with proper normalization at multiple boundaries.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the default browser zoom feature; no unrelated modifications were introduced.

✏️ 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ts renderer module — re-exports shared functions, keeps webview-specific applyBrowserPageZoom (with optional resetLevel) and new setBrowserPageZoomLevel.
  • Add browserDefaultZoomLevel to store slice — persisted state with normalization at hydration, initial value, and setter boundaries.
  • Normalize in PersistencebrowserDefaultZoomLevel is clamped to Electron-supported steps on read and write.
  • Accept browserDefaultZoomLevel in RPC ui.set — validates as z.number().finite().optional().
  • New BrowserDefaultZoomSetting component — a select dropdown listing all supported zoom levels by percentage.
  • Wire into BrowserPane settings and browser-search — setting is searchable; insertion shifts existing search-entry indices.
  • Apply default zoom to new browser tabsbrowserZoomPercent initializes from default; new/reparented webviews receive default zoom on dom-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 browserDefaultZoomPercent instead 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

Pullfrog  | Fix it ➔View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/main/persistence.test.ts (1)

2862-2869: ⚡ Quick win

Consider 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8a6b8eb and 382c3a8.

📒 Files selected for processing (16)
  • src/main/persistence.test.ts
  • src/main/persistence.ts
  • src/main/runtime/rpc/methods/client-ui.test.ts
  • src/main/runtime/rpc/methods/client-ui.ts
  • src/renderer/src/components/browser-pane/BrowserPane.tsx
  • src/renderer/src/components/browser-pane/browser-page-zoom.test.ts
  • src/renderer/src/components/browser-pane/browser-page-zoom.ts
  • src/renderer/src/components/settings/BrowserDefaultZoomSetting.tsx
  • src/renderer/src/components/settings/BrowserPane.tsx
  • src/renderer/src/components/settings/browser-search.test.ts
  • src/renderer/src/components/settings/browser-search.ts
  • src/renderer/src/store/slices/ui.test.ts
  • src/renderer/src/store/slices/ui.ts
  • src/shared/browser-page-zoom.ts
  • src/shared/constants.ts
  • src/shared/types.ts

Comment on lines 2862 to +2869

it('normalizes default browser zoom UI writes', async () => {
const store = await createStore()

store.updateUI({ browserDefaultZoomLevel: 1.26 })

expect(store.getUI().browserDefaultZoomLevel).toBe(1.5)
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +4262 to +4263
const showBrowserZoomIndicator =
browserZoomFeedbackVisible || browserZoomPercent !== browserDefaultZoomPercent
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Copy link
Copy Markdown
Contributor

@Jinwoo-H Jinwoo-H left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Jinwoo-H Jinwoo-H merged commit 8aa5b53 into stablyai:main Jun 7, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Allow setting default browser zoom

2 participants