[Bug]: Window snapped to a display's edge reopens on the primary display; saved bounds 1px past the edge are discarded#21563

opened8d agobyJjahands

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Related: #4841 (same symptom for a maximized window; the cause there is that mainWindowBounds holds the pre-maximize bounds). This window is not maximized.

Area

apps/desktop

Steps to reproduce

Displays: primary 1920x1280 at (0,0); secondary rotated portrait 1280x1920 at (1920,0).

  1. Snap the T3 Code window to the top half of the portrait display.
  2. Quit T3 Code and relaunch it.

Expected behavior

The window reopens on the top half of the portrait display, as other apps do (ChatGPT, for example).

Actual behavior

It opens at the default size on the primary display every time.

The snapped window is persisted as {"x":1921,"y":30,"width":1280,"height":945}, which extends 1px past the display's right edge (1921 + 1280 = 3201 > 3200). windowFitsWithinDisplay in apps/desktop/src/window/DesktopWindow.ts requires the bounds to lie entirely within one display, so resolveInitialMainWindowBounds discards them and logs saved main window bounds could not be restored; using defaults.

A second problem shows up when the main window is closed and re-created inside the same process: the saved bounds are {"x":1920,"y":30,"width":1280,"height":945}, which pass the check, but the next persisted bounds are {"x":640,"y":30,...}, centered horizontally on the primary display. The window still ends up on the primary display.

Impact

Minor bug or occasional failure

Version or commit

0.1.1-nightly.20261019.3348 (Electron 44.4.5)

Environment

macOS 26 (Darwin 25.5.0), Apple M4 Max, two BenQ displays at scale factor 2, one rotated 270°

Logs or stack traces

Exact lines from ~/.t3/userdata/logs/desktop.trace.ndjson.1, prefixed with their line numbers (rg -n). runId 731a412456ae is one app process; b9161434e657 is the next one after quitting and relaunching.

shell
116786:{"type":"effect-span","name":"desktop.window.createWindow","traceId":"f2cd7b17b6db5b7448aa19725d206e30","spanId":"42f2d94c26985991","parentSpanId":"1f8a728726a69c1b","sampled":true,"kind":"internal","startTimeUnixNano":"1792391261825385750","endTimeUnixNano":"1792391261870378125","durationMs":44.992375,"attributes":{},"events":[{"name":"saved main window bounds could not be restored; using defaults","timeUnixNano":"1792391261826968500","attributes":{"scope":"desktop","runId":"731a412456ae","component":"desktop-window","effect.fiberId":189,"effect.logLevel":"WARN"}}],"links":[],"exit":{"_tag":"Success"}}
217937:{"type":"effect-span","name":"desktop.settings.setMainWindowBounds","traceId":"ad7d86289246174a7be314db4c75b265","spanId":"53952931287083a1","sampled":true,"kind":"internal","startTimeUnixNano":"1792391510280173875","endTimeUnixNano":"1792391510280317750","durationMs":0.143875,"attributes":{"x":1920,"y":30,"width":1280,"height":945,"isMaximized":false},"events":[],"links":[],"exit":{"_tag":"Success"}}
317945:{"type":"effect-span","name":"desktop.window.createWindow","traceId":"f2cd7b17b6db5b7448aa19725d206e30","spanId":"e2d87627a101f5e9","parentSpanId":"81484b4af8cbac92","sampled":true,"kind":"internal","startTimeUnixNano":"1792391512696948292","endTimeUnixNano":"1792391512709797792","durationMs":12.8495,"attributes":{},"events":[],"links":[],"exit":{"_tag":"Success"}}
417946:{"type":"effect-span","name":"desktop.window.createMain","traceId":"f2cd7b17b6db5b7448aa19725d206e30","spanId":"81484b4af8cbac92","parentSpanId":"5b19d2200ef23cea","sampled":true,"kind":"internal","startTimeUnixNano":"1792391512696927667","endTimeUnixNano":"1792391512709909667","durationMs":12.982,"attributes":{},"events":[{"name":"main window created","timeUnixNano":"1792391512709905292","attributes":{"scope":"desktop","runId":"731a412456ae","component":"desktop-window","effect.fiberId":1472,"effect.logLevel":"INFO"}}],"links":[],"exit":{"_tag":"Success"}}
518102:{"type":"effect-span","name":"desktop.settings.setMainWindowBounds","traceId":"4da9ca699931f60f88719a8db78629fa","spanId":"0b05685b82dc209c","sampled":true,"kind":"internal","startTimeUnixNano":"1792391513286035000","endTimeUnixNano":"1792391513286800542","durationMs":0.765542,"attributes":{"x":640,"y":30,"width":1280,"height":945,"isMaximized":false},"events":[],"links":[],"exit":{"_tag":"Success"}}
618181:{"type":"effect-span","name":"desktop.settings.setMainWindowBounds","traceId":"53c6d2c0a4110e63af5bd097fd4ff93c","spanId":"19151d651fd6f0df","sampled":true,"kind":"internal","startTimeUnixNano":"1792391516482122333","endTimeUnixNano":"1792391516482940833","durationMs":0.8185,"attributes":{"x":1921,"y":30,"width":1280,"height":945,"isMaximized":false},"events":[],"links":[],"exit":{"_tag":"Success"}}
718588:{"type":"effect-span","name":"desktop.window.createWindow","traceId":"7d9bdbe056394fc8e7dcbb416dba24c4","spanId":"7d80b9997a4799f7","parentSpanId":"1cd61c3eba37028d","sampled":true,"kind":"internal","startTimeUnixNano":"1792391578900951250","endTimeUnixNano":"1792391578945087166","durationMs":44.135916,"attributes":{},"events":[{"name":"saved main window bounds could not be restored; using defaults","timeUnixNano":"1792391578902404875","attributes":{"scope":"desktop","runId":"b9161434e657","component":"desktop-window","effect.fiberId":189,"effect.logLevel":"WARN"}}],"links":[],"exit":{"_tag":"Success"}}

Workaround

Drag the window back after each launch.

Note

Grok responding on behalf of Julius.

The first part is confirmed on main and accepted as a bug. This is separate from #4841, which is about a maximized window saving its pre-maximize bounds.

windowFitsWithinDisplay (DesktopWindow.ts:140-151) requires the saved bounds to sit entirely inside one display, with no tolerance. resolveInitialMainWindowBounds (DesktopWindow.ts:164-176) falls back to the default size when no display passes. A snapped window that ends 1px past the edge is therefore thrown away.

The second part, a re-created window moving from x=1920 to x=640, is not explained by the app code. Bounds that pass the check go straight into the window constructor (DesktopWindow.ts:374-380), and nothing in DesktopWindow.ts or ElectronWindow.ts moves the window after that. It is not part of what is accepted here.

Open #13125 changes the same functions. It checks against each display's work area instead of its full bounds, so it does not add any tolerance for this case.

Synced from GitHub

@juliusmarminge Thanks. I replaced the log section in the issue body with the exact trace lines, with their line numbers and the file they came from (~/.t3/userdata/logs/desktop.trace.ndjson.1). The previous excerpt was a hand-written summary and named the wrong file.