@@ -416,8 +416,21 @@ export class WindowHelper {
416416 // EDGE CASE: if the grown window would overflow the work area's right edge,
417417 // the X clamp below shifts the window left — that one case can show a
418418 // one-frame shift, same as any clamped move always could.
419- public setOverlayDimensionsAnchored ( width : number , height : number ) : void {
420- if ( ! this . overlayWindow || this . overlayWindow . isDestroyed ( ) ) return ;
419+ //
420+ // RETURNS the size actually APPLIED after clamping. The renderer needs this:
421+ // it mirrors the same floor(workArea * 0.9) clamp locally, but the display it
422+ // measures (window.screen.availWidth) and the one this method measures
423+ // (the work area of the display the window sits on) can disagree — on a
424+ // multi-monitor setup they routinely do. Adopting the echoed value keeps the
425+ // renderer's panel width, the toggle anchor and the hover-gate margin locked
426+ // to the window that actually exists, instead of the one it asked for.
427+ public setOverlayDimensionsAnchored (
428+ width : number ,
429+ height : number ,
430+ ) : { width : number ; height : number } {
431+ if ( ! this . overlayWindow || this . overlayWindow . isDestroyed ( ) ) {
432+ return { width, height } ;
433+ }
421434
422435 const currentBounds = this . overlayWindow . getBounds ( ) ;
423436 const currentContentSize = this . overlayWindow . getContentSize ( ) ;
@@ -456,27 +469,33 @@ export class WindowHelper {
456469 currentContentSize,
457470 computed : { x : newX , y : newY , width : newWidth , height : newHeight } ,
458471 } ) ;
459- return ;
472+ return { width : currentContentSize [ 0 ] , height : currentContentSize [ 1 ] } ;
460473 }
461474
462475 // Atomic frame change: a single setBounds avoids the 1-frame split where
463476 // the OS window has the new size but the old origin (or vice versa), which
464477 // is what causes the shell to visibly slide and snap during code-expansion.
465478 this . overlayWindow . setBounds ( { x : newX , y : newY , width : newWidth , height : newHeight } ) ;
466479 this . overlayBounds = this . overlayWindow . getBounds ( ) ;
480+ const appliedContentSize = this . overlayWindow . getContentSize ( ) ;
467481 traceOverlayResize ( 'setOverlayDimensionsAnchored:applied' , {
468482 requested : { width, height } ,
469483 appliedBounds : this . overlayBounds ,
470- contentSizeAfter : this . overlayWindow . getContentSize ( ) ,
484+ contentSizeAfter : appliedContentSize ,
471485 } ) ;
486+ return { width : appliedContentSize [ 0 ] , height : appliedContentSize [ 1 ] } ;
472487 }
473488
474- // NOTE: the overlay window is a FIXED WIDTH (OVERLAY_DEFAULT_WIDTH = 732)
475- // for its entire visible lifetime; the renderer always reports that fixed
476- // width, so every report here is height-only (width delta 0) — top-anchored,
477- // X never moves, no width setBounds ever. The expand/contract animation is
478- // CSS-only in the renderer (panel tweens 600↔732 centered inside the fixed
479- // window). See NativelyInterface.startTransition for the renderer side.
489+ // NOTE: the overlay window's width is FIXED FOR THE WHOLE LIFETIME OF AN
490+ // ANIMATION. It is born at OVERLAY_DEFAULT_WIDTH (732) and only ever changes
491+ // when the USER drags a resize handle (or on restore of a previously dragged
492+ // size) — never during the expand/contract spring, which stays CSS-only in
493+ // the renderer (the panel tweens collapsed↔expanded centered inside the
494+ // window). So every report arriving here DURING an animation is still
495+ // height-only (width delta 0): top-anchored, X never moves, no width
496+ // setBounds. See NativelyInterface.startTransition for the renderer side and
497+ // src/lib/overlayCustomSize.mjs for why the invariant is per-animation
498+ // rather than per-lifetime.
480499
481500 public createWindow ( ) : void {
482501 if ( this . launcherWindow !== null ) return ; // Already created
@@ -1392,12 +1411,22 @@ export class WindowHelper {
13921411 this . repositionOverlayPopovers ( ) ;
13931412 }
13941413
1395- // The panel's live LEFT margin inside the fixed window: (732 - panelW)/2,
1396- // derived from the streamed togglePanelRight = (732 + panelW)/2. Popover
1414+ // The panel's live LEFT margin inside the window: (windowW - panelW)/2,
1415+ // derived from the streamed togglePanelRight = (windowW + panelW)/2. Popover
13971416 // anchors are stored relative to the panel, not the window, so they follow
13981417 // the symmetric width spring.
1418+ //
1419+ // Reads the window's LIVE width rather than OVERLAY_DEFAULT_WIDTH: the user
1420+ // can now resize the overlay, and the renderer streams togglePanelRight
1421+ // against whatever width the window actually has. Using the constant here
1422+ // while the renderer used the live width would offset every popover by
1423+ // (732 - actualWidth) / 2.
13991424 public getOverlayPanelLeftMargin ( ) : number {
1400- return Math . max ( 0 , WindowHelper . OVERLAY_DEFAULT_WIDTH - this . togglePanelRight ) ;
1425+ const windowWidth =
1426+ this . overlayWindow && ! this . overlayWindow . isDestroyed ( )
1427+ ? this . overlayWindow . getContentSize ( ) [ 0 ]
1428+ : WindowHelper . OVERLAY_DEFAULT_WIDTH ;
1429+ return Math . max ( 0 , windowWidth - this . togglePanelRight ) ;
14011430 }
14021431
14031432 // Re-anchor any open overlay popovers (settings / model-selector) to the
0 commit comments