diff --git a/linux-features/pet-overlay/README.md b/linux-features/pet-overlay/README.md index 399532e23..2501991f7 100644 --- a/linux-features/pet-overlay/README.md +++ b/linux-features/pet-overlay/README.md @@ -46,8 +46,13 @@ Feature settings can be overridden in the gitignored `features.json` file: ``` `lockPosition: false` preserves manual pet moves when the existing window -position is visible. Set it to `true` only when you want the mascot pinned to -the configured screen corner on every layout pass. +position is visible. In this default unlocked mode the overlay is frameless: +transparent space moves the native `356×320` window, while the mascot is a +`no-drag` region so its renderer gesture can reposition the mascot within the +view. Existing tray controls keep their own interactive regions. Set +`lockPosition` to `true` only when you want the mascot pinned to the configured +screen corner on every layout pass; that mode removes the full-surface +drag/input opt-in. ## Options @@ -164,6 +169,10 @@ node --test linux-features/pet-overlay/test.js For a manual check, enable the feature, rebuild, and launch the app: - The pet overlay should remain transparent. +- With `lockPosition: false`, it should be frameless; drag transparent space + to move the window, and drag the mascot to reposition it inside the view. + Tray controls should remain clickable, and the window position lasts only + for the running session. - Selecting a different pet should update the open overlay without restarting Codex. - On Hyprland, the pet should have no visible compositor border or shadow. - On Niri, the pet should open floating, avoid initial focus, and move by diff --git a/linux-features/pet-overlay/patch.js b/linux-features/pet-overlay/patch.js index adb0d5606..301e7fb15 100644 --- a/linux-features/pet-overlay/patch.js +++ b/linux-features/pet-overlay/patch.js @@ -2,7 +2,6 @@ const VALID_GRAVITIES = new Set(["bottom-right", "bottom-left", "top-right", "top-left"]); const DESCRIPTOR_ID = "pet-overlay-main"; -const POINTER_REGION_DESCRIPTOR_ID = "pet-overlay-pointer-region"; const AVATAR_SELECTION_REFRESH_MARKER = "codexPetOverlayRefreshAvatarWindows"; function findMatchingBrace(source, openIndex) { @@ -159,12 +158,17 @@ function buildPetOverlayMethods(settings) { "codexPetOverlayMascotRect(e){let t=e?.mascot;if(t==null)return null;let n=Number(t.left),r=Number(t.top),i=Number(t.width),a=Number(t.height);return[n,r,i,a].every(Number.isFinite)&&i>0&&a>0?{left:n,top:r,width:i,height:a}:null}", "codexPetOverlayLayoutAtWindowPosition(e,t){if(e==null||t==null||t.windowBounds==null)return t;let n={...t.windowBounds,x:Math.round(e.x),y:Math.round(e.y)},r=this.codexPetOverlayMascotRect(t),i=r==null?{x:n.x,y:n.y,width:t.anchor?.width??n.width,height:t.anchor?.height??n.height}:{x:n.x+r.left,y:n.y+r.top,width:r.width,height:r.height},a=t.anchor==null?t.anchor:{...t.anchor,x:Math.round(i.x),y:Math.round(i.y),width:t.anchor.width??i.width,height:t.anchor.height??i.height};return{...t,anchor:a,windowBounds:n}}", "codexPetOverlayGravityBounds(e,t,n){if(e==null||t==null||t.windowBounds==null)return null;let r={...t.windowBounds},i=this.codexPetOverlayMascotRect(t)??{left:0,top:0,width:Number(r.width),height:Number(r.height)},a=Number(i.left),o=Number(i.top),s=Number(i.width),c=Number(i.height);if(![a,o,s,c].every(Number.isFinite)||s<=0||c<=0)return null;let l=Math.max(0,Math.min(512,Number(n?.margin)||0)),u=String(n?.gravity??`bottom-right`);return r.x=u.endsWith(`left`)?Math.round(e.x+l-a):Math.round(e.x+e.width-l-a-s),r.y=u.startsWith(`top`)?Math.round(e.y+l-o):Math.round(e.y+e.height-l-o-c),r}", - "codexPetOverlayTrayAboveLeft(e){if(process.platform!==`linux`||e==null||e.windowBounds==null||e.mascot==null||e.tray==null)return e;let t=Number(e.windowBounds.width),n=Number(e.windowBounds.height),r=Number(e.mascot.width),i=Number(e.mascot.height),a=Number(e.tray.width),o=Number(e.tray.height);if(![t,n,r,i,a,o].every(Number.isFinite)||t<=0||n<=0||r<=0||i<=0||a<=0||o<=0)return e;let s=Math.max(0,Math.round(t-r)),c=Math.max(0,Math.round(n-i)),l=Math.max(0,Math.min(Math.round(t-a),Math.round(s+r-a))),u=Math.max(0,Math.min(Math.round(n-o),Math.round(c-o-4))),d=e.anchor??{x:Number(e.windowBounds.x)+(Number(e.mascot.left)||0),y:Number(e.windowBounds.y)+(Number(e.mascot.top)||0),width:r,height:i},p={...e.windowBounds,x:Math.round(Number(d.x)-s),y:Math.round(Number(d.y)-c)},h={...d,x:Math.round(Number(d.x)),y:Math.round(Number(d.y)),width:d.width??r,height:d.height??i};return{...e,anchor:h,mascot:{...e.mascot,left:s,top:c,width:r,height:i},tray:{...e.tray,left:l,top:u,width:a,height:o},placement:`top-end`,windowBounds:p}}", + "codexPetOverlayTrayAboveLeft(e){if(process.platform!==`linux`||e==null||e.windowBounds==null||e.mascot==null||e.tray==null)return e;let t=Number(e.windowBounds.width),n=Number(e.windowBounds.height),r=Number(e.mascot.width),i=Number(e.mascot.height),a=Number(e.tray.width),o=Number(e.tray.height);if(![t,n,r,i,a,o].every(Number.isFinite)||t<=0||n<=0||r<=0||i<=0||a<=0||o<=0)return e;let s=4,c=Math.max(0,Math.min(Math.round(t),Math.round(a))),l=Math.max(0,Math.min(Math.max(0,Math.round(n-i-s)),Math.round(o))),u=this.codexPetOverlayMascotLocalPosition,d=Number(u?.left),p=Number(u?.top),h=[d,p].every(Number.isFinite),m=Math.max(0,Math.min(Math.round(t-r),Math.round(h?d:t-r))),f=Math.max(0,Math.min(Math.round(n-i),Math.round(h?p:n-i))),g=Math.max(0,Math.round(n-i-s-l)),v=Math.max(0,Math.min(Math.round(n-i),Math.round(l+s)));if(gg&&f=l+s?f-l-s:f+i+s;x=Math.max(0,Math.min(Math.round(n-l),Math.round(x)));let y=e.anchor??{x:Number(e.windowBounds.x)+(Number(e.mascot.left)||0),y:Number(e.windowBounds.y)+(Number(e.mascot.top)||0),width:r,height:i},b=h?{...e.windowBounds}:{...e.windowBounds,x:Math.round(Number(y.x)-m),y:Math.round(Number(y.y)-f)},k={...y,x:Math.round(Number(b.x)+m),y:Math.round(Number(b.y)+f),width:y.width??r,height:y.height??i};return{...e,anchor:k,mascot:{...e.mascot,left:m,top:f,width:r,height:i},tray:{...e.tray,left:w,top:x,width:c,height:l},placement:`top-end`,windowBounds:b}}", + "codexPetOverlayDragFromCurrentRenderer(e){let t=this.window;return!(t==null||t.isDestroyed?.()||t.webContents?.id!==e)}", + "codexPetOverlayStartLocalMascotDrag(e,t){if(!this.codexPetOverlayDragFromCurrentRenderer(e)||process.platform!==`linux`||this.codexPetOverlayShouldLockPosition())return!1;let n=this.layout,r=this.codexPetOverlayMascotRect(n),i=Number(t?.pointerWindowX),a=Number(t?.pointerWindowY);if(r==null||![i,a].every(Number.isFinite)||ir.left+r.width||a>r.top+r.height)return!1;return this.codexPetOverlayMascotDragState={offsetX:i-r.left,offsetY:a-r.top},!0}", + "codexPetOverlayMoveLocalMascotDrag(e,t){if(!this.codexPetOverlayDragFromCurrentRenderer(e))return!1;let n=this.codexPetOverlayMascotDragState,r=this.layout,i=this.window,a=this.codexPetOverlayMascotRect(r),o=this.codexPetOverlayWindowBounds(i);if(n==null||r==null||a==null||o==null)return!1;let s=Number(t?.pointerWindowX),c=Number(t?.pointerWindowY);if(![s,c].every(Number.isFinite)){let e=Number(t?.pointerScreenX),r=Number(t?.pointerScreenY);if(![e,r].every(Number.isFinite))return!0;s=e-o.x,c=r-o.y}let l=Math.max(0,Math.min(Math.round(o.width-a.width),Math.round(s-n.offsetX))),u=Math.max(0,Math.min(Math.round(o.height-a.height),Math.round(c-n.offsetY)));this.codexPetOverlayMascotLocalPosition={left:l,top:u};let d=this.codexPetOverlayTrayAboveLeft({...r,windowBounds:{...r.windowBounds,x:o.x,y:o.y,width:o.width,height:o.height}});this.layout=d;try{this.compositionHost.updateMascotRect?.(d.mascot)}catch{}try{this.sendLayoutToRenderer(i,null)}catch{}return!0}", + "codexPetOverlayEndLocalMascotDrag(e){return!this.codexPetOverlayDragFromCurrentRenderer(e)||this.codexPetOverlayMascotDragState==null?!1:(this.codexPetOverlayMascotDragState=null,!0)}", "codexPetOverlayRememberLayout(e,t){let n=this.codexPetOverlayRect(t);this.codexPetOverlayDesiredDisplayBounds=n==null?null:{x:Math.round(n.x),y:Math.round(n.y),width:Math.round(n.width),height:Math.round(n.height)};let r=this.codexPetOverlayRect(e?.windowBounds);if(r!=null){let i={x:Math.round(r.x),y:Math.round(r.y),width:Math.round(r.width),height:Math.round(r.height)},a=this.codexPetOverlayDesiredWindowBounds,o=a==null||a.x!==i.x||a.y!==i.y||a.width!==i.width||a.height!==i.height;this.codexPetOverlayDesiredWindowBounds=i;if(o&&this.window!=null){let e=this.codexPetOverlaySettings();try{e.lockPosition===!0&&this.codexPetOverlayScheduleHyprlandHints(this.window)}catch{}try{this.dragState!=null?this.codexPetOverlayQueueKWinDrag(this.window):this.codexPetOverlayScheduleKWinHints(this.window)}catch{}try{this.dragState!=null?this.codexPetOverlayQueueNiriDrag(this.window):this.codexPetOverlayScheduleNiriHints(this.window)}catch{}}}return e}", "codexPetOverlayLayoutForDisplay(e,t,n){if(process.platform!==`linux`||t==null||t.windowBounds==null)return this.codexPetOverlayRememberLayout(this.codexPetOverlayTrayAboveLeft(t));let r=this.codexPetOverlayDisplayRect(e),i=this.codexPetOverlaySettings(),a=t;if(i.lockPosition===!0&&r!=null){let e=this.codexPetOverlayGravityBounds(r,a,i);e!=null&&(a=this.codexPetOverlayLayoutAtWindowPosition(e,a))}else if(this.dragState==null){let e=this.codexPetOverlayWindowBounds(n),o=!1;try{o=n?.isVisible?.()===!0}catch{}e!=null&&r!=null&&this.codexPetOverlayBoundsNearDisplay(e,r)&&(o||this.codexPetOverlayInitialPositionDone===!0||this.codexPetOverlayManualPosition===!0)?(this.codexPetOverlayInitialPositionDone=!0,this.codexPetOverlayMoved(e,a.windowBounds)&&(this.codexPetOverlayManualPosition=!0),a=this.codexPetOverlayLayoutAtWindowPosition(e,a)):this.codexPetOverlayInitialPositionDone=!0}return this.codexPetOverlayRememberLayout(this.codexPetOverlayTrayAboveLeft(a),r)}", - "codexPetOverlayInstallTransparentRenderer(e){try{if(e.__codexPetOverlayTransparentRendererInstalled)return;e.__codexPetOverlayTransparentRendererInstalled=!0;let t=e.webContents,n=()=>{try{t==null||t.isDestroyed?.()||t.insertCSS?.(`html,body,#root,main,[data-avatar-overlay-content-frame=\"true\"]{background:transparent!important;background-color:transparent!important;}[data-codex-window-type=\"electron\"].electron-opaque,[data-codex-window-type=\"electron\"].electron-opaque body{background:transparent!important;background-color:transparent!important;background-image:none!important;}`,{cssOrigin:`author`}),t==null||t.isDestroyed?.()||t.executeJavaScript?.(`try{document.documentElement.style.background=\"transparent\";document.body&&(document.body.style.background=\"transparent\")}catch{}`,!0)}catch{}};t?.on?.(`did-finish-load`,n),n()}catch{}}", + "codexPetOverlayShouldUseWholeWindowInput(){return process.platform===`linux`&&this.codexPetOverlaySettings().lockPosition!==!0}", + "codexPetOverlayInstallTransparentRenderer(e){try{this.codexLinuxWholeWindowInput=this.codexPetOverlayShouldUseWholeWindowInput();if(e.__codexPetOverlayTransparentRendererInstalled)return;e.__codexPetOverlayTransparentRendererInstalled=!0;let t=e.webContents,n=()=>{try{let n=`html,body,#root,main,[data-avatar-overlay-content-frame=\"true\"]{background:transparent!important;background-color:transparent!important;}[data-codex-window-type=\"electron\"].electron-opaque,[data-codex-window-type=\"electron\"].electron-opaque body{background:transparent!important;background-color:transparent!important;background-image:none!important;}`;this.codexLinuxWholeWindowInput=this.codexPetOverlayShouldUseWholeWindowInput(),this.codexLinuxWholeWindowInput&&(n+=`html,body,#root,main,[data-avatar-overlay-content-frame=\"true\"]{-webkit-app-region:drag!important;app-region:drag!important;user-select:none!important;-webkit-user-select:none!important;}[data-avatar-overlay-hit-region=\"mascot\"],[data-avatar-mascot=\"true\"],.no-drag,[data-avatar-overlay-hit-region=\"notification-tray\"],[data-avatar-overlay-hit-region=\"notification-scroll-control\"]{-webkit-app-region:no-drag!important;app-region:no-drag!important;}`),t==null||t.isDestroyed?.()||t.insertCSS?.(n,{cssOrigin:`author`}),t==null||t.isDestroyed?.()||t.executeJavaScript?.(`try{document.documentElement.style.background=\"transparent\";document.body&&(document.body.style.background=\"transparent\")}catch{}`,!0)}catch{}};t?.on?.(`did-finish-load`,n),n()}catch{}}", "codexPetOverlayRestoreFocusableAfterInactiveShow(e){try{let t=setTimeout(()=>{try{e==null||e.isDestroyed?.()||this.window!==e||this.codexPetOverlaySettings().mode===`passive`||e.setFocusable?.(!0)}catch{}},0);try{t.unref?.()}catch{}}catch{}}", - "codexPetOverlaySyncWindow(e,t=!1){if(process.platform!==`linux`||e==null||e.isDestroyed?.())return;let n=this.codexPetOverlaySettings(),r=n.mode!==`passive`;try{e.setTitle?.(`Codex Pet Overlay`)}catch{}try{e.setFocusable?.(r&&!t)}catch{}try{t&&r&&this.codexPetOverlayRestoreFocusableAfterInactiveShow(e)}catch{}try{e.setSkipTaskbar?.(!!n.skipTaskbar)}catch{}try{e.setAlwaysOnTop?.(!!n.alwaysOnTop)}catch{}try{e.setBackgroundColor?.(`#00000000`)}catch{}try{this.codexPetOverlayInstallTransparentRenderer(e)}catch{}try{e.setOpacity?.(1)}catch{}try{e.setVisibleOnAllWorkspaces?.(!!n.allWorkspaces,{visibleOnFullScreen:!!n.allWorkspaces})}catch{try{e.setVisibleOnAllWorkspaces?.(!!n.allWorkspaces)}catch{}}try{n.alwaysOnTop&&e.moveTop?.()}catch{}try{this.codexPetOverlayScheduleHyprlandHints(e)}catch{}try{this.codexPetOverlayScheduleKWinHints(e)}catch{}try{this.codexPetOverlayScheduleNiriHints(e)}catch{}}", + "codexPetOverlaySyncWindow(e,t=!1){if(process.platform!==`linux`||e==null||e.isDestroyed?.())return;this.codexLinuxWholeWindowInput=this.codexPetOverlayShouldUseWholeWindowInput();let n=this.codexPetOverlaySettings(),r=n.mode!==`passive`;try{e.setTitle?.(`Codex Pet Overlay`)}catch{}try{e.setFocusable?.(r&&!t)}catch{}try{t&&r&&this.codexPetOverlayRestoreFocusableAfterInactiveShow(e)}catch{}try{e.setSkipTaskbar?.(!!n.skipTaskbar)}catch{}try{e.setAlwaysOnTop?.(!!n.alwaysOnTop)}catch{}try{e.setBackgroundColor?.(`#00000000`)}catch{}try{this.codexPetOverlayInstallTransparentRenderer(e)}catch{}try{e.setOpacity?.(1)}catch{}try{e.setVisibleOnAllWorkspaces?.(!!n.allWorkspaces,{visibleOnFullScreen:!!n.allWorkspaces})}catch{try{e.setVisibleOnAllWorkspaces?.(!!n.allWorkspaces)}catch{}}try{n.alwaysOnTop&&e.moveTop?.()}catch{}try{this.codexPetOverlayScheduleHyprlandHints(e)}catch{}try{this.codexPetOverlayScheduleKWinHints(e)}catch{}try{this.codexPetOverlayScheduleNiriHints(e)}catch{}}", "codexPetOverlayHyprlandSession(){if(process.platform!==`linux`)return!1;let e=[process.env.HYPRLAND_INSTANCE_SIGNATURE,process.env.XDG_CURRENT_DESKTOP,process.env.DESKTOP_SESSION].filter(Boolean).join(`:`).toLowerCase();return e.includes(`hyprland`)}", "codexPetOverlayShouldUseHyprland(){return process.platform===`linux`&&this.codexPetOverlaySettings().hyprland===!0&&this.codexPetOverlayHyprlandSession()}", "codexPetOverlayHyprctl(e,t){if(this.codexPetOverlayHyprctlUnavailable)return;try{let n=typeof require==`function`?require(`node:child_process`):null;if(typeof n?.execFile!=`function`){this.codexPetOverlayHyprctlUnavailable=!0;return}n.execFile(`hyprctl`,e,{timeout:1200},(e,...n)=>{e?.code===`ENOENT`&&(this.codexPetOverlayHyprctlUnavailable=!0),typeof t==`function`&&t(e,...n)})}catch(e){e?.code===`ENOENT`&&(this.codexPetOverlayHyprctlUnavailable=!0);try{typeof t==`function`&&t(e)}catch{}}}", @@ -178,19 +182,19 @@ function buildPetOverlayMethods(settings) { "codexPetOverlayScheduleHyprlandHints(e){if(!this.codexPetOverlayShouldUseHyprland())return;try{this.codexPetOverlayHyprlandTimers?.forEach(clearTimeout)}catch{}this.codexPetOverlayHyprlandTimers=[0,80,300,1000,2500,5000,10000].map(t=>{let n=setTimeout(()=>{try{e==null||e.isDestroyed?.()||this.codexPetOverlayApplyHyprlandHints(e)}catch{}},t);try{n.unref?.()}catch{}return n})}", "codexPetOverlayKWinSession(){if(process.platform!==`linux`)return!1;let e=String(process.env.KDE_FULL_SESSION??``).toLowerCase();if(process.env.KDE_SESSION_VERSION||e===`1`||e===`true`)return!0;let t=[process.env.XDG_CURRENT_DESKTOP,process.env.DESKTOP_SESSION].filter(Boolean).join(`:`).toLowerCase();return t.includes(`kde`)||t.includes(`plasma`)}", "codexPetOverlayShouldUseKWin(){return process.platform===`linux`&&this.codexPetOverlaySettings().kwin===!0&&this.codexPetOverlayKWinSession()}", - "codexPetOverlayKWinQdbus(e,t,n=!1){if(this.codexPetOverlayKWinUnavailable){try{typeof t==`function`&&t({code:`ENOENT`})}catch{}return}let r=e;try{let i=typeof require==`function`?require(`node:child_process`):null;if(typeof i?.execFile!=`function`){this.codexPetOverlayKWinUnavailable=!0;try{typeof t==`function`&&t({code:`ENOENT`})}catch{}return}i.execFile(n?`qdbus`:`qdbus6`,r,{timeout:1500},(e,...i)=>{if(e?.code===`ENOENT`&&!n){this.codexPetOverlayKWinQdbus(r,t,!0);return}e?.code===`ENOENT`&&(this.codexPetOverlayKWinUnavailable=!0);try{typeof t==`function`&&t(e,...i)}catch{}})}catch(e){if(e?.code===`ENOENT`&&!n){this.codexPetOverlayKWinQdbus(r,t,!0);return}e?.code===`ENOENT`&&(this.codexPetOverlayKWinUnavailable=!0);try{typeof t==`function`&&t(e)}catch{}}}", + "codexPetOverlayKWinQdbus(e,t,n=!1){if(this.codexPetOverlayKWinUnavailable){try{typeof t===`function`&&t({code:`ENOENT`})}catch{}return}let r=e;try{let i=typeof require===`function`?require(`node:child_process`):null;if(typeof i?.execFile!==`function`){this.codexPetOverlayKWinUnavailable=!0;try{typeof t===`function`&&t({code:`ENOENT`})}catch{}return}i.execFile(n?`qdbus`:`qdbus6`,r,{timeout:1500},(e,...i)=>{if(e?.code===`ENOENT`&&!n){this.codexPetOverlayKWinQdbus(r,t,!0);return}e?.code===`ENOENT`&&(this.codexPetOverlayKWinUnavailable=!0);try{typeof t===`function`&&t(e,...i)}catch{}})}catch(e){if(e?.code===`ENOENT`&&!n){this.codexPetOverlayKWinQdbus(r,t,!0);return}e?.code===`ENOENT`&&(this.codexPetOverlayKWinUnavailable=!0);try{typeof t===`function`&&t(e)}catch{}}}", "codexPetOverlayKWinScript(e,t){let n=this.codexPetOverlayRect(e),r=this.codexPetOverlaySettings(),i={pid:Number(process.pid),title:`Codex Pet Overlay`,alwaysOnTop:!!r.alwaysOnTop,allWorkspaces:!!r.allWorkspaces,skipTaskbar:!!r.skipTaskbar,move:!!t,x:Math.round(Number(n?.x)),y:Math.round(Number(n?.y)),width:Math.round(Number(n?.width)),height:Math.round(Number(n?.height))};return `(function(){var d=${JSON.stringify(i)};function windows(){try{if(typeof workspace.windowList==='function')return workspace.windowList()}catch(e){}try{if(typeof workspace.clientList==='function')return workspace.clientList()}catch(e){}try{if(workspace.stackingOrder&&typeof workspace.stackingOrder.length==='number')return workspace.stackingOrder}catch(e){}return[]}var a=windows().filter(function(w){try{return String(w.caption||'')===d.title&&Number(w.pid)===d.pid}catch(e){return false}});if(a.length!==1)return;var w=a[0];try{w.keepAbove=d.alwaysOnTop}catch(e){}try{w.skipTaskbar=d.skipTaskbar}catch(e){}try{w.skipPager=d.skipTaskbar}catch(e){}try{w.onAllDesktops=d.allWorkspaces}catch(e){}try{w.noBorder=true}catch(e){}if(d.move&&isFinite(d.x)&&isFinite(d.y)){try{var g=w.frameGeometry;w.frameGeometry={x:d.x,y:d.y,width:isFinite(d.width)&&d.width>0?d.width:g.width,height:isFinite(d.height)&&d.height>0?d.height:g.height}}catch(e){}}if(d.alwaysOnTop){try{if(typeof workspace.raiseWindow==='function')workspace.raiseWindow(w)}catch(e){}}})()`}", - "codexPetOverlayKWinRun(e,t,n){if(!this.codexPetOverlayShouldUseKWin()){try{typeof n==`function`&&n({code:`DISABLED`})}catch{}return}let r;try{let i=require(`node:fs`),a=require(`node:os`),o=require(`node:path`),s=(this.codexPetOverlayKWinScriptGeneration??0)+1;this.codexPetOverlayKWinScriptGeneration=s;let c=`codex_pet_overlay_${process.pid}_${Date.now()}_${s}`,l=o.join(a.tmpdir(),`${c}.js`);i.writeFileSync(l,this.codexPetOverlayKWinScript(e,t),{encoding:`utf8`,flag:`wx`,mode:384}),r=()=>{try{i.unlinkSync(l)}catch{}};let u=[`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.loadScript`,l,c];this.codexPetOverlayKWinQdbus(u,e=>{if(e){r();try{typeof n==`function`&&n(e)}catch{}return}this.codexPetOverlayKWinQdbus([`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.start`],e=>{this.codexPetOverlayKWinQdbus([`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.unloadScript`,c],()=>{r();try{typeof n==`function`&&n(e)}catch{}})})})}catch(e){try{r?.()}catch{}try{typeof n==`function`&&n(e)}catch{}}}", + "codexPetOverlayKWinRun(e,t,n){if(!this.codexPetOverlayShouldUseKWin()){try{typeof n===`function`&&n({code:`DISABLED`})}catch{}return}let r;try{let i=require(`node:fs`),a=require(`node:os`),o=require(`node:path`),s=(this.codexPetOverlayKWinScriptGeneration??0)+1;this.codexPetOverlayKWinScriptGeneration=s;let c=`codex_pet_overlay_${process.pid}_${Date.now()}_${s}`,l=o.join(a.tmpdir(),`${c}.js`);i.writeFileSync(l,this.codexPetOverlayKWinScript(e,t),{encoding:`utf8`,flag:`wx`,mode:384}),r=()=>{try{i.unlinkSync(l)}catch{}};let u=[`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.loadScript`,l,c];this.codexPetOverlayKWinQdbus(u,e=>{if(e){r();try{typeof n===`function`&&n(e)}catch{}return}this.codexPetOverlayKWinQdbus([`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.start`],e=>{this.codexPetOverlayKWinQdbus([`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.unloadScript`,c],()=>{r();try{typeof n===`function`&&n(e)}catch{}})})})}catch(e){try{r?.()}catch{}try{typeof n===`function`&&n(e)}catch{}}}", "codexPetOverlayApplyKWinHints(e){if(e==null||e.isDestroyed?.()||this.window!==e||!this.codexPetOverlayShouldUseKWin()||this.dragState!=null||this.codexPetOverlayKWinDragState!=null||this.codexPetOverlayKWinHintInFlight)return;this.codexPetOverlayKWinHintInFlight=!0;let t=this.codexPetOverlaySettings();this.codexPetOverlayKWinRun(this.codexPetOverlayDesiredWindowBounds,t.lockPosition===!0,()=>{this.codexPetOverlayKWinHintInFlight=!1,this.codexPetOverlayKWinDragState==null&&this.window===e&&!e.isDestroyed?.()&&this.codexPetOverlayKWinPendingHints&&(this.codexPetOverlayKWinPendingHints=!1,this.codexPetOverlayScheduleKWinHints(e))})}", "codexPetOverlayScheduleKWinHints(e){if(!this.codexPetOverlayShouldUseKWin()||this.dragState!=null||this.codexPetOverlayKWinDragState!=null)return;if(this.codexPetOverlayKWinHintInFlight){this.codexPetOverlayKWinPendingHints=!0;return}this.codexPetOverlayKWinPendingHints=!1;try{this.codexPetOverlayKWinTimers?.forEach(clearTimeout)}catch{}this.codexPetOverlayKWinTimers=[0,80,300,1000,2500].map(t=>{let n=setTimeout(()=>{try{this.codexPetOverlayApplyKWinHints(e)}catch{}},t);try{n.unref?.()}catch{}return n})}", - "codexPetOverlayKWinExecSync(e){if(this.codexPetOverlayKWinUnavailable||!this.codexPetOverlayShouldUseKWin())return!1;try{let t=typeof require==`function`?require(`node:child_process`):null;if(typeof t?.execFileSync!=`function`)return!1;for(let n of [`qdbus6`,`qdbus`])try{t.execFileSync(n,e,{timeout:750,stdio:`ignore`});return!0}catch(e){if(e?.code!==`ENOENT`)return!1}this.codexPetOverlayKWinUnavailable=!0}catch{}return!1}", + "codexPetOverlayKWinExecSync(e){if(this.codexPetOverlayKWinUnavailable||!this.codexPetOverlayShouldUseKWin())return!1;try{let t=typeof require===`function`?require(`node:child_process`):null;if(typeof t?.execFileSync!==`function`)return!1;for(let n of [`qdbus6`,`qdbus`])try{t.execFileSync(n,e,{timeout:750,stdio:`ignore`});return!0}catch(e){if(e?.code!==`ENOENT`)return!1}this.codexPetOverlayKWinUnavailable=!0}catch{}return!1}", "codexPetOverlayKWinDragScript(){let e={pid:Number(process.pid),title:`Codex Pet Overlay`};return `(function(){var d=${JSON.stringify(e)};function windows(){try{if(typeof workspace.windowList==='function')return workspace.windowList()}catch(e){}try{if(typeof workspace.clientList==='function')return workspace.clientList()}catch(e){}return[]}var a=windows().filter(function(w){try{return String(w.caption||'')===d.title&&Number(w.pid)===d.pid}catch(e){return false}});if(a.length!==1)return;var w=a[0],p=workspace.cursorPos,g=w.frameGeometry,dx=Number(p.x)-Number(g.x),dy=Number(p.y)-Number(g.y),active=true;function move(){if(!active)return;try{var p=workspace.cursorPos,g=w.frameGeometry;w.frameGeometry={x:Math.round(Number(p.x)-dx),y:Math.round(Number(p.y)-dy),width:g.width,height:g.height}}catch(e){stop()}}function stop(){if(!active)return;active=false;try{workspace.cursorPosChanged.disconnect(move)}catch(e){}}try{workspace.cursorPosChanged.connect(move)}catch(e){return}try{workspace.windowRemoved.connect(function(v){if(v===w)stop()})}catch(e){}try{if(typeof workspace.raiseWindow==='function')workspace.raiseWindow(w)}catch(e){}})()`}", "codexPetOverlayReleaseKWinDrag(e){if(e==null)return;try{this.codexPetOverlayKWinExecSync([`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.unloadScript`,e.pluginName])}catch{}try{require(`node:fs`).unlinkSync(e.scriptPath)}catch{}}", "codexPetOverlayStartKWinDrag(e){let t;try{let n=require(`node:fs`),r=require(`node:os`),i=require(`node:path`),a=(this.codexPetOverlayKWinDragGeneration??0)+1,o=`codex_pet_overlay_drag_${process.pid}_${Date.now()}_${a}`,s=i.join(r.tmpdir(),`${o}.js`);n.writeFileSync(s,this.codexPetOverlayKWinDragScript(),{encoding:`utf8`,flag:`wx`,mode:384}),t={generation:a,window:e,pluginName:o,scriptPath:s};let c=[`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.loadScript`,s,o];if(!this.codexPetOverlayKWinExecSync(c)||!this.codexPetOverlayKWinExecSync([`org.kde.KWin`,`/Scripting`,`org.kde.kwin.Scripting.start`])){this.codexPetOverlayReleaseKWinDrag(t);return null}return t}catch(e){this.codexPetOverlayReleaseKWinDrag(t);return null}}", "codexPetOverlayBeginKWinDrag(e){if(e==null||e.isDestroyed?.()||this.window!==e||!this.codexPetOverlayShouldUseKWin())return;try{this.codexPetOverlayKWinTimers?.forEach(clearTimeout)}catch{}let t=this.codexPetOverlayKWinDragState;t!=null&&(this.codexPetOverlayKWinDragState=null,this.codexPetOverlayReleaseKWinDrag(t));let n=this.codexPetOverlayStartKWinDrag(e);if(n==null)return;let r=null;try{r=Number(e.getContentBounds?.().x)}catch{}this.codexPetOverlayKWinDragGeneration=n.generation,this.codexPetOverlayKWinDragState=n,this.windowServerDragActive=!0,Number.isFinite(r)&&(this.windowServerDragWindowX=r)}", "codexPetOverlayKWinDragCurrent(e){if(e==null||this.codexPetOverlayKWinDragState!==e||this.codexPetOverlayKWinDragGeneration!==e.generation)return!1;if(this.window===e.window&&!e.window?.isDestroyed?.())return!0;this.codexPetOverlayKWinDragState=null,this.codexPetOverlayReleaseKWinDrag(e);return!1}", "codexPetOverlayQueueKWinDrag(e){let t=this.codexPetOverlayKWinDragState;this.codexPetOverlayKWinDragCurrent(t)&&t.window===e&&(this.windowServerDragActive=!0)}", - "codexPetOverlayEndKWinDrag(e,t){let n=this.codexPetOverlayKWinDragState;if(!this.codexPetOverlayKWinDragCurrent(n)||n.window!==e)return!1;this.codexPetOverlayKWinDragState=null,this.codexPetOverlayReleaseKWinDrag(n);try{typeof t==`function`&&t()}catch{}try{this.window!=null&&!this.window.isDestroyed?.()&&this.codexPetOverlayScheduleKWinHints(this.window)}catch{}return!0}", + "codexPetOverlayEndKWinDrag(e,t){let n=this.codexPetOverlayKWinDragState;if(!this.codexPetOverlayKWinDragCurrent(n)||n.window!==e)return!1;this.codexPetOverlayKWinDragState=null,this.codexPetOverlayReleaseKWinDrag(n);try{typeof t===`function`&&t()}catch{}try{this.window!=null&&!this.window.isDestroyed?.()&&this.codexPetOverlayScheduleKWinHints(this.window)}catch{}return!0}", "codexPetOverlayNiriSession(){if(process.platform!==`linux`)return!1;let e=[process.env.NIRI_SOCKET,process.env.XDG_CURRENT_DESKTOP,process.env.DESKTOP_SESSION].filter(Boolean).join(`:`).toLowerCase();return e.includes(`niri`)}", "codexPetOverlayShouldUseNiri(){return process.platform===`linux`&&this.codexPetOverlaySettings().niri===!0&&this.codexPetOverlayNiriSession()}", "codexPetOverlayFinishNiriProcess(){this.codexPetOverlayNiriProcessCount=Math.max(0,(this.codexPetOverlayNiriProcessCount??1)-1);if(this.codexPetOverlayNiriProcessCount===0){let e=this.codexPetOverlayNiriDragState;if(e!=null)this.codexPetOverlayPumpNiriDrag(e);else{let e=this.codexPetOverlayNiriPendingHintsWindow;this.codexPetOverlayNiriPendingHintsWindow=null;try{e!=null&&!e.isDestroyed?.()&&this.window===e&&this.codexPetOverlayScheduleNiriHints(e)}catch{}}}}", @@ -236,6 +240,26 @@ function patchCreateWindowTitle(source) { return replaceMethodText(source, method, replacement); } +function patchCreateWindowFrame(source) { + const method = findAvatarOverlayMethod(source, /async createWindow\([^)]*\)\{/); + if (method == null) { + console.warn("WARN: Could not find avatar overlay createWindow - skipping pet overlay frame patch"); + return source; + } + if (method.text.includes("frame:process.platform===`linux`?!1:!0")) { + return source; + } + const replacement = method.text.replace( + "appearance:`avatarOverlay`,", + "appearance:`avatarOverlay`,frame:process.platform===`linux`?!1:!0,", + ); + if (replacement === method.text) { + console.warn("WARN: Could not identify avatar overlay appearance option - skipping pet overlay frame patch"); + return source; + } + return replaceMethodText(source, method, replacement); +} + function ensurePetOverlayMethods(source, settings) { if (source.includes("codexPetOverlaySettings(){")) { return source; @@ -376,13 +400,65 @@ function patchLockedDrag(source) { ); } +function patchLocalMascotDrag(source) { + if (source.includes("if(this.codexPetOverlayStartLocalMascotDrag(")) { + return source; + } + const patches = [ + ["startDrag", "codexPetOverlayStartLocalMascotDrag"], + ["moveDrag", "codexPetOverlayMoveLocalMascotDrag"], + ["endDrag", "codexPetOverlayEndLocalMascotDrag"], + ]; + let patched = source; + for (const [methodName, helperName] of patches) { + const method = findAvatarOverlayMethod(patched, new RegExp(`${methodName}\\([^)]*\\)\\{`)); + const callerArg = method == null ? null : firstMethodArgument(method.match[0], methodName, 0); + const eventArg = method == null ? null : firstMethodArgument(method.match[0], methodName, 1); + if (method == null || callerArg == null || (methodName !== "endDrag" && eventArg == null)) { + if (methodName === "moveDrag") { + return source; + } + console.warn(`WARN: Could not find avatar overlay ${methodName} - skipping local pet drag patch`); + return source; + } + const call = methodName === "endDrag" + ? `this.${helperName}(${callerArg})` + : `this.${helperName}(${callerArg},${eventArg})`; + patched = replaceMethodText( + patched, + method, + method.text.slice(0, method.match[0].length) + `if(${call})return;` + method.text.slice(method.match[0].length), + ); + } + return patched; +} + +function patchLocalMascotDragLifecycle(source) { + if (source.includes("this.dragState=null,this.codexPetOverlayMascotDragState=null,")) { + return source; + } + const method = findAvatarOverlayMethod(source, /async createWindow\([^)]*\)\{/); + if (method == null || !method.text.includes("this.dragState=null,")) { + console.warn("WARN: Could not find avatar overlay lifecycle state - skipping local pet drag cleanup"); + return source; + } + return replaceMethodText( + source, + method, + method.text.replaceAll( + "this.dragState=null,", + "this.dragState=null,this.codexPetOverlayMascotDragState=null,", + ), + ); +} + function patchPassiveCreateWindow(source, settings) { if (settings.mode !== "passive") { return source; } return source - .split("appearance:`avatarOverlay`,alwaysOnTop:process.platform===`linux`,skipTaskbar:process.platform===`linux`,focusable:process.platform===`linux`?!0:!1") - .join("appearance:`avatarOverlay`,alwaysOnTop:process.platform===`linux`,skipTaskbar:process.platform===`linux`,focusable:!1"); + .split("appearance:`avatarOverlay`,frame:process.platform===`linux`?!1:!0,alwaysOnTop:process.platform===`linux`,skipTaskbar:process.platform===`linux`,focusable:process.platform===`linux`?!0:!1") + .join("appearance:`avatarOverlay`,frame:process.platform===`linux`?!1:!0,alwaysOnTop:process.platform===`linux`,skipTaskbar:process.platform===`linux`,focusable:!1"); } function patchAvatarSelectionRefresh(source) { @@ -403,12 +479,13 @@ function patchAvatarSelectionRefresh(source) { return helper + source.replace(handler, replacement); } -function hasCompletePetOverlayPatch(source, settings, avatarSelectionRefreshExpected) { +function hasCompletePetOverlayPatch(source, settings, avatarSelectionRefreshExpected, localMascotDragExpected) { const requiredMarkers = [ source.includes("codexPetOverlaySettings(){"), /let [A-Za-z_$][\w$]*=this\.codexPetOverlayLayoutForDisplay\([A-Za-z_$][\w$]*,this\.getLayoutForDisplay\([A-Za-z_$][\w$]*\),[A-Za-z_$][\w$]*\);/.test(source), /process\.platform===`linux`\?this\.codexPetOverlaySyncWindow\([A-Za-z_$][\w$]*,!0\):[A-Za-z_$][\w$]*\.moveTop\(\),[A-Za-z_$][\w$]*\.showInactive\(\),/.test(source), source.includes("if(this.codexPetOverlayShouldLockPosition())return;"), + source.includes("codexPetOverlayKWinQdbus("), source.includes("this.codexPetOverlayBeginKWinDrag("), source.includes("this.codexPetOverlayEndKWinDrag("), source.includes("this.codexPetOverlayBeginNiriDrag("), @@ -416,6 +493,14 @@ function hasCompletePetOverlayPatch(source, settings, avatarSelectionRefreshExpe source.includes("===`avatarOverlay`?{backgroundColor:`#00000000`,backgroundMaterial:null}:"), source.includes("title:`Codex Pet Overlay`,width:"), ]; + if (localMascotDragExpected) { + requiredMarkers.push( + source.includes("if(this.codexPetOverlayStartLocalMascotDrag("), + source.includes("if(this.codexPetOverlayMoveLocalMascotDrag("), + source.includes("if(this.codexPetOverlayEndLocalMascotDrag("), + source.includes("this.dragState=null,this.codexPetOverlayMascotDragState=null,"), + ); + } if (avatarSelectionRefreshExpected) { requiredMarkers.push( source.includes(`function ${AVATAR_SELECTION_REFRESH_MARKER}(`), @@ -424,7 +509,7 @@ function hasCompletePetOverlayPatch(source, settings, avatarSelectionRefreshExpe } if (settings.mode === "passive") { requiredMarkers.push( - source.includes("appearance:`avatarOverlay`,alwaysOnTop:process.platform===`linux`,skipTaskbar:process.platform===`linux`,focusable:!1"), + source.includes("appearance:`avatarOverlay`,frame:process.platform===`linux`?!1:!0,alwaysOnTop:process.platform===`linux`,skipTaskbar:process.platform===`linux`,focusable:!1"), ); } return requiredMarkers.every(Boolean); @@ -456,52 +541,30 @@ function applyPetOverlayPatch(source, context) { } const settings = mergedPetOverlaySettings(context); const avatarSelectionRefreshExpected = source.includes('"set-setting":async'); + const localMascotDragExpected = /moveDrag\([^)]*\)\{/.test(source); let patched = patchAvatarTransparentBackground(source); patched = patchCreateWindowTitle(patched); + patched = patchCreateWindowFrame(patched); patched = patchApplyLayout(patched); patched = patchShowWindow(patched); patched = patchLockedDrag(patched); patched = patchCompositorDragLifecycle(patched); patched = ensurePetOverlayMethods(patched, settings); + patched = patchLocalMascotDrag(patched); + if (localMascotDragExpected) { + patched = patchLocalMascotDragLifecycle(patched); + } patched = patchPassiveCreateWindow(patched, settings); if (avatarSelectionRefreshExpected) { patched = patchAvatarSelectionRefresh(patched); } - if (!hasCompletePetOverlayPatch(patched, settings, avatarSelectionRefreshExpected)) { + if (!hasCompletePetOverlayPatch(patched, settings, avatarSelectionRefreshExpected, localMascotDragExpected)) { console.warn("WARN: Pet overlay patch is incomplete - discarding all pet overlay changes"); return source; } return patched; } -function applyPetOverlayPointerRegionPatch(source) { - const marker = "closest(`[data-avatar-overlay-hit-region]`)==null"; - if (source.includes(marker)) { - return source; - } - const dragMarkers = [...source.matchAll(/avatar-overlay-drag-start/g)]; - if (dragMarkers.length !== 1) { - console.warn("WARN: Expected one avatar overlay drag marker - skipping pet pointer-region patch"); - return source; - } - const dragMarkerIndex = dragMarkers[0].index; - // Scope the generic pointer predicate to the minified handler that owns the - // avatar drag dispatch. Other handlers in this asset use the same predicate. - const handlerStart = source.lastIndexOf("=>{", dragMarkerIndex); - const handlerPrefix = handlerStart === -1 ? "" : source.slice(handlerStart + 3, dragMarkerIndex); - const pointerGuard = /([A-Za-z_$][\w$]*)\.button!==0\|\|!\(\1\.target instanceof Element\)\|\|\1\.target\.closest\(`\.no-drag`\)!=null\|\|\(/g; - const matches = [...handlerPrefix.matchAll(pointerGuard)]; - if (matches.length !== 1) { - console.warn("WARN: Could not find avatar overlay pointer-down guard - skipping pet pointer-region patch"); - return source; - } - const [match] = matches; - const matchIndex = handlerStart + 3 + match.index; - const eventVar = match[1]; - const replacement = `${eventVar}.button!==0||!(${eventVar}.target instanceof Element)||${eventVar}.target.closest(\`[data-avatar-overlay-hit-region]\`)==null||${eventVar}.target.closest(\`.no-drag\`)!=null||(`; - return source.slice(0, matchIndex) + replacement + source.slice(matchIndex + match[0].length); -} - const descriptors = [ { id: DESCRIPTOR_ID, @@ -510,23 +573,11 @@ const descriptors = [ ciPolicy: "optional", apply: applyPetOverlayPatch, }, - { - id: POINTER_REGION_DESCRIPTOR_ID, - phase: "webview-asset", - order: 20_510, - ciPolicy: "optional", - pattern: /^avatar-overlay-page-[^.]+\.js$/, - missingDescription: "avatar overlay page bundle", - skipDescription: "pet pointer-region patch", - apply: applyPetOverlayPointerRegionPatch, - }, ]; module.exports = { DESCRIPTOR_ID, - POINTER_REGION_DESCRIPTOR_ID, descriptors, applyPetOverlayPatch, - applyPetOverlayPointerRegionPatch, mergedPetOverlaySettings, }; diff --git a/linux-features/pet-overlay/test.js b/linux-features/pet-overlay/test.js index 697c3854f..eda3b17f5 100644 --- a/linux-features/pet-overlay/test.js +++ b/linux-features/pet-overlay/test.js @@ -16,9 +16,7 @@ const { } = require("../../scripts/lib/linux-features.js"); const { DESCRIPTOR_ID, - POINTER_REGION_DESCRIPTOR_ID, applyPetOverlayPatch, - applyPetOverlayPointerRegionPatch, mergedPetOverlaySettings, } = require("./patch.js"); @@ -57,13 +55,13 @@ function currentAvatarOverlayBundleFixture() { "constructor(e,t){this.windowManager=e,this.globalState=t}", "isOpen(){let e=this.window;return e!=null&&!e.isDestroyed()&&e.isVisible()&&!this.windowStagedForNativePresentation}", "startDrag(e,t,n=!1){let r=this.window;if(r==null||r.isDestroyed()||r.webContents.id!==e)return;this.cancelMomentum();let i=this.getLayout(r),o=this.compositionHost.getCursorPosition(),s=t.pointerScreenX!=null?{x:t.pointerScreenX,y:t.pointerScreenY}:a.screen.getCursorScreenPoint();this.dragState=new h2(o==null?`renderer`:`native`,t.pointerWindowX-i.mascot.left,t.pointerWindowY-i.mascot.top,a.screen.getDisplayNearestPoint(s).bounds,n),this.windowServerDragActive=this.layoutMode===`native`&&!n&&this.compositionHost.performWindowDrag(),this.windowServerDragActive||(this.windowServerDragWindowX=null)}", - "endDrag(e,t){let n=this.window;if(n==null||n.isDestroyed()||n.webContents.id!==e)return;let r=this.dragState,i=this.windowServerDragActive,a=null;this.dragState=null,this.windowServerDragActive=!1,this.windowServerDragWindowX=null,i?this.persistWindowBounds(n,a??this.getCurrentDisplay()):this.reclampWindowToVisibleDisplay({shouldPersist:!0});let o=this.dockTarget;o!=null&&this.dockPresentation(o.anchor,o.onDock)}", + "moveDrag(e,t){let n=this.window;if(n==null||n.isDestroyed()||n.webContents.id!==e)return;this.lastMove=t}endDrag(e,t){let n=this.window;if(n==null||n.isDestroyed()||n.webContents.id!==e)return;let r=this.dragState,i=this.windowServerDragActive,a=null;this.dragState=null,this.windowServerDragActive=!1,this.windowServerDragWindowX=null,i?this.persistWindowBounds(n,a??this.getCurrentDisplay()):this.reclampWindowToVisibleDisplay({shouldPersist:!0});let o=this.dockTarget;o!=null&&this.dockPresentation(o.anchor,o.onDock)}", "setElementSize(e,{elementSizeRevision:t,isTrayVisible:n,mascot:r,nativeCompositionEnabled:a,tray:o}){let i=this.window;i==null||i.isDestroyed()||i.webContents.id!==e||(this.cancelMomentum(),this.layoutMode=n==null?`native`:`legacy`,this.mascotSize=r,this.traySize=o,this.applyLatestElementSizes(i),this.stageWindowForNativePresentation(i),this.showWindowIfReady(i))}", "applyLatestElementSizes(e){this.anchor={...this.anchor,width:this.mascotSize.width,height:this.mascotSize.height},this.applyLayout(e)}", "async createWindow(e){let t=await this.windowManager.createWindow({title:a.app.getName(),width:zB.width,height:zB.height,appearance:`avatarOverlay`,alwaysOnTop:process.platform===`linux`,skipTaskbar:process.platform===`linux`,focusable:process.platform===`linux`?!0:!1,show:!1,initialRoute:rV});return this.window=t,this.compositionHost.setOverlayWindow(t),this.rendererReady=this.windowManager.isWebContentsReady(t.webContents.id),this.displayBounds=null,this.displayId=null,this.dragState=null,this.layout=null,this.mascotSize=oV,this.mousePassthroughEnabled=!1,this.traySize=null,t.on(`closed`,()=>{this.window===t&&(this.cancelMomentum(),this.window=null,this.dragState=null,this.layout=null,this.rendererReady=!1,this.pointerInteractive=!1,this.mousePassthroughEnabled=!1,this.compositionHost.setOverlayWindow(null),this.broadcastOpenState())}),t}", "applyLayout(e,t=this.getCurrentDisplay(),n=!1,r=!0,i=null){if(e.isDestroyed())return;let o=this.getLayoutForDisplay(t);this.displayId=t.id,this.layout=o,this.setWindowBounds(e,o.windowBounds,n,r),this.compositionHost.updateMascotRect(o.mascot),this.sendLayoutToRenderer(e,i)}getLayoutForDisplay(e){return UB({anchor:this.anchor,displayBounds:this.layoutMode===`native`?e.workArea:e.bounds,mode:this.layoutMode,mascotSize:this.mascotSize,nativeMaterialAttached:this.compositionHost.isNativeMaterialAttached(),previousPlacement:this.placement,traySize:this.traySize??(this.layoutMode===`native`?k2:O2)})}getLayout(e){if(this.layout??this.applyLayout(e),this.layout==null)throw Error(`Expected avatar overlay layout`);return this.layout}", "showWindow(e){if(e.isDestroyed())return;let t=this.isOpen();this.windowStagedForNativePresentation&&=(e.setOpacity(1),!1),e.moveTop(),e.showInactive(),!t&&this.isOpen()&&(this.finishPendingPresentation(),this.broadcastOpenState())}showWindowIfReady(e){!this.rendererReady||this.initialPresentationState!==`ready`||(this.showWindow(e),this.applyPointerInteractivityPolicy())}stageWindowForNativePresentation(e){e.isDestroyed()||this.applyPointerInteractivityPolicy()}broadcastOpenState(){this.windowManager.sendMessageToAllRegisteredWindows({type:`avatar-overlay-open-state-changed`,isOpen:this.isOpen()})}", - "applyPointerInteractivityPolicy(){return null}cancelMomentum(){}finishPendingPresentation(){}sendLayoutToRenderer(){}setWindowBounds(){}persistWindowBounds(){}reclampWindowToVisibleDisplay({shouldPersist:e}){e&&this.persistWindowBounds(this.window,this.getCurrentDisplay())}dockPresentation(){}getCurrentDisplay(){return{id:1,bounds:{x:0,y:0,width:1920,height:1080},workArea:{x:0,y:0,width:1920,height:1080}}}};", + "applyPointerInteractivityPolicy(){return null}cancelMomentum(){}finishPendingPresentation(){}sendLayoutToRenderer(){}setWindowBounds(){}getCurrentDisplay(){return{id:1,bounds:{x:0,y:0,width:1920,height:1080},workArea:{x:0,y:0,width:1920,height:1080}}}};", "function L9({platform:e,appearance:t,opaqueWindowSurfaceEnabled:n,prefersDarkColors:r}){return n?{backgroundColor:r?_ne:vne,backgroundMaterial:e===`win32`?`none`:null}:e===`win32`?{backgroundColor:k9,backgroundMaterial:`mica`}:{backgroundColor:k9,backgroundMaterial:null}}", ].join(""); } @@ -94,15 +92,9 @@ function controllerFromPatchedSource(patched, overrides = {}) { if (moduleName === "node:child_process") { return overrides.childProcess ?? { execFile() {} }; } - if (moduleName === "node:fs") { - return fs; - } - if (moduleName === "node:os") { - return os; - } - if (moduleName === "node:path") { - return path; - } + if (moduleName === "node:fs") return fs; + if (moduleName === "node:os") return os; + if (moduleName === "node:path") return path; if (moduleName === "electron") { return { app: { getName: () => "Codex" }, @@ -163,10 +155,7 @@ test("pet-overlay is discoverable and disabled until listed in features.json", ( const descriptors = loadLinuxFeaturePatchDescriptors({ featuresRoot }); assert.deepEqual( descriptors.map((descriptor) => [descriptor.id, descriptor.phase, descriptor.ciPolicy]), - [ - [`feature:pet-overlay:${DESCRIPTOR_ID}`, "main-bundle", "optional"], - [`feature:pet-overlay:${POINTER_REGION_DESCRIPTOR_ID}`, "webview-asset", "optional"], - ], + [[`feature:pet-overlay:${DESCRIPTOR_ID}`, "main-bundle", "optional"]], ); const plan = enabledLinuxFeatureInstallPlan({ featuresRoot }); assert.deepEqual( @@ -227,28 +216,6 @@ test("patches current avatar overlay layout, transparency, and window sync", () assert.equal((patched.match(/codexPetOverlayLayoutForDisplay/g) ?? []).length, 2); }); -test("limits renderer drag starts to visible pet and tray hit regions", () => { - const unrelated = "a=e=>{e.button!==0||!(e.target instanceof Element)||e.target.closest(`.no-drag`)!=null||(e.preventDefault(),k.dispatchMessage(`unrelated-drag-start`,{}))};"; - const avatar = "Ge=e=>{e.button!==0||!(e.target instanceof Element)||e.target.closest(`.no-drag`)!=null||(e.preventDefault(),k.dispatchMessage(`avatar-overlay-drag-start`,{}))}"; - const source = unrelated + avatar; - const patched = applyPetOverlayPointerRegionPatch(source); - - assert.match(patched, new RegExp(unrelated.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); - assert.match( - patched, - /e\.target\.closest\(`\[data-avatar-overlay-hit-region\]`\)==null\|\|e\.target\.closest\(`\.no-drag`\)!=null/, - ); - assert.equal(applyPetOverlayPointerRegionPatch(patched), patched); -}); - -test("pointer-region patch fails closed when the current pointer guard drifts", () => { - const source = "Ge=e=>{if(e.button===0)k.dispatchMessage(`avatar-overlay-drag-start`,{})}"; - const { result, warnings } = captureWarnings(() => applyPetOverlayPointerRegionPatch(source)); - - assert.equal(result, source); - assert.match(warnings.join("\n"), /Could not find avatar overlay pointer-down guard/); -}); - test("refreshes only the avatar overlay after the selected pet changes", async () => { const patched = applyPatchTwice(currentAvatarOverlayBundleFixture()); const reloads = []; @@ -342,20 +309,6 @@ test("discards every change when a required current hook drifts", () => { assert.match(warnings.join("\n"), /Pet overlay patch is incomplete/); }); -test("patches the current dock-threshold drag completion shape", () => { - const source = currentAvatarOverlayBundleFixture().replace( - "i?this.persistWindowBounds(n,a??this.getCurrentDisplay()):this.reclampWindowToVisibleDisplay({shouldPersist:!0});let o=this.dockTarget;o!=null&&this.dockPresentation(o.anchor,o.onDock)", - "i?this.persistWindowBounds(n,a??this.getCurrentDisplay()):this.reclampWindowToVisibleDisplay({shouldPersist:!0});let o=this.dockTarget,s=this.anchor;o!=null&&shouldDock({current:s,target:o.anchor}).shouldDock&&this.dockPresentation(o.anchor,o.onDock)", - ); - const { result, warnings } = captureWarnings(() => applyPetOverlayPatch(source)); - - assert.notEqual(result, source); - assert.doesNotMatch(warnings.join("\n"), /drag completion shape/); - assert.match(result, /this\.codexPetOverlayEndKWinDrag\(n,\(\)=>\{i\?this\.persistWindowBounds/); - assert.match(result, /this\.codexPetOverlayEndNiriDrag\(n,\(\)=>\{i\?this\.persistWindowBounds/); - assert.match(result, /let o=this\.dockTarget,s=this\.anchor;o!=null&&shouldDock/); -}); - test("passive mode fails closed when the current create-window shape drifts", () => { const source = currentAvatarOverlayBundleFixture().replace( "appearance:`avatarOverlay`,alwaysOnTop:process.platform===`linux`,skipTaskbar:process.platform===`linux`,focusable:process.platform===`linux`?!0:!1", @@ -499,6 +452,199 @@ test("unlocked layout does not re-anchor while a drag is active", () => { assert.deepEqual(JSON.parse(JSON.stringify(result.windowBounds)), layout.windowBounds); }); +test("unlocked mascot drags stay inside the overlay and do not invoke native window dragging", () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + const { controller } = controllerFromPatchedSource(patched); + const nativeDragCalls = []; + const rendererLayouts = []; + controller.window = { + getBounds: () => ({ x: 100, y: 200, width: 356, height: 320 }), + isDestroyed: () => false, + webContents: { id: 1 }, + }; + controller.layout = { + anchor: { x: 110, y: 210, width: 40, height: 40 }, + mascot: { left: 10, top: 10, width: 40, height: 40 }, + placement: "top-end", + tray: { left: 10, top: 54, width: 276, height: 131 }, + windowBounds: { x: 100, y: 200, width: 356, height: 320 }, + }; + controller.compositionHost = { + performWindowDrag: () => nativeDragCalls.push("window"), + updateMascotRect: () => {}, + }; + controller.sendLayoutToRenderer = (_window, layout) => rendererLayouts.push(layout); + + controller.startDrag(1, { pointerWindowX: 20, pointerWindowY: 20 }); + controller.moveDrag(1, { pointerScreenX: 350, pointerScreenY: 500 }); + + assert.deepEqual(nativeDragCalls, []); + assert.deepEqual(JSON.parse(JSON.stringify(controller.codexPetOverlayMascotLocalPosition)), { left: 240, top: 280 }); + assert.deepEqual(JSON.parse(JSON.stringify(controller.layout.mascot)), { left: 240, top: 280, width: 40, height: 40 }); + assert.deepEqual(JSON.parse(JSON.stringify(controller.layout.tray)), { left: 4, top: 145, width: 276, height: 131 }); + assert.equal(rendererLayouts.length, 1); +}); + +test("notification tray chooses a non-overlapping vertical side and stays within overlay bounds", () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + const { controller } = controllerFromPatchedSource(patched); + const windowBounds = { x: 400, y: 300, width: 356, height: 320 }; + const baseLayout = { + anchor: { x: 410, y: 310, width: 112, height: 121 }, + mascot: { left: 10, top: 10, width: 112, height: 121 }, + placement: "top-end", + tray: { left: 0, top: 0, width: 276, height: 131 }, + windowBounds, + }; + + controller.codexPetOverlayMascotLocalPosition = { left: 0, top: 0 }; + const atTop = controller.codexPetOverlayTrayAboveLeft(baseLayout); + assert.deepEqual(JSON.parse(JSON.stringify(atTop.mascot)), { left: 0, top: 0, width: 112, height: 121 }); + assert.equal(atTop.tray.top, 125); + assert.ok(atTop.tray.top >= atTop.mascot.top + atTop.mascot.height + 4); + assert.deepEqual(JSON.parse(JSON.stringify(atTop.windowBounds)), windowBounds); + + controller.codexPetOverlayMascotLocalPosition = { left: 100, top: 100 }; + const snappedFromMiddle = controller.codexPetOverlayTrayAboveLeft(baseLayout); + assert.deepEqual(JSON.parse(JSON.stringify(snappedFromMiddle.mascot)), { left: 100, top: 135, width: 112, height: 121 }); + assert.deepEqual(JSON.parse(JSON.stringify(controller.codexPetOverlayMascotLocalPosition)), { left: 100, top: 135 }); + assert.equal(snappedFromMiddle.tray.top, 0); + assert.ok(snappedFromMiddle.tray.top + snappedFromMiddle.tray.height + 4 <= snappedFromMiddle.mascot.top); + assert.deepEqual(JSON.parse(JSON.stringify(snappedFromMiddle.windowBounds)), windowBounds); + + controller.codexPetOverlayMascotLocalPosition = { left: 244, top: 180 }; + const withUpperSpace = controller.codexPetOverlayTrayAboveLeft(baseLayout); + assert.deepEqual(JSON.parse(JSON.stringify(withUpperSpace.mascot)), { left: 244, top: 180, width: 112, height: 121 }); + assert.equal(withUpperSpace.tray.top, 45); + assert.ok(withUpperSpace.tray.top + withUpperSpace.tray.height + 4 <= withUpperSpace.mascot.top); + assert.ok(withUpperSpace.tray.left >= 0); + assert.ok(withUpperSpace.tray.left + withUpperSpace.tray.width <= windowBounds.width); + assert.ok(withUpperSpace.tray.top >= 0); + assert.ok(withUpperSpace.tray.top + withUpperSpace.tray.height <= windowBounds.height); + assert.deepEqual(JSON.parse(JSON.stringify(withUpperSpace.windowBounds)), windowBounds); + + controller.codexPetOverlayMascotLocalPosition = { left: 0, top: 0 }; + const oversizedTray = controller.codexPetOverlayTrayAboveLeft({ + ...baseLayout, + tray: { left: 0, top: 0, width: 700, height: 700 }, + }); + assert.equal(oversizedTray.tray.width, 356); + assert.equal(oversizedTray.tray.height, 195); + assert.ok(oversizedTray.tray.left >= 0); + assert.ok(oversizedTray.tray.top >= 0); + assert.ok(oversizedTray.tray.left + oversizedTray.tray.width <= windowBounds.width); + assert.ok(oversizedTray.tray.top + oversizedTray.tray.height <= windowBounds.height); + assert.ok(oversizedTray.tray.top >= oversizedTray.mascot.top + oversizedTray.mascot.height + 4); +}); + +test("another renderer cannot consume or mutate a local mascot drag", () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + const { controller } = controllerFromPatchedSource(patched); + const nativeDragCalls = []; + let boundsWrites = 0; + controller.window = { + getBounds: () => ({ x: 100, y: 200, width: 356, height: 320 }), + isDestroyed: () => false, + webContents: { id: 1 }, + }; + controller.layout = { + anchor: { x: 110, y: 210, width: 40, height: 40 }, + mascot: { left: 10, top: 10, width: 40, height: 40 }, + placement: "top-end", + tray: { left: 10, top: 54, width: 276, height: 131 }, + windowBounds: { x: 100, y: 200, width: 356, height: 320 }, + }; + controller.compositionHost = { + getCursorPosition: () => null, + performWindowDrag: () => nativeDragCalls.push("window"), + updateMascotRect() {}, + }; + controller.setWindowBounds = () => { + boundsWrites += 1; + }; + const before = JSON.parse(JSON.stringify(controller.layout)); + + controller.startDrag(2, { pointerWindowX: 20, pointerWindowY: 20 }); + controller.moveDrag(2, { pointerScreenX: 350, pointerScreenY: 500 }); + + assert.equal(controller.codexPetOverlayMascotDragState, undefined); + assert.equal(controller.codexPetOverlayMascotLocalPosition, undefined); + assert.deepEqual(JSON.parse(JSON.stringify(controller.layout)), before); + assert.deepEqual(nativeDragCalls, []); + assert.equal(boundsWrites, 0); + + controller.endDrag(2, {}); + assert.equal(controller.codexPetOverlayMascotDragState, undefined); +}); + +test("overlay close and recreate clear only ephemeral mascot drag state", async () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + const { controller } = controllerFromPatchedSource(patched); + let closed; + const localPosition = { left: 120, top: 80 }; + controller.codexPetOverlayMascotDragState = { offsetX: 3, offsetY: 4 }; + controller.codexPetOverlayMascotLocalPosition = localPosition; + controller.windowManager.createWindow = async () => ({ + isDestroyed: () => false, + isVisible: () => false, + on(event, handler) { + if (event === "closed") { + closed = handler; + } + }, + webContents: { id: 1 }, + }); + + await controller.createWindow(); + assert.equal(controller.codexPetOverlayMascotDragState, null); + assert.equal(controller.codexPetOverlayMascotLocalPosition, localPosition); + + controller.codexPetOverlayMascotDragState = { offsetX: 7, offsetY: 8 }; + closed(); + assert.equal(controller.codexPetOverlayMascotDragState, null); + assert.equal(controller.codexPetOverlayMascotLocalPosition, localPosition); +}); + +test("background drags retain native window movement and local mascot offsets survive later layout", () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + const { controller } = controllerFromPatchedSource(patched); + const nativeDragCalls = []; + controller.window = { + getBounds: () => ({ x: 900, y: 410, width: 356, height: 320 }), + isDestroyed: () => false, + isVisible: () => true, + webContents: { id: 1 }, + }; + controller.layout = { + anchor: { x: 110, y: 210, width: 40, height: 40 }, + mascot: { left: 10, top: 10, width: 40, height: 40 }, + placement: "top-end", + tray: { left: 10, top: 54, width: 276, height: 131 }, + windowBounds: { x: 100, y: 200, width: 356, height: 320 }, + }; + controller.compositionHost = { + getCursorPosition: () => null, + performWindowDrag: () => nativeDragCalls.push("window"), + updateMascotRect() {}, + }; + controller.getCurrentDisplay = () => null; + controller.persistWindowBounds = () => {}; + controller.codexPetOverlayMascotLocalPosition = { left: 100, top: 50 }; + + controller.startDrag(1, { pointerWindowX: 300, pointerWindowY: 20 }); + assert.deepEqual(nativeDragCalls, ["window"]); + controller.endDrag(1, {}); + + const result = controller.codexPetOverlayLayoutForDisplay( + { workArea: { x: 0, y: 0, width: 1920, height: 1080 } }, + controller.layout, + controller.window, + ); + assert.deepEqual(JSON.parse(JSON.stringify(result.windowBounds)), { x: 900, y: 410, width: 356, height: 320 }); + assert.deepEqual(JSON.parse(JSON.stringify(result.mascot)), { left: 100, top: 50, width: 40, height: 40 }); + assert.deepEqual(JSON.parse(JSON.stringify(result.tray)), { left: 0, top: 94, width: 276, height: 131 }); +}); + test("syncs overlay window hints without requiring Hyprland", () => { const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); const calls = []; @@ -550,6 +696,13 @@ test("syncs overlay window hints without requiring Hyprland", () => { assert.equal(calls[6][2].cssOrigin, "author"); assert.equal(calls[7][0], "js"); assert.match(calls[6][1], /background:transparent!important/); + assert.match(calls[6][1], /-webkit-app-region:drag!important/); + assert.match(calls[6][1], /app-region:drag!important/); + assert.match(calls[6][1], /user-select:none!important/); + assert.match( + calls[6][1], + /\[data-avatar-overlay-hit-region="mascot"\],\[data-avatar-mascot="true"\],\.no-drag,\[data-avatar-overlay-hit-region="notification-tray"\],\[data-avatar-overlay-hit-region="notification-scroll-control"\]\{-webkit-app-region:no-drag!important;app-region:no-drag!important;\}/, + ); assert.match(calls[7][1], /document\.documentElement\.style\.background/); assert.deepEqual(calls.slice(8), [["opacity", 1], ["workspaces", true, true], "moveTop", "showInactive"]); assert.deepEqual(timers.map((timer) => timer.delay), [0]); @@ -560,8 +713,97 @@ test("syncs overlay window hints without requiring Hyprland", () => { assert.deepEqual(calls.at(-1), ["focusable", true]); handlers["did-finish-load"](); - assert.equal(calls.filter(([kind]) => kind === "css").length, 2); - assert.equal(calls.filter(([kind]) => kind === "js").length, 2); + const cssCalls = calls.filter(([kind]) => kind === "css"); + const jsCalls = calls.filter(([kind]) => kind === "js"); + assert.equal(cssCalls.length, 2); + assert.equal(jsCalls.length, 2); + assert.match(cssCalls[1][1], /background:transparent!important/); + assert.match(cssCalls[1][1], /-webkit-app-region:drag!important/); + assert.match(cssCalls[1][1], /app-region:drag!important/); + assert.match( + cssCalls[1][1], + /\[data-avatar-overlay-hit-region="mascot"\],\[data-avatar-mascot="true"\],\.no-drag,\[data-avatar-overlay-hit-region="notification-tray"\],\[data-avatar-overlay-hit-region="notification-scroll-control"\]\{-webkit-app-region:no-drag!important;app-region:no-drag!important;\}/, + ); + assert.match(jsCalls[1][1], /document\.documentElement\.style\.background/); +}); + +test("unlocked pet overlays are frameless only on Linux and opt into whole-window input", async () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + assert.match(patched, /frame:process\.platform===`linux`\?!1:!0/); + assert.match(patched, /codexPetOverlayShouldUseWholeWindowInput\(\)\{return process\.platform===`linux`&&this\.codexPetOverlaySettings\(\)\.lockPosition!==!0\}/); + assert.match(patched, /codexLinuxWholeWindowInput=this\.codexPetOverlayShouldUseWholeWindowInput\(\)/); + + const { controller } = controllerFromPatchedSource(patched); + const created = []; + const makeWindow = () => ({ + isDestroyed: () => false, + isVisible: () => false, + on() {}, + webContents: { id: 1 }, + }); + controller.windowManager.createWindow = async (options) => { + created.push(options); + return makeWindow(); + }; + await controller.createWindow(); + assert.equal(created[0].frame, false); + controller.codexPetOverlaySyncWindow(controller.window); + assert.equal(controller.codexLinuxWholeWindowInput, true); + + const { controller: nonLinuxController } = controllerFromPatchedSource(patched, { + process: { platform: "darwin" }, + }); + const nonLinuxCreated = []; + nonLinuxController.windowManager.createWindow = async (options) => { + nonLinuxCreated.push(options); + return makeWindow(); + }; + await nonLinuxController.createWindow(); + assert.equal(nonLinuxCreated[0].frame, true); + assert.equal(nonLinuxController.codexLinuxWholeWindowInput, undefined); +}); + +test("unlocked drag CSS protects native mascot markup without an overlay hit region", () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + const insertedCss = []; + const { controller } = controllerFromPatchedSource(patched); + const nativeMascotMarkup = '
'; + assert.doesNotMatch(nativeMascotMarkup, /data-avatar-overlay-hit-region/); + + controller.codexPetOverlayInstallTransparentRenderer({ + isDestroyed: () => false, + webContents: { + insertCSS: (css) => insertedCss.push(css), + isDestroyed: () => false, + on() {}, + }, + }); + + assert.match( + insertedCss[0], + /\[data-avatar-mascot="true"\],\.no-drag,\[data-avatar-overlay-hit-region="notification-tray"\],\[data-avatar-overlay-hit-region="notification-scroll-control"\]\{-webkit-app-region:no-drag!important;app-region:no-drag!important;\}/, + ); +}); + +test("locked pet overlays omit drag CSS and whole-window input", () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture(), { + feature: { manifest: { petOverlay: { lockPosition: true } }, settings: {} }, + }); + const calls = []; + const { controller } = controllerFromPatchedSource(patched); + const window = { + isDestroyed: () => false, + webContents: { + insertCSS: (css) => calls.push(css), + isDestroyed: () => false, + on() {}, + }, + }; + controller.codexPetOverlayInstallTransparentRenderer(window); + assert.equal(calls.length, 1); + assert.doesNotMatch(calls[0], /app-region:(drag|no-drag)/); + assert.doesNotMatch(calls[0], /-webkit-app-region:(drag|no-drag)/); + assert.equal(controller.codexLinuxWholeWindowInput, false); }); test("passive mode makes the overlay non-focusable", () => { @@ -571,7 +813,7 @@ test("passive mode makes the overlay non-focusable", () => { assert.match( patched, - /appearance:`avatarOverlay`,alwaysOnTop:process\.platform===`linux`,skipTaskbar:process\.platform===`linux`,focusable:!1/, + /appearance:`avatarOverlay`,frame:process\.platform===`linux`\?!1:!0,alwaysOnTop:process\.platform===`linux`,skipTaskbar:process\.platform===`linux`,focusable:!1/, ); }); @@ -671,6 +913,7 @@ test("runtime lock override blocks drag start", () => { process: { env: { CODEX_PET_OVERLAY_LOCK_POSITION: "1" } }, }); controller.window = { isDestroyed: () => false, webContents: { id: 1 } }; + controller.layoutMode = "legacy"; controller.getLayout = () => ({ mascot: { left: 0, top: 0 } }); controller.dragState = { preserved: true }; @@ -692,6 +935,7 @@ test("runtime unlock override permits drag on a locked build", () => { process: { env: { CODEX_PET_OVERLAY_LOCK_POSITION: "0" } }, }); controller.window = { isDestroyed: () => false, webContents: { id: 1 } }; + controller.layoutMode = "legacy"; controller.getLayout = () => ({ mascot: { left: 0, top: 0 } }); controller.startDrag(1, { @@ -1278,202 +1522,6 @@ test("environment overrides can turn Hyprland handling off", () => { assert.deepEqual(calls, []); }); -test("KWin bridge applies keep-above hints and exact Wayland geometry to only the matching pet", () => { - const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture(), { - feature: { manifest: { petOverlay: { kwin: true, lockPosition: true } }, settings: {} }, - }); - const calls = []; - let script = null; - const { controller } = controllerFromPatchedSource(patched, { - process: { env: { XDG_CURRENT_DESKTOP: "KDE" } }, - childProcess: { - execFile(command, args, options, callback) { - calls.push([command, args]); - assert.equal(command, "qdbus6"); - assert.equal(options.timeout, 1500); - if (args.includes("org.kde.kwin.Scripting.loadScript")) { - script = fs.readFileSync(args[3], "utf8"); - } - callback(null, "ok"); - }, - }, - }); - const window = { isDestroyed: () => false }; - controller.window = window; - controller.codexPetOverlayDesiredWindowBounds = { x: 610, y: 330, width: 356, height: 320 }; - - controller.codexPetOverlayApplyKWinHints(window); - - assert.deepEqual( - calls.map(([, args]) => args[2]), - [ - "org.kde.kwin.Scripting.loadScript", - "org.kde.kwin.Scripting.start", - "org.kde.kwin.Scripting.unloadScript", - ], - ); - assert.ok(script); - const pet = { - caption: "Codex Pet Overlay", - frameGeometry: { x: 10, y: 20, width: 356, height: 320 }, - pid: 4242, - }; - const main = { - caption: "ChatGPT", - frameGeometry: { x: 0, y: 0, width: 1280, height: 820 }, - pid: 4242, - }; - const raised = []; - vm.runInNewContext(script, { - workspace: { - raiseWindow: (target) => raised.push(target), - windowList: () => [main, pet], - }, - }); - - assert.equal(pet.keepAbove, true); - assert.equal(pet.onAllDesktops, true); - assert.equal(pet.skipTaskbar, true); - assert.equal(pet.skipPager, true); - assert.equal(pet.noBorder, true); - assert.deepEqual(JSON.parse(JSON.stringify(pet.frameGeometry)), { x: 610, y: 330, width: 356, height: 320 }); - assert.deepEqual(raised, [pet]); - assert.equal(main.keepAbove, undefined); - - const duplicateA = { caption: "Codex Pet Overlay", frameGeometry: {}, pid: 4242 }; - const duplicateB = { caption: "Codex Pet Overlay", frameGeometry: {}, pid: 4242 }; - vm.runInNewContext(script, { - workspace: { - raiseWindow() {}, - windowList: () => [duplicateA, duplicateB], - }, - }); - assert.equal(duplicateA.keepAbove, undefined); - assert.equal(duplicateB.keepAbove, undefined); -}); - -test("KWin drag follows compositor cursor changes without losing the grab offset", () => { - const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); - const calls = []; - let script = null; - const { controller } = controllerFromPatchedSource(patched, { - process: { env: { XDG_CURRENT_DESKTOP: "KDE" } }, - childProcess: { - execFileSync(command, args, options) { - calls.push([command, args, options]); - if (args.includes("org.kde.kwin.Scripting.loadScript")) { - script = fs.readFileSync(args[3], "utf8"); - } - }, - }, - }); - const window = { - getContentBounds: () => ({ x: 145, y: 210, width: 356, height: 320 }), - isDestroyed: () => false, - }; - let persisted = false; - controller.window = window; - - controller.codexPetOverlayBeginKWinDrag(window); - assert.equal(calls.length, 2); - assert.equal(calls[0][0], "qdbus6"); - assert.equal(calls[0][1][2], "org.kde.kwin.Scripting.loadScript"); - assert.equal(calls[1][1][2], "org.kde.kwin.Scripting.start"); - assert.equal(calls[0][2].timeout, 750); - assert.equal(controller.windowServerDragActive, true); - assert.equal(controller.windowServerDragWindowX, 145); - assert.ok(script); - - const cursorSignal = { callback: null, connect(callback) { this.callback = callback; }, disconnect() {} }; - const removedSignal = { connect() {} }; - const pet = { - caption: "Codex Pet Overlay", - frameGeometry: { x: 100, y: 200, width: 356, height: 320 }, - pid: 4242, - }; - const workspace = { - cursorPos: { x: 130, y: 250 }, - cursorPosChanged: cursorSignal, - raiseWindow() {}, - windowList: () => [pet], - windowRemoved: removedSignal, - }; - vm.runInNewContext(script, { workspace }); - assert.equal(typeof cursorSignal.callback, "function"); - workspace.cursorPos = { x: 300, y: 410 }; - cursorSignal.callback(); - assert.deepEqual( - JSON.parse(JSON.stringify(pet.frameGeometry)), - { x: 270, y: 360, width: 356, height: 320 }, - ); - - controller.codexPetOverlayQueueKWinDrag(window); - assert.equal(calls.length, 2, "pointer updates must not spawn compositor processes"); - const scriptPath = controller.codexPetOverlayKWinDragState.scriptPath; - assert.equal(fs.existsSync(scriptPath), true); - assert.equal(controller.codexPetOverlayEndKWinDrag(window, () => { persisted = true; }), true); - assert.equal(calls.length, 3); - assert.equal(calls[2][1][2], "org.kde.kwin.Scripting.unloadScript"); - assert.equal(fs.existsSync(scriptPath), false); - assert.equal(persisted, true); - assert.equal(controller.codexPetOverlayKWinDragState, null); -}); - -test("KWin drag falls back without repeatedly probing missing qdbus commands", () => { - const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); - let calls = 0; - const { controller } = controllerFromPatchedSource(patched, { - process: { env: { XDG_CURRENT_DESKTOP: "KDE" } }, - childProcess: { - execFileSync() { - calls += 1; - const error = new Error("qdbus missing"); - error.code = "ENOENT"; - throw error; - }, - }, - }); - const window = { isDestroyed: () => false }; - controller.window = window; - - controller.codexPetOverlayBeginKWinDrag(window); - - assert.equal(calls, 2); - assert.equal(controller.codexPetOverlayKWinDragState, undefined); - assert.equal(controller.windowServerDragActive, undefined); - - controller.codexPetOverlayBeginKWinDrag(window); - assert.equal(calls, 2); -}); - -test("settings and environment can disable KWin handling", () => { - const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture(), { - feature: { manifest: { petOverlay: { kwin: true } }, settings: { petOverlay: { kwin: false } } }, - }); - const disabled = controllerFromPatchedSource(patched, { - process: { env: { XDG_CURRENT_DESKTOP: "KDE" } }, - }).controller; - assert.equal(disabled.codexPetOverlayShouldUseKWin(), false); - - const overridden = controllerFromPatchedSource( - applyPetOverlayPatch(currentAvatarOverlayBundleFixture()), - { process: { env: { XDG_CURRENT_DESKTOP: "KDE", CODEX_PET_OVERLAY_KWIN: "0" } } }, - ).controller; - assert.equal(overridden.codexPetOverlayShouldUseKWin(), false); - - const kdeSession = controllerFromPatchedSource( - applyPetOverlayPatch(currentAvatarOverlayBundleFixture()), - { process: { env: { KDE_SESSION_VERSION: "6" } } }, - ).controller; - assert.equal(kdeSession.codexPetOverlayShouldUseKWin(), true); - - const falseKdeSession = controllerFromPatchedSource( - applyPetOverlayPatch(currentAvatarOverlayBundleFixture()), - { process: { env: { KDE_FULL_SESSION: "false" } } }, - ).controller; - assert.equal(falseKdeSession.codexPetOverlayShouldUseKWin(), false); -}); - test("targets a tiled Niri pet window by id and moves it without focus actions", () => { const calls = runNiriHintScenario({ windowsJson: JSON.stringify([ @@ -1854,6 +1902,7 @@ test("Niri endDrag drains the final move before persisting and docking", () => { const { controller, pending, window } = scenario; const completed = []; controller.getLayout = () => ({ mascot: { left: 0, top: 0 } }); + controller.compositionHost.performWindowDrag = () => true; controller.persistWindowBounds = (target, display) => completed.push(["persist", target, display]); controller.dockTarget = { anchor: "dock-anchor", onDock: "dock-handler" }; controller.dockPresentation = (anchor, onDock) => completed.push(["dock", anchor, onDock]); @@ -2040,3 +2089,152 @@ test("settings validation falls back to safe defaults", () => { }, ); }); + +test("KWin Plasma bridge remains available with its runtime override and drag lifecycle", () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + + assert.equal(mergedPetOverlaySettings({}).kwin, true); + assert.match(patched, /CODEX_PET_OVERLAY_KWIN/); + assert.match(patched, /codexPetOverlayKWinQdbus\(/); + assert.match(patched, /codexPetOverlayKWinDragScript\(/); + assert.match(patched, /codexPetOverlayBeginKWinDrag\(/); + assert.match(patched, /codexPetOverlayEndKWinDrag\(/); +}); + +test("KWin hints target only the matching Plasma pet and apply its Wayland bounds", () => { + const calls = []; + let script = null; + const { controller } = controllerFromPatchedSource( + applyPetOverlayPatch(currentAvatarOverlayBundleFixture(), { + feature: { manifest: { petOverlay: { kwin: true, lockPosition: true } }, settings: {} }, + }), + { + process: { env: { XDG_CURRENT_DESKTOP: "KDE" } }, + childProcess: { + execFile(command, args, options, callback) { + calls.push([command, args]); + assert.equal(command, "qdbus6"); + assert.equal(options.timeout, 1500); + if (args.includes("org.kde.kwin.Scripting.loadScript")) script = fs.readFileSync(args[3], "utf8"); + callback(null, "ok"); + }, + }, + }, + ); + const window = { isDestroyed: () => false }; + controller.window = window; + controller.codexPetOverlayDesiredWindowBounds = { x: 610, y: 330, width: 356, height: 320 }; + + controller.codexPetOverlayApplyKWinHints(window); + + assert.deepEqual(calls.map(([, args]) => args[2]), [ + "org.kde.kwin.Scripting.loadScript", + "org.kde.kwin.Scripting.start", + "org.kde.kwin.Scripting.unloadScript", + ]); + const pet = { caption: "Codex Pet Overlay", frameGeometry: { x: 0, y: 0, width: 356, height: 320 }, pid: 4242 }; + const foreign = { caption: "ChatGPT", frameGeometry: {}, pid: 4242 }; + vm.runInNewContext(script, { workspace: { raiseWindow() {}, windowList: () => [foreign, pet] } }); + assert.equal(pet.keepAbove, true); + assert.equal(pet.noBorder, true); + assert.deepEqual(JSON.parse(JSON.stringify(pet.frameGeometry)), { x: 610, y: 330, width: 356, height: 320 }); + assert.equal(foreign.keepAbove, undefined); +}); + +test("KWin drag uses qdbus, cleans up its temporary script, and honors its runtime override", () => { + const calls = []; + let script = null; + const { controller } = controllerFromPatchedSource(applyPetOverlayPatch(currentAvatarOverlayBundleFixture()), { + process: { env: { XDG_CURRENT_DESKTOP: "KDE" } }, + childProcess: { + execFileSync(command, args, options) { + calls.push([command, args, options]); + if (args.includes("org.kde.kwin.Scripting.loadScript")) { + script = fs.readFileSync(args[3], "utf8"); + } + }, + }, + }); + const window = { + getContentBounds: () => ({ x: 145, y: 210, width: 356, height: 320 }), + isDestroyed: () => false, + }; + let persisted = false; + controller.window = window; + + controller.codexPetOverlayBeginKWinDrag(window); + const scriptPath = controller.codexPetOverlayKWinDragState.scriptPath; + assert.deepEqual(calls.slice(0, 2).map(([, args]) => args[2]), [ + "org.kde.kwin.Scripting.loadScript", + "org.kde.kwin.Scripting.start", + ]); + assert.equal(calls[0][0], "qdbus6"); + assert.equal(calls[0][2].timeout, 750); + assert.equal(controller.windowServerDragActive, true); + assert.equal(controller.windowServerDragWindowX, 145); + assert.ok(script); + + const cursorSignal = { callback: null, connect(callback) { this.callback = callback; }, disconnect() {} }; + const removedSignal = { connect() {} }; + const pet = { + caption: "Codex Pet Overlay", + frameGeometry: { x: 100, y: 200, width: 356, height: 320 }, + pid: 4242, + }; + const workspace = { + cursorPos: { x: 130, y: 250 }, + cursorPosChanged: cursorSignal, + raiseWindow() {}, + windowList: () => [pet], + windowRemoved: removedSignal, + }; + vm.runInNewContext(script, { workspace }); + assert.equal(typeof cursorSignal.callback, "function"); + workspace.cursorPos = { x: 300, y: 410 }; + cursorSignal.callback(); + assert.deepEqual( + JSON.parse(JSON.stringify(pet.frameGeometry)), + { x: 270, y: 360, width: 356, height: 320 }, + ); + + controller.codexPetOverlayQueueKWinDrag(window); + assert.equal(calls.length, 2, "pointer updates must not spawn compositor processes"); + assert.equal(fs.existsSync(scriptPath), true); + assert.equal(controller.codexPetOverlayEndKWinDrag(window, () => { persisted = true; }), true); + assert.equal(calls[2][1][2], "org.kde.kwin.Scripting.unloadScript"); + assert.equal(fs.existsSync(scriptPath), false); + assert.equal(persisted, true); + assert.equal(controller.codexPetOverlayKWinDragState, null); + + const overridden = controllerFromPatchedSource(applyPetOverlayPatch(currentAvatarOverlayBundleFixture()), { + process: { env: { XDG_CURRENT_DESKTOP: "KDE", CODEX_PET_OVERLAY_KWIN: "0" } }, + }).controller; + assert.equal(overridden.codexPetOverlayShouldUseKWin(), false); +}); + +test("KWin drag falls back without repeatedly probing missing qdbus commands", () => { + const patched = applyPetOverlayPatch(currentAvatarOverlayBundleFixture()); + let calls = 0; + const { controller } = controllerFromPatchedSource(patched, { + process: { env: { XDG_CURRENT_DESKTOP: "KDE" } }, + childProcess: { + execFileSync() { + calls += 1; + const error = new Error("qdbus missing"); + error.code = "ENOENT"; + throw error; + }, + }, + }); + const window = { isDestroyed: () => false }; + controller.window = window; + + controller.codexPetOverlayBeginKWinDrag(window); + + assert.equal(calls, 2); + assert.equal(controller.codexPetOverlayKWinDragState, undefined); + assert.equal(controller.windowServerDragActive, undefined); + + controller.codexPetOverlayBeginKWinDrag(window); + assert.equal(calls, 2); +}); diff --git a/scripts/patch-linux-window-ui.test.js b/scripts/patch-linux-window-ui.test.js index 82fcd4f93..1e6f282ea 100644 --- a/scripts/patch-linux-window-ui.test.js +++ b/scripts/patch-linux-window-ui.test.js @@ -3391,6 +3391,7 @@ test("adds Linux avatar overlay mouse passthrough recovery", () => { assert.match(patched, /codexLinuxSyncAvatarPointerInteractivity\(e\)/); assert.match(patched, /codexLinuxBuildAvatarInputShape\(e\)/); assert.match(patched, /codexLinuxApplyAvatarInputShape\(e\)/); + assert.match(patched, /codexLinuxShouldUseWholeWindowInput\(\)\{return this\.codexLinuxWholeWindowInput===!0\}/); assert.match(patched, /codexLinuxIsI3Session\(\)/); assert.match(patched, /process\.env\.I3SOCK/); assert.match(patched, /codexLinuxApplyAvatarCompositorHints\(e\)/); @@ -3479,6 +3480,126 @@ test("keeps the avatar overlay core patch idempotent after pet overlay compositi assert.deepEqual(warnings, []); }); +test("pet overlay opts into full-window input on X11 and Wayland", () => { + const patched = applyPetOverlayPatch( + applyLinuxOpaqueBackgroundPatch( + applyLinuxAvatarOverlayMousePassthroughPatch( + `${latestAvatarOverlayBundleFixture()}${currentOpaqueWindowSurfaceBackgroundBundle}`, + ), + ), + ); + const cursor = { x: 100, y: 100 }; + let ozonePlatform = "x11"; + const context = { + globalThis: {}, + clearInterval() {}, + process: { env: {}, platform: "linux" }, + require(moduleName) { + if (moduleName === "node:child_process") return { execFile() {} }; + assert.equal(moduleName, "electron"); + return { + app: { commandLine: { getSwitchValue: () => ozonePlatform }, getName: () => "Codex" }, + screen: { getCursorScreenPoint: () => cursor }, + }; + }, + setInterval() { + return { unref() {} }; + }, + }; + vm.runInNewContext(`${patched};globalThis.AvatarOverlayController=fV;`, context); + const controller = new context.globalThis.AvatarOverlayController( + { sendMessageToAllRegisteredWindows() {} }, + { set() {} }, + ); + controller.layout = { + mascot: { left: 220, top: 190, width: 113, height: 122 }, + tray: { left: 57, top: 55, width: 276, height: 131 }, + }; + const window = { + getContentBounds: () => ({ x: 0, y: 0, width: 356, height: 320 }), + isDestroyed: () => false, + isVisible: () => true, + setIgnoreMouseEvents() {}, + setShape() {}, + }; + + controller.codexPetOverlaySyncWindow(window); + assert.equal(controller.codexLinuxWholeWindowInput, true); + + assert.deepEqual(JSON.parse(JSON.stringify(controller.codexLinuxBuildAvatarInputShape(window))), [ + { x: 0, y: 0, width: 356, height: 320 }, + ]); + cursor.x = 10; + cursor.y = 300; + assert.equal(controller.codexLinuxIsCursorInAvatarInteractiveRegion(window), true); + ozonePlatform = "wayland"; + controller.pointerInteractive = false; + assert.equal(controller.codexLinuxIsAvatarShapeBackend(), false); + assert.equal(controller.codexLinuxSyncAvatarPointerInteractivity(window), true); + assert.equal(controller.pointerInteractive, true); +}); + +test("locked pet overlay keeps only mascot and tray interactive on X11 and Wayland", () => { + const patched = applyPetOverlayPatch( + applyLinuxOpaqueBackgroundPatch( + applyLinuxAvatarOverlayMousePassthroughPatch( + `${latestAvatarOverlayBundleFixture()}${currentOpaqueWindowSurfaceBackgroundBundle}`, + ), + ), + { feature: { manifest: { petOverlay: { lockPosition: true } }, settings: {} } }, + ); + const cursor = { x: 10, y: 300 }; + let ozonePlatform = "x11"; + const context = { + globalThis: {}, + clearInterval() {}, + process: { env: {}, platform: "linux" }, + require(moduleName) { + if (moduleName === "node:child_process") return { execFile() {} }; + assert.equal(moduleName, "electron"); + return { + app: { commandLine: { getSwitchValue: () => ozonePlatform }, getName: () => "Codex" }, + screen: { getCursorScreenPoint: () => cursor }, + }; + }, + setInterval() { + return { unref() {} }; + }, + }; + vm.runInNewContext(`${patched};globalThis.AvatarOverlayController=fV;`, context); + const controller = new context.globalThis.AvatarOverlayController( + { sendMessageToAllRegisteredWindows() {} }, + { set() {} }, + ); + controller.layout = { + mascot: { left: 220, top: 190, width: 113, height: 122 }, + tray: { left: 57, top: 55, width: 276, height: 131 }, + }; + const ignored = []; + const window = { + getContentBounds: () => ({ x: 0, y: 0, width: 356, height: 320 }), + isDestroyed: () => false, + isVisible: () => true, + setIgnoreMouseEvents: (...args) => ignored.push(args), + setShape() {}, + }; + + controller.codexPetOverlaySyncWindow(window); + assert.equal(controller.codexLinuxWholeWindowInput, false); + assert.deepEqual(JSON.parse(JSON.stringify(controller.codexLinuxBuildAvatarInputShape(window))), [ + { x: 220, y: 190, width: 113, height: 122 }, + { x: 57, y: 55, width: 276, height: 131 }, + ]); + + ozonePlatform = "wayland"; + controller.window = window; + controller.pointerInteractive = true; + assert.equal(controller.codexLinuxIsAvatarShapeBackend(), false); + assert.equal(controller.codexLinuxIsCursorInAvatarInteractiveRegion(window), false); + controller.applyPointerInteractivityPolicy(); + assert.deepEqual(JSON.parse(JSON.stringify(ignored)), [[true, { forward: true }]]); +}); + test("keeps Linux avatar overlay above the app while reply inputs are focusable", () => { const patched = applyPatchTwice( applyLinuxAvatarOverlayMousePassthroughPatch, @@ -3581,6 +3702,18 @@ test("Linux avatar overlay interactivity is bounded to avatar regions", () => { { x: 0, y: 0, width: 356, height: 320 }, ]); controller.dragState = null; + assert.equal(controller.codexLinuxShouldUseWholeWindowInput(), false); + controller.codexLinuxWholeWindowInput = true; + assert.deepEqual(serializeShape(controller.codexLinuxBuildAvatarInputShape(overlayWindow)), [ + { x: 0, y: 0, width: 356, height: 320 }, + ]); + assert.equal( + controller.codexLinuxIsCursorInAvatarInteractiveRegion({ + getContentBounds: () => ({ x: 5743, y: 936, width: 356, height: 320 }), + }), + true, + ); + controller.codexLinuxWholeWindowInput = false; context.process.env.WAYLAND_DISPLAY = "wayland-0"; assert.equal(controller.codexLinuxIsAvatarShapeBackend(), false); assert.equal(controller.codexLinuxApplyAvatarInputShape(overlayWindow), false); diff --git a/scripts/patches/impl/avatar-overlay.js b/scripts/patches/impl/avatar-overlay.js index 3f9cc06d9..405436047 100644 --- a/scripts/patches/impl/avatar-overlay.js +++ b/scripts/patches/impl/avatar-overlay.js @@ -77,11 +77,11 @@ function replaceAvatarMethodText(source, method, replacement) { } function avatarCursorRegionPatch(electronVar) { - return `codexLinuxIsCursorInAvatarInteractiveRegion(e){let t=this.layout;if(t==null)return!1;let __codexCursor=${electronVar}.screen.getCursorScreenPoint(),__codexBounds=e.getContentBounds(),__codexX=__codexCursor.x-__codexBounds.x,__codexY=__codexCursor.y-__codexBounds.y,__codexWindowHit=__codexX>=0&&__codexY>=0&&__codexX<=__codexBounds.width&&__codexY<=__codexBounds.height;if(!__codexWindowHit)return!1;let __codexHit=e=>e!=null&&__codexX>=e.left&&__codexX<=e.left+e.width&&__codexY>=e.top&&__codexY<=e.top+e.height;return __codexHit(t.mascot)||__codexHit(t.tray)}`; + return `codexLinuxIsCursorInAvatarInteractiveRegion(e){let t=this.layout;if(t==null)return!1;let __codexCursor=${electronVar}.screen.getCursorScreenPoint(),__codexBounds=e.getContentBounds(),__codexX=__codexCursor.x-__codexBounds.x,__codexY=__codexCursor.y-__codexBounds.y,__codexWindowHit=__codexX>=0&&__codexY>=0&&__codexX<=__codexBounds.width&&__codexY<=__codexBounds.height;if(!__codexWindowHit)return!1;if(this.codexLinuxShouldUseWholeWindowInput())return!0;let __codexHit=e=>e!=null&&__codexX>=e.left&&__codexX<=e.left+e.width&&__codexY>=e.top&&__codexY<=e.top+e.height;return __codexHit(t.mascot)||__codexHit(t.tray)}`; } function avatarInputShapePatch() { - return "codexLinuxBuildAvatarInputShape(e){let t=this.layout;if(t==null)return null;let r;try{r=e.getContentBounds()}catch{return null}if(r==null||!Number.isFinite(r.width)||!Number.isFinite(r.height))return null;if(this.dragState!=null)return[{x:0,y:0,width:r.width,height:r.height}];let i=e=>{if(e==null)return null;let t=Math.max(0,e.left),n=Math.max(0,e.top),i=Math.min(r.width,e.left+e.width)-t,a=Math.min(r.height,e.top+e.height)-n;return i<=0||a<=0?null:{x:t,y:n,width:i,height:a}};return[i(t.mascot),i(t.tray)].filter(Boolean)}"; + return "codexLinuxShouldUseWholeWindowInput(){return this.codexLinuxWholeWindowInput===!0}codexLinuxBuildAvatarInputShape(e){let t=this.layout;if(t==null)return null;let r;try{r=e.getContentBounds()}catch{return null}if(r==null||!Number.isFinite(r.width)||!Number.isFinite(r.height))return null;if(this.dragState!=null||this.codexLinuxShouldUseWholeWindowInput())return[{x:0,y:0,width:r.width,height:r.height}];let i=e=>{if(e==null)return null;let t=Math.max(0,e.left),n=Math.max(0,e.top),i=Math.min(r.width,e.left+e.width)-t,a=Math.min(r.height,e.top+e.height)-n;return i<=0||a<=0?null:{x:t,y:n,width:i,height:a}};return[i(t.mascot),i(t.tray)].filter(Boolean)}"; } function avatarApplyInputShapePatch() {