Skip to content

feat(desktop): remember page zoom and add a sidebar zoom control on macOS and Linux - #6335

Closed
sungyongcho wants to merge 3 commits into
lidge-jun:devfrom
sungyongcho:feat/desktop-zoom-persist
Closed

sungyongcho wants to merge 3 commits into
lidge-jun:devfrom
sungyongcho:feat/desktop-zoom-persist

Conversation

@sungyongcho

@sungyongcho sungyongcho commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The desktop window forgot its page zoom every time the app restarted, and had no visible zoom control. On macOS and Linux the level came from Tauri's zoom-hotkey polyfill (#5737), which keeps it in let zoomLevel = 1 inside the injected script. That value restarts at 1 on every page load, so the level was never saved, and after the hop from the bootstrap page to the dashboard the first shortcut jumped from the real level to 120%.

The dashboard now owns zoom on macOS and Linux:

  • gui/src/lib/desktop-zoom.ts (new): the same keys as before (Cmd on macOS, Ctrl on Linux, with +, -, 0, plus Ctrl + wheel), 50%-300% in 10% steps on whole percents, the remembered level in localStorage, and the one call to plugin:webview|set_webview_zoom that capabilities/dashboard-zoom.json already grants to the loopback dashboard. No new IPC permission is added.
  • gui/src/use-desktop-zoom.ts (new): applies the remembered level when the dashboard starts, so a restart or a page navigation cannot leave the webview at a level the dashboard does not know, then follows the shortcuts and the control and saves each change. Touchpad pinch deltas accumulate before stepping instead of stepping on every event.
  • gui/src/components/desktop-zoom-control.tsx (new) and App.tsx: a - 130% + row in the sidebar footer beside the theme and language rows, shown only in the desktop shell on macOS and Linux. Clicking the percentage resets to 100%. Strings are in all ten locale files (French keeps "Zoom", added to the intentional-English list in fr-localization.test.ts).
  • No change under desktop/. The shell keeps Tauri's zoom hotkeys on (an earlier revision of this PR switched them off on macOS and Linux; the review pointed out why that is wrong, see below). On Windows WebView2 keeps its native zoom, which is not remembered, and the sidebar control is hidden there.
  • Shell and dashboard versions can differ. The runtime serves the dashboard and the installed app is the shell, so a declined takeover (the app attaches to an older runtime) or a service that has not restarted after an app update puts them on different versions. The dashboard's listeners therefore run in the capture phase and call stopImmediatePropagation(): Tauri's polyfill listens on window in the bubble phase, so a dashboard that handles zoom is the only writer under any shell version, an older dashboard has no handler and the polyfill keeps working, and an older shell's polyfill is pre-empted by a newer dashboard. No handshake is involved. The legacy mousewheel event the polyfill uses is silenced as well and not counted, so one gesture is one step. Alt chords are handled like the polyfill handled them, so they cannot sneak a second write past the dashboard. If maintainers prefer an explicit capability handshake (for example a user-agent token the shell announces), that is a small change on top.
  • gui/src/styles/sidebar-zoom.css (new, imported from main.tsx; it is kept out of styles.css, which is under a file-size cap that only moves down): the control's styles, and on the desktop layout (761px and up) .sidebar nav is now the scrolling region, so the footer stays on screen. Without it the new control was unreachable exactly when it is needed: page zoom shrinks the CSS viewport (1100x720 becomes 846x554 at 130%), the sidebar is a fixed-height column that does not scroll, and the language, theme, zoom and proxy rows were pushed out of the window (screenshot 1). The narrow layout is untouched because its drawer already scrolls as a whole. The padding: 4px; margin: -4px pair stops the scroll box from clipping focus rings and moves nothing. gui/design-system/components.md records the contract.
  • structure/desktop-shell.md and a Zoom section in docs-site/.../guides/desktop-app.md describe the new ownership and what an app attached to an older proxy does (earlier behaviour: 20% steps, not remembered, no sidebar control).

Screenshots of the real dashboard at the app's default window size, in both themes, at 100% and 130%, plus the clipped footer before the change, are attached below.

