Skip to content

fix(dpi): per-monitor DPI awareness, and isolate --clean's WebView2 folder - #175

Merged
mortenaslo merged 1 commit into
mainfrom
fix/per-monitor-dpi
Nov 6, 2026
Merged

mortenaslo merged 1 commit into
mainfrom
fix/per-monitor-dpi

Conversation

@mortenaslo

Copy link
Copy Markdown
Contributor

Summary

Makes the app per-monitor DPI aware (v2), so it re-scales properly when moved between monitors, docked/undocked, or when the Windows scale changes. Gives --clean runs their own WebView2 data folder, and makes a WebView2 start failure fail visibly instead of leaving a pane spinning forever.

Why

The app had no manifest, so WPF ran system-DPI aware: each process fixed its scale to the primary monitor's DPI at the moment it started and kept it for life. Observed on a 3×100% setup:

  • The installed app had started at 150% and stayed there — Windows was bitmap-stretching the whole window.
  • A dev build started later came up at the correct 100%, but its terminals were 1.5× too large: WebView2 runs one browser process per user-data folder, shared by every app process using it, and the installed copy's process rendered the dev build's panes at its own 150%.

Fixing DPI exposed the next problem: a per-monitor build cannot attach to a system-DPI copy's browser process — EnsureCoreWebView2Async throws COMException 0x8007139F. That exception escaped a launch nobody awaited, so it reached crash.log only later from the finalizer as an unobserved TASK:, the pane spun forever, and a new session stayed saved in state.json with no sidebar row.

Implementation notes

  • app.manifest: PerMonitorV2, PerMonitor (+ true/pm fallback). WFO0003 is suppressed: it recommends WinForms' Application.SetHighDpiMode, which a WPF app never calls.
  • Terminal/WebView2DataFolder: normal runs keep %AppData%\CodeShellManager\WebView2 (renaming it would orphan every profile); --clean uses WebView2-clean.
  • LaunchSessionAsync catches any InitializeAsync failure and tears down like the PTY-failure path. New session → plain message ("close the other copy"); restore / wake / restart → session ends asleep in the sidebar. The restore loop batches 0x8007139F with the existing access-denied case.
  • Removed the dead per-session UserDataFolder in MainWindow — the explicit environment always won, and no such folder ever existed on disk.
  • Upgrade note: the first launch of this release fails per the above only if a pre-manifest copy is still running (portable + installed, say). Closing the other copy clears it.
  • CLAUDE.md: new "DPI awareness and the WebView2 data folder" section; --clean docs now say what it does not isolate (output.db, crash.log).

Test plan

  • DpiAndWebView2FolderTests, incl. that the built CodeShellManager.exe embeds PerMonitorV2 — verified the pre-change build does not.
  • Live: --clean dev build next to the 150%-stuck installed copy — terminal at true size, WebView2 up in 0.4s in its own folder.
  • Live: build forced onto the shared folder — 0x8007139F caught in 75ms, logged, message shown, no orphan pane or session.
  • Full suite on the combined merge with every other pending branch: 669 passing.
  • Manual: change the Windows display scale while the app is open — text re-scales crisply rather than blurring. (Couldn't test here: all three monitors are at 100%.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX

…older

The app had no manifest, so WPF ran system-DPI aware: each process fixed its
scale to the primary monitor's DPI at start and kept it for life. After a
monitor, dock or scale change Windows bitmap-stretched the window. Observed:
the installed app stuck at 150% on 100% monitors, and a dev build whose WPF
chrome was correct while its terminals - rendered by the installed copy's
shared WebView2 browser process - were 1.5x too large.

- app.manifest declares PerMonitorV2 (WFO0003 suppressed: it recommends the
  WinForms API, which a WPF app never calls).
- --clean uses its own WebView2-clean data folder. A per-monitor build cannot
  attach to a system-DPI copy's browser process: EnsureCoreWebView2Async fails
  with 0x8007139F.
- LaunchSessionAsync now catches a WebView2 start failure and tears down like
  the PTY-failure path. Before, the exception escaped an unawaited launch: the
  pane spun forever and a new session stayed in SessionManager with no row.
  0x8007139F gets a plain "close the other copy" message, and restore batches
  it with the access-denied case, leaving those sessions asleep in the sidebar.
- Removed the dead per-session UserDataFolder (never used; the explicit
  environment wins).

Tests: DpiAndWebView2FolderTests, including that the built exe embeds
PerMonitorV2 (the pre-change build does not).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX
@mortenaslo
mortenaslo merged commit c76bce1 into main Nov 6, 2026
1 check passed
mortenaslo added a commit that referenced this pull request Nov 6, 2026
… relaunch, worktree remove safety (#182)

* fix(explorer): open the right folder for forward-slash and comma paths

"Open in Explorer" ran explorer.exe with the folder as raw command-line text,
and Explorer silently opened Documents instead for two kinds of path —
measured by asking the Shell which folder each new Explorer window showed:

- forward slashes (C:/repos/wt-feat): the form git prints, and so the form a
  session opened through "New session in sibling worktree" — and now the
  Worktrees menu — stored as its WorkingFolder;
- a comma in the name: Explorer splits its arguments on commas, and quoting
  the path does not help (also measured).

Services/FolderOpener opens the folder itself through the shell (ShellExecute
"open" on the full Windows path), which opened the right folder for every case
tried: plain, spaces, comma, &, %, forward slashes, trailing separator, 8.3. It
refuses anything that is not an existing directory first — ShellExecute on a
FILE runs it, and a session's folder comes from state.json and imports — and
runs off the UI thread (the check is \wsl$ I/O for WSL). Used by the toolbar
📁, the sidebar menu and note search results.

LaunchSessionInSiblingWorktreeAsync now stores the Windows form of the git
path, so new worktree sessions don't carry forward slashes at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX

* fix: pre-release review — never delete a session on a failed relaunch; worktree remove safety

From two read-only reviews of everything since v0.9.0. Each finding was
reproduced before fixing.

Relaunch (restore, wake) — data loss since before v0.9.0:
- Both called LaunchSessionAsync with the default removeOnFailure: true, so a
  session whose launch was refused or whose PTY failed to start (WSL distro
  uninstalled, claude briefly off PATH) was DELETED, and the next save made it
  permanent. Wake's catch never ran: the PTY-failure path returns, not throws.
  Both now pass removeOnFailure: false and end in FallBackToDormantIfNotLive,
  which also covers every WebView2 failure on restore (#175 only covered the
  two batched ones). Verified before/after with a real state.json via
  CSM_STATE_PATH: old build — session gone from sidebar and file; fixed —
  kept, asleep.

Worktree remove (#180):
- Refused when another worktree lives inside the target. git's clean check
  skips ignored folders: with B in A's .gitignored .worktrees/ holding
  uncommitted work, `git worktree remove A` exited 0 and deleted B (reproduced
  with git 2.54). Checked against a list re-read at click time.
- Sleeping sessions are deleted only after git removed the folder — they never
  go through Recently closed, so a refusal used to lose them for good.
- WSL: \wsl.localhost folded into \wsl$ (otherwise no match at all), and
  below the distro paths compare case-sensitively (/home/u/WT is not .../wt).

Not fixed here (follow-up issue): window bounds restore under per-monitor DPI
on mixed-DPI setups, the busy-slot key before RepoRoot resolves, and
ParseStatusV2 showing every unmerged entry as "UU".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant