openai / openai/codex

[Windows][Pets] Pet overlay stays fully click-through: no input shape installed and mouse-move forwarding never wakes the renderer

Open
#43,061 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app bug pets windows-os
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

What version of the Codex App are you using (From “About Codex” dialog)?

26.901.41600 (Windows Store package OpenAI.Codex_26.901.5280.0_x64__2p2nqsd0c76g0, Chromium 152.0.7977.64)

What subscription do you have?
What platform is your computer?

Microsoft Windows NT 10.0.26200.0 x64

What issue are you seeing?

The desktop pet renders and animates correctly but is completely unreachable by the mouse: single click, drag and hover all do nothing. Every mouse event on the pet is delivered to the window underneath it.

This is not a custom-asset problem — everything drawn by the same avatar-overlay window is dead to the mouse: the mascot, the round expand/activity-stack button under it, and the activity pill above it.

Native Win32 measurements (read-only probes, per-monitor-DPI-aware-v2, physical pixels):

  1. The overlay window is in mouse passthrough state and has no input shape:
    hwnd 0xd0a78 | class Chrome_WidgetWin_1 | pid 30652
    style 0x14000000 (WS_VISIBLE | WS_CLIPSIBLINGS)
    exstyle 0x002800a8 -> WS_EX_TRANSPARENT = TRUE (+ TOPMOST | TOOLWINDOW | LAYERED | NOREDIRECTIONBITMAP)
    rect (1876,0)-(3072,1920) = 1196x1920 px = 598x960 DIP @ 200% scaling
    children: Chrome_RenderWidgetHostHWND 0x220950, Intermediate D3D Window 0xd07ee
    GetWindowRgn(hwnd, NULL) = 0 (NULLREGION)
    GetWindowRgnBox(hwnd, &rc) = 0 (NULLREGION) <- no clickable region at all

  2. WindowFromPoint grid over the whole overlay rect (8px step, 36000 samples): 0 samples (0.0%) resolve to the overlay.

  3. The visible mascot occupies physical x 2545-2650, y 1135-1375. 72 WindowFromPoint samples on it (and on the control button at 2665,1400) all resolve to a different top-level window (0x401306 = the Codex main window behind the pet). Clicking the pet clicks whatever is underneath.

  4. 25 seconds of real SendInput(MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE) motion parked on the mascot: WS_EX_TRANSPARENT never flipped once (0 transitions in 25 samples at 1 Hz). No hover reaction, pet control buttons never appear.

  5. The renderer is alive: the activity-pill text updates live with the running task and the mascot sprite state changed over several minutes. Not a hung/crashed renderer, and not a DPI offset (the overlay rect maps cleanly to 598x960 DIP and the mascot is well inside it).

  6. Reproduced after a full app restart (session started 2026-09-05T12:35:05Z). The avatar-overlay logger emitted zero lines in that session, so this was only diagnosable from external Win32 probes.

  7. Persisted state ~/.codex/.codex-global-state.json:
    "electron-avatar-overlay-open": true
    "electron-avatar-overlay-bounds": { x:1253, y:568, displayId:3815641688, placement:"top-end", displayBounds:{1536x960},
    byDisplayId: {
    "1029007254": { width:356, height:320, mascot:{left:243,top:189,width:113,height:123}, tray:{...} },
    "3815641688": { x:1253, y:568, placement:"top-end" } // current display: no width/height/mascot/tray
    } }
    Same physical resolution, but the displayId changed (a display re-enumeration happened at 2026-09-05T03:44:31Z; the only real overlay log line that day is info [avatar-overlay] Discarding pending pet move for display change), and for the new displayId the mascot geometry / pointer regions were never (re)published.

Code path (app.asar main bundle, logger avatar-overlay):

applyPointerInteractivityPolicy() {
const shapeApplied = this.applyInputShape(win);
if (!win.isVisible()) { win.setIgnoreMouseEvents(true, { forward: false }); return; }
if (shapeApplied) return; // native input shape short-circuits
const next = this.pointerInteractive ? 'disabled' : 'forwarding';
if (this.mousePassthroughMode !== next) {
this.mousePassthroughMode = next;
if (next === 'forwarding') { win.setIgnoreMouseEvents(true, { forward: true }); return; }
win.setIgnoreMouseEvents(false);
this.refreshCursorAtCurrentMousePosition(win);
}
}

applyInputShape(win) {
if (!this.supportsInputShape || this.inputShape == null) return false;
if (this.mousePassthroughMode !== 'disabled') { win.setIgnoreMouseEvents(false); this.mousePassthroughMode = 'disabled'; }
const ok = win.setInputShape(this.inputShape.map(({ height, left, top, width }) => ({ height, width, x: left, y: top })));
return ok;
}

setPointerRegions(webContentsId, rects, proximityRects) {
const win = this.window;
if (!win || win.isDestroyed() || win.webContents.id !== webContentsId) return; // silently dropped
this.inputShape = rects;
this.applyPointerInteractivityPolicy();
}

inputShape is reset to null in createWindow(), which also resets layoutMode='legacy', mousePassthroughMode='disabled', pointerInteractive=false.

The observed state (WS_EX_TRANSPARENT=1 AND NULLREGION) is only explainable by applyInputShape() returning false, i.e. one of:
(a) supportsInputShape === false;
(b) inputShape === null because the renderer's setPointerRegions report was never received/replayed for the current window instance (it is silently dropped on webContents.id mismatch, and createWindow() clears it);
(c) inputShape === [] and win.setInputShape([]) returns false. [] is what the renderer reports when no [data-avatar-overlay-hit-region] element passes its visibility filter (ec() requires pointer-events !== 'none', not inside [inert], non-zero size).

Renderer hover detection (use-floating-window-pointer-interactivity-*.js) is driven exclusively by window.addEventListener('mousemove', ...) + document.elementsFromPoint(...), so while the window is click-through it depends entirely on Electron's { forward: true } delivery - which measurement 4 shows is not arriving.

The deadlock: passthrough -> no mousemove -> pointerInteractive stays false -> stays passthrough. The only thing that can break the loop is the native input shape, which is not installed.

What steps can reproduce the bug?

Not a session/token issue (this is the desktop overlay window; no session id, token limit or context window usage involved).

  1. Windows 11, Codex desktop app from the Windows Store, pet enabled in settings, any pet (built-in or custom).
  2. Let a task run so the pet shows an activity pill, then let it go idle.
  3. Try to click the pet, drag the pet, or hover the pet.
  4. Nothing happens; the window behind the pet receives the click.

Verify with a read-only probe (Python 3, stdlib only):

import ctypes
from ctypes import wintypes
u = ctypes.windll.user32
u.SetProcessDpiAwarenessContext(ctypes.c_void_p(-4))
u.GetWindowLongW.restype = ctypes.c_long
u.WindowFromPoint.restype = wintypes.HWND
u.GetAncestor.restype = wintypes.HWND
u.GetAncestor.argtypes = [wintypes.HWND, ctypes.c_uint]

PROC = ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM)
ov = []
def cb(h, l):
if not u.IsWindowVisible(h): return True
cn = ctypes.create_unicode_buffer(64); u.GetClassNameW(h, cn, 64)
if cn.value != 'Chrome_WidgetWin_1': return True
ex = u.GetWindowLongW(h, -20) & 0xFFFFFFFF
if (ex & 0x8) and (ex & 0x80): ov.append((h, ex)) # TOPMOST + TOOLWINDOW = avatar overlay
return True
u.EnumWindows(PROC(cb), 0)
h, ex = ov[0]
r = wintypes.RECT(); u.GetWindowRect(h, ctypes.byref(r))
print('overlay', hex(h), 'exstyle 0x%08x' % ex, 'TRANSPARENT =', bool(ex & 0x20))
hits = total = 0
for y in range(r.top + 4, r.bottom, 8):
for x in range(r.left + 4, r.right, 8):
w2 = u.WindowFromPoint(wintypes.POINT(x, y)); total += 1
if w2 and u.GetAncestor(w2, 2) == h: hits += 1
print('hit-test: %d/%d points resolve to the overlay' % (hits, total))

Healthy: a small island around the mascot resolves to the overlay, and moving the cursor onto the pet clears WS_EX_TRANSPARENT.
This bug: 0 points resolve, WS_EX_TRANSPARENT never clears.

What is the expected behavior?
  • The mascot's visible area (and the pet controls / activity pill drawn by the same overlay) should accept hover, click and drag for the whole lifetime of the overlay window.
  • WS_EX_TRANSPARENT should be cleared as soon as the cursor is over the pet, without depending on {forward: true} mouse-move delivery actually reaching the renderer.
  • If setInputShape() fails or inputShape is empty, the overlay must not fall back to permanent whole-window click-through; it should stay interactive (or at least self-correct), and log an error.
  • Pointer regions / mascot geometry should be re-published (or replayed by the main process) after a display re-enumeration, a webContents.id change, or createWindow().
Additional information

Likely duplicates - happy to close this if one of these is the canonical tracker:
#41513 (most active, 25 comments), #41465 (19), #41960 (15), #42661, #42190, #41535, #42061, #42927, #41596, #42230, #42923.

What is new here:

  1. GetWindowRgnBox() == NULLREGION proves the native input shape is not installed at all, so this is not an "offset region" variant like #42661 / #42190 - there is no region.
  2. 25s of real SendInput mouse motion on the mascot produced 0 WS_EX_TRANSPARENT transitions, showing the {forward: true} fallback is not delivering mouse-move either. Both paths dead at once is what makes the state unrecoverable, and it survives an app restart here.
  3. #42927 reports "responds to hover but cannot be clicked" - i.e. forwarding works there. Here even hover is dead, so it is a deeper failure mode.
  4. Correlates with a display re-enumeration: displayId changed under the same 1536x960 DIP resolution and the persisted byDisplayId[] entry never gained width/height/mascot/tray.
  5. Single monitor at 200% scaling, so multi-monitor coordinate mapping is not involved.

Suggested fixes

  1. Do not rely on {forward: true} as the only hover source. While mousePassthroughMode === 'forwarding', run a standing timer (the app already has petControlsProximityTimer polling screen.getCursorScreenPoint()) that feeds the renderer via the existing sendCursorPointToAvatarOverlay() / webContents.sendInputEvent({type:'mouseMove'}).
  2. Make setPointerRegions replayable: cache the last rects and re-apply on createWindow() / display change / webContents.id change; log a warning instead of silently dropping on id mismatch.
  3. Never fall back to whole-window {forward: true} passthrough when setInputShape() fails or inputShape is empty.
  4. Make passthrough application idempotent against the real window exstyle instead of the JS-side mousePassthroughMode string. createWindow() sets it to 'disabled' while the window may actually be transparent, so the policy then never calls setIgnoreMouseEvents(false).
  5. Log applyPointerInteractivityPolicy / applyInputShape / setPointerRegions inputs and return values. The avatar-overlay logger produced zero output for the whole session.

Attachments: evidence-hit-test.png (green box = visible mascot, yellow dots = 72 WindowFromPoint samples, none hitting the overlay, red box = overlay window bounds), evidence-fullscreen.png.

Image Image

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the avatar-overlay entry points in the app.asar main bundle, especially applyPointerInteractivityPolicy, applyInputShape, setPointerRegions, and createWindow; then inspect use-floating-window-pointer-interactivity-*.js. Reproduce with the supplied Python Win32 probe and review the avatar-overlay logger output. Done means the overlay receives hit tests and hover, click, and drag remain usable after startup, display changes, and renderer or window recreation.

Written by the indexing model from the issue text.

Assessment

Tech stack
electron, javascript
Domain
desktop, operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.