Verification

  • cd gui && bun run build, bun run lint and bun run lint:i18n: pass.
  • New gui/tests/desktop-zoom.test.ts (16 tests: steps and clamping, storage failures, key bindings per host, Alt chords, wheel accumulation) and gui/tests/desktop-zoom-dom.test.tsx (13 tests against a stand-in shell that records its commands: the remembered level is applied on start, Ctrl plus moves and saves it, Ctrl zero resets, the buttons step, the ceiling disables plus, unmanaged hosts are left alone, and six version-skew cases). The skew cases register a stand-in for the polyfill before the dashboard renders, the way the shell's injected script is registered, then check that a newer dashboard is the only writer for keys, Alt chords and Ctrl + wheel, that an older dashboard that does not manage zoom leaves the polyfill working, and that non-zoom keys and a plain wheel turn still reach every listener. Switching the listeners back to the bubble phase, and removing the propagation stop, each made three of them fail; both were restored. (My first version of these tests registered the stand-in after the dashboard, which let the bubble phase pass by accident; the registration order is what the tests now pin.)
  • Everything below was rerun on the current head, rebased onto dev at f86ad0a.
  • The whole GUI suite the way CI runs it, cd gui && bun test --isolate tests: 2747 pass, 0 fail across 314 files. While developing, the first DOM test left localStorage non-writable for later files in a shared process and broke 14 of them; it now keeps its overrides writable.
  • bun run structure:check and bun run privacy:scan: pass. desktop/ is identical to dev, so there is no Rust to format or compile.
  • npx react-doctor@0.9.11 --verbose --scope changed (the version the repo's gui script pins) against dev: 19 files scanned, no issues. The CI run of the same check is waiting for a maintainer to approve workflows for a first-time contributor, so this local run is the only evidence for it so far.
  • Root: bun run typecheck passes, and so do the two extra tsc runs in the CI Typecheck step (tests/tsconfig.doctor-service-memory-contract.json and scripts/ci/docker-smoke.ts). The size ratchet caught gui/src/styles.css growing past its cap, which is why the styles moved to a new file.
  • Full root suite, bun run test: 34936 pass, 77 skip, 93 fail, all 93 in 17 files this PR does not touch (Lab, Kiro catalogue, serving runtimes, packaged startup probe, and a few CLI and Codex integration files). Rerunning those 17 files at the dev tip without this PR gives the same 93 failures in the same files, so they are not caused by this change. My machine has no standalone Bun, so the suite runs through the packaged ocx binary in Bun mode, which at least explains the packaged-probe failure (Script not found "cached"); I did not chase the rest.
  • Not verified: the behaviour inside a running packaged app, and how WebKitGTK orders wheel against the legacy mousewheel event on real hardware (the code silences both and counts only wheel, so it does not depend on the order, but I could only exercise it in happy-dom, whose WheelEvent drops the modifier flags, so the tests set ctrlKey on the instance). The screenshots are the built dashboard opened in headless Chrome with the desktop shell's user agent and a stand-in for the shell's zoom command; page zoom was reproduced by shrinking the CSS viewport to 1100/z by 720/z with a device scale factor of z, which is what the webview does. They are not the packaged app. Windows is untouched.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults. The only IPC reach is still the existing zoom setter for the main window on the loopback origin.

Screenshots

The five screenshots are added to this description in the web editor, in this order:
1-before-dark-130-footer-clipped
2-after-dark-100
3-after-dark-130
4-after-light-100
5-after-light-130

  1. 1-before-dark-130-footer-clipped.png: 130% before the sidebar change. Only "English" is left of the footer; theme, zoom, proxy and GitHub are cut off.
  2. 2-after-dark-100.png: 100%, dark. The zoom row sits between the theme and proxy rows.
  3. 3-after-dark-130.png: 130%, dark. The menu scrolls and the whole footer, including - 130% +, stays visible.
  4. 4-after-light-100.png: 100%, light.
  5. 5-after-light-130.png: 130%, light.

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • Required local validation passed; commands, results, and any full-suite exception are documented.

  • I pushed my PR to a recent dev commit (at most 10 behind; a maintainer may still ask for the exact tip before merge).

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

Summary by CodeRabbit

  • New Features
    • Desktop users on macOS and Linux can adjust zoom from 50% to 300% in 10% increments using sidebar controls, keyboard shortcuts, or Ctrl+mouse wheel. Zoom settings persist across launches.
    • Zoom controls are available in multiple languages, and the desktop guide explains platform and older-version differences.
  • Improvements
    • The desktop sidebar navigation can scroll while keeping its footer controls accessible at reduced window heights or page zoom levels.

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (2)
gui/AGENTS.md — auto-discovered
structure/AGENTS.md — auto-discovered

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: lidge-jun/opencodex/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 76706a6d-52fb-4537-9a62-6c53211df49c

📥 Commits

Reviewing files that changed from the base of the PR and between f86ad0a and 3e62096.

📒 Files selected for processing (23)
  • docs-site/src/content/docs/guides/desktop-app.md
  • gui/design-system/components.md
  • gui/src/App.tsx
  • gui/src/components/desktop-zoom-control.tsx
  • gui/src/i18n/de.ts
  • gui/src/i18n/en.ts
  • gui/src/i18n/fr.ts
  • gui/src/i18n/ja.ts
  • gui/src/i18n/ko.ts
  • gui/src/i18n/ru.ts
  • gui/src/i18n/tr.ts
  • gui/src/i18n/vi.ts
  • gui/src/i18n/zh-TW.ts
  • gui/src/i18n/zh.ts
  • gui/src/icons.tsx
  • gui/src/lib/desktop-zoom.ts
  • gui/src/main.tsx
  • gui/src/styles/sidebar-zoom.css
  • gui/src/use-desktop-zoom.ts
  • gui/tests/desktop-zoom-dom.test.tsx
  • gui/tests/desktop-zoom.test.ts
  • gui/tests/fr-localization.test.ts
  • structure/desktop-shell.md

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The dashboard adds persistent zoom management for macOS and Linux. It supports keyboard shortcuts, Ctrl+wheel, and sidebar controls. Windows continues to use browser-engine zoom. Tests and documentation cover the behavior and compatibility with older proxies.

Changes

Desktop zoom

Layer / File(s) Summary
Zoom state and event handling
gui/src/lib/desktop-zoom.ts, gui/src/use-desktop-zoom.ts
The zoom utilities set platform support, bounds, 10% steps, persistence, keyboard mappings, wheel accumulation, and Tauri webview updates. The hook applies and saves zoom when management is enabled, and captures supported keyboard and wheel events.
Sidebar controls and platform wiring
gui/src/App.tsx, gui/src/components/desktop-zoom-control.tsx, gui/src/icons.tsx, gui/src/i18n/*, gui/src/main.tsx, gui/src/styles/sidebar-zoom.css, gui/design-system/components.md, docs-site/src/content/docs/guides/desktop-app.md, structure/desktop-shell.md
App enables zoom management for supported desktop platforms and displays the translated sidebar control. Styles make sidebar navigation scrollable. Documentation describes platform behavior, compatibility, and sidebar layout.
Zoom behavior tests
gui/tests/desktop-zoom.test.ts, gui/tests/desktop-zoom-dom.test.tsx, gui/tests/fr-localization.test.ts
Tests cover zoom limits, storage, shortcuts, wheel gestures, sidebar actions, Tauri updates, and event handling with the shell polyfill. The French localization test allows the shared English zoom label.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant useDesktopZoom
  participant desktopZoom
  participant BrowserStorage
  participant TauriWebview
  App->>useDesktopZoom: initialize with managed status
  useDesktopZoom->>desktopZoom: readSavedZoom
  desktopZoom->>BrowserStorage: read ocx-desktop-zoom
  useDesktopZoom->>desktopZoom: applyWebviewZoom
  desktopZoom->>TauriWebview: invoke set_webview_zoom
  useDesktopZoom->>desktopZoom: writeSavedZoom after zoom change
  desktopZoom->>BrowserStorage: save ocx-desktop-zoom
Loading

Merge Risk: 🔵 Low · up to 3e620

This adds remembered zoom and a sidebar zoom control on macOS and Linux. No concrete defects were found. Real-hardware zoom event ordering in a packaged app is unverified, so a quick manual check before release is advisable.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 3e620

The change is limited to desktop presentation settings and uses the existing zoom permission. No introduced security vulnerability was established. Risk remains low rather than minimal because packaged-runtime permission enforcement has not been verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The inspected new flow affects local webview presentation and an origin-local zoom preference. It submits only a zoom value, with no target label or additional privileged operation. No expanded tenant, credential, filesystem, or deployment authority was established for this flow.

Trust Boundaries and Controls

  • observed — Page-controlled preference and event input crosses into native zoom through the Tauri invocation API. The normal dashboard path bounds the preference and step values; the capability declares command and window scope. The platform gate controls ownership, not authentication or authorization.
  • observed — Source tests pin the declared window, URL pattern, and zoom permission, and exercise rejection of several nonmatching origins. They do not establish packaged-runtime enforcement. The remaining origin-validation and enforcement proof gaps are uncertainty, not verified bypasses.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 19 files. (4 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main changes: remembered page zoom and a sidebar zoom control for macOS and Linux desktop apps.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 19 files. (4 skipped: 4 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the enhancement New feature or request label Sep 30, 2026
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed; the review readiness checklist is complete.

Review readiness checklist

  • ✅ Required local validation passed; commands, results, and any full-suite exception are documented.
  • ✅ I pushed my PR to a recent dev commit (at most 10 behind; a maintainer may still ask for the exact tip before merge).
  • ✅ I resolved all correct Codex and CodeRabbit findings.
  • ✅ My PR is ready for review.

✅ 4/4 boxes ticked.

This pull request is already Ready for Review.
The review-ready label marks this PR as ready; review automation runs independently.
Maintainers: @lidge-jun @Ingwannu

Hygiene

✅ Deterministic PR hygiene checks passed.

@lidge-jun

Copy link
Copy Markdown
Owner

Thanks for this. Maintainer review found a version-skew problem, so this stays open until zoom ownership is negotiated:

  • desktop/src-tauri/src/lib.rs:326 turns off the macOS/Linux shell zoom hotkeys on the assumption that the served dashboard handles zoom. But startup can attach to an older runtime when takeover is declined (startup.rs:937), and that older dashboard has no zoom handler, so keyboard zoom stops working.
  • The reverse also happens: an older shell can attach to a newer runtime (startup.rs:1153). gui/src/App.tsx:222 enables the dashboard handler regardless of the shell version while the older shell still injects its own, so both write zoom and the saved/displayed percentage can disagree.

A small capability handshake (shell announces it delegates zoom; dashboard only takes over when it sees that) plus version-skew regression tests would resolve both. No security concerns found.

@sungyongcho
sungyongcho force-pushed the feat/desktop-zoom-persist branch from 0e362bd to c3e5eae Compare October 1, 2026 12:20
@sungyongcho

sungyongcho commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks, both cases are real, and I had the premise wrong. The dashboard comes from the runtime and the shell is the installed app, so they can differ: a declined takeover attaches to an older runtime, and an app update leaves the shell newer than a service that has not restarted yet.

Fixed in 3e62096 (rebased onto the current dev). I went with capture-phase handling rather than a handshake, because it needs no protocol between the shell and the dashboard and leaves desktop/ untouched:

  • The shell keeps Tauri's zoom hotkeys on again. desktop/ is now identical to dev, so this PR no longer touches Rust (the lib.rs:326 line is gone).
  • The dashboard's zoom handlers run in the capture phase and call stopImmediatePropagation(). Tauri's polyfill listens on window in the bubble phase, so the dashboard runs first. A newer dashboard is then the only writer under any shell version, an older dashboard has no handler so the polyfill keeps working, and an older shell's polyfill is pre-empted by a newer dashboard. No handshake is needed for that.
  • The polyfill's legacy mousewheel event is silenced and not counted, so one gesture is one step. Alt chords are handled the way the polyfill handled them, so they cannot sneak a second write past the dashboard.
  • Six version-skew tests register a stand-in for the polyfill before the dashboard renders, the way the shell's injected script is registered. Reverting the listeners to the bubble phase, or dropping the propagation stop, fails three of them each (I checked both). My first version of these tests registered the stand-in after the dashboard and passed by accident, so the registration order is what they pin now.
  • The branch is rebased onto current dev (it was 11 behind). The GUI suite passes in memory-capped shards (2732 passed, 0 failed) and React Doctor is clean.

I am not attached to this approach. If you would prefer an explicit capability token from the shell (for example in the user agent), tell me and I will switch to that.

Not verified: a packaged app, and the order in which real WebKitGTK fires wheel and mousewheel. The code silences both and counts only wheel, so it does not depend on the order, but I could only exercise it in happy-dom.

@sungyongcho
sungyongcho force-pushed the feat/desktop-zoom-persist branch from c3e5eae to 3e62096 Compare October 1, 2026 20:41
@github-actions
github-actions Bot marked this pull request as ready for review October 1, 2026 20:42
robin-bially pushed a commit to robin-bially/opencodex that referenced this pull request Oct 3, 2026
)

Desktop page zoom reset across launches and clipped sidebar footer controls.
Carry persisted macOS/Linux zoom and capture-phase events that preempt older shell hotkeys.
Keep native shell defaults, Windows behavior, localization, and the existing IPC permission unchanged.

Carries lidge-jun#6335 by @sungyongcho.
Co-authored-by: sungyongcho <46742040+sungyongcho@users.noreply.github.com>
@lidge-jun

Copy link
Copy Markdown
Owner

Superseded by the integration in #6487, with reviewed follow-up fixes in #6490 and Windows validation repairs in #6494/#6495, all merged into dev.

Desktop zoom persistence and sidebar controls were carried, including the capture-phase behavior and locale coverage. Packaged-user acceptance limits remain documented separately.

Original carry commit: c294e5811999551272cf2196301cfe8b68fcc325. Attribution to @sungyongcho is preserved in the integration history and merge trailers. The final integrated candidate passed the complete cross-platform CI run.

Closing this PR as superseded, not claiming that its original head was merged. Thank you for the contribution.

@lidge-jun lidge-jun closed this Oct 3, 2026
@sungyongcho
sungyongcho deleted the feat/desktop-zoom-persist branch October 3, 2026 13:40
@lidge-jun lidge-jun mentioned this pull request Oct 4, 2026
3 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